これは フェンリル デザインとテクノロジー Advent Calendar 2022 2日目の記事です。
GIMLE チームの野田です。 GIMLE では、お客様によるクラウドサービスを活用したビジネスの推進を支援しています。 この記事では、以前お客様が DevOps 領域で抱えていた課題を題材に、クラウドサービスを活用して解決する方法を紹介します。
背景
アプリケーションのコードを変更し、すぐにビルド・テスト・デプロイできるようにする CI/CD は、ビジネスニーズに即したサービスを提供し続ける上で、もはや必須とも言える開発技法です。 AWS では、AWS CodeCommit・AWS CodeBuild・AWS CodeDeploy・AWS CodePipeline といった Code シリーズが提供されているため、サービスの提供に必要な一通りの CI/CD 環境を整えることができます。
AWS では、各システム毎や環境毎に AWS アカウントを分離していくことが推奨されています。 この方針に従うと、各 AWS アカウントで個別に CI/CD フローを構築していくことになります。 しかし、各システムのコードリポジトリは、システム毎に用意するよりも、組織内での共有を図るため、共通でアクセスできる場所に用意しておきたくなるケースがあるかと思います。 また、システムが不要になった際にもコードは残しておきたいはずですが、AWS アカウントごと削除することになっても、コードが中央のリポジトリで管理されていれば、退避させずに済みます。
フェンリルでは、組織内のコードリポジトリとして主に GitHub Enterprise Server、または GitHub Enterprise Cloud を利用し、CodeBuild 等と連携させて CI/CD を構成しています。 一方、先述のお客様は、組織内のコードリポジトリとして Backlog で提供される Git リポジトリを使用し、AWS 上で構築したシステムへのデプロイをしたいと考えておられました。
CodePipeline や CodeBuild は、ソースリポジトリとして GitHub・GitHub Enterprise・Bitbucket などをサポートしているため、簡単に連携できます。 また、GitLab ではリポジトリのミラーリング機能が提供されているため、GitLab と CodeCommit のリポジトリを同期させ、CI/CD フローを構成することができます。 しかし、Backlog では上記のようなサポートが提供されていないため、どうにか Code シリーズへ効率良く連携する方法がないかと調べ、以下に紹介する方法を考えてみました。
方針
API Gateway + Lambda
まず思いつく方法としては、Backlog の Webhook を Amazon API Gateway と AWS Lambda で処理して、リポジトリのデータを取得する方法です。 これは、クラスメソッドさんのブログでも紹介されています。
この方法の懸念点として、毎回のソースコードの変更は部分的であるにも関わらず、リポジトリ全体を取得してしまうため、データ転送量が多くなってしまいがちです。 この対策としては、Git の Shallow Clone や Partial Clone といった機能を利用し、ビルドに必要なデータのみ取得する方法が挙げられます。 また、実際に試してはいないですが、Lambda Function から Amazon EFS のファイルシステムをマウントし、以前に取得した時点から更新差分のみ取得できるようにすると、データ転送量を抑えて、効率良く扱えそうです。 ただ、ここまで考慮してコードを記述するのは、なかなか骨が折れそうですね。
API Gateway + CodeBuild
ところで、API Gataway には、別の AWS サービスを呼び出すための、サービスプロキシとして使う方法があるのをご存知でしょうか。 この機能を利用すると、Lambda に限らず、様々な AWS のサービスを API Gateway から呼び出すことができます。
ここでは、Backlog の Webhook を API Gateway で受けて CodeBuild を動作させ、Backlog の Git リポジトリが更新される度に、CodeCommit に自動的に同期させる方法を考えてみます。 リポジトリの同期処理に Lambda ではなく CodeBuild を利用するのは、CodeBuild が備える Amazon S3 によるキャッシュ機能を利用したいためです。 また、CodeBuild でソースコードを取得して直接アプリケーションをビルドしても良いのですが、一旦 CodeCommit にソースコードをミラーリングすることで、Amazon CodeGuru Reviewer によるコードの分析機能を利用できるようにもなります。
この記事の後半では、これらの構成を実現する CloudFormation テンプレートも紹介します。
必要なリソースを準備
前提
アプリケーションのソースコードが保管されている Backlog のプロジェクトとリポジトリは既にあると想定します。
本記事では CodeBuild を VPC 外で動作させますが、Backlog の接続元 IP アドレスによるアクセス制限機能を利用している場合は、CodeBuild を Elastic IP を割り当てた NAT Gateway のある VPC 内で動作させ、アクセス元 IP アドレスを制限するなどの追加設定が必要になると考えられます。
Backlog の認証情報を登録
本記事では、Backlog の Git リポジトリには、公開鍵認証による SSH でアクセスします。
ssh-keygen
コマンド等で鍵ペアを生成し、Backlog の個人設定から公開鍵を登録しておきます。
秘密鍵は、Systems Manager のパラメータストアに、安全な文字列として登録します。
CodeCommit リポジトリを作成
ミラー先の CodeCommit リポジトリを作成します。
S3 バケットを作成
CodeBuild のビルドキャッシュを保管する、空の S3 バケットを作成します。
S3 バケットのライフサイクルルールで、保管してから一定期間が経過したオブジェクトを自動削除するように設定しておくと、不要になったオブジェクトが自動的に削除されて良いでしょう。
CodeBuild でリポジトリをミラーする
CodeBuild で使う IAM ロールを作成します。
信頼されたエンティティには、CodeBuild(codebuild.amazonaws.com
)を指定します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRole", "Principal": { "Service": "codebuild.amazonaws.com" } } ] }
許可ポリシーでは、以下のサービスへのアクセス権限を割り当てます。
<...>
の箇所は、自身の環境に合わせて書き換えてください。
- CloudWatch Logs へのログ出力
- キャッシュ保管用 S3 バケットへの読み書き
- CodeCommit リポジトリへの Push
- Systems Manager パラメータストアで暗号化して保管する SSH 秘密鍵の取得
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject" ], "Resource": "arn:aws:s3:::<s3-cache-bucket-name>/*" }, { "Effect": "Allow", "Action": "codecommit:GitPush", "Resource": "arn:aws:codecommit:<aws-region>:<aws-account-id>:<codecommit-repository-name>" }, { "Effect": "Allow", "Action": "ssm:GetParameters", "Resource": "arn:aws:ssm:<aws-region>:<aws-account-id>:parameter/<ssm-parameter-name-without-first-slash>" }, { "Effect": "Allow", "Action": "kms:Decrypt", "Resource": "*", "Condition": { "StringLike": { "kms:RequestAlias": "aws/ssm" } } } ] }
IAM ロールを作成したら、次は CodeBuild でビルドプロジェクトを作成します。 ソースプロバイダの指定は不要です。
ビルド環境には Amazon Linux 2 ベースのイメージ(amazonlinux2-aarch64-standard:2.0
)を使用します。
IAM ロールには、先ほど作成した CodeBuild 用のロールを指定します。
Buildspec には、以下のビルドコマンドを挿入します。
env:
ブロック内の <...>
の箇所は、自身の環境に合わせて書き換えてください。
version: 0.2 env: variables: BACKLOG_SPACE_KEY: "<backlog-space-key>" # xxx.backlog.zzz の xxx BACKLOG_DOMAIN_NAME: "<backlog-domain-name>" # backlog.com または backlog.jp BACKLOG_PROJECT_KEY: "<backlog-project-key>" BACKLOG_REPOSITORY_NAME: "<baclog-repository-name>" CODECOMMIT_REPOSITORY_NAME: "<codecommit-repository-name>" parameter-store: SRC_GIT_CREDENTIAL: "<systems-manager-secure-parameter-name>" phases: install: commands: - pip3 install git-remote-codecommit pre_build: commands: - eval $(ssh-agent) - ssh-add - <<< "$SRC_GIT_CREDENTIAL" - ssh-keyscan "${BACKLOG_SPACE_KEY}.git.${BACKLOG_DOMAIN_NAME}" >> ~/.ssh/known_hosts build: commands: | if [ ! -d "${BACKLOG_REPOSITORY_NAME}.git" ]; then git clone --mirror "${BACKLOG_SPACE_KEY}@${BACKLOG_SPACE_KEY}.git.${BACKLOG_DOMAIN_NAME}:/${BACKLOG_PROJECT_KEY}/${BACKLOG_REPOSITORY_NAME}.git" cd "${BACKLOG_REPOSITORY_NAME}.git" git remote add codecommit "codecommit::${AWS_REGION}://${CODECOMMIT_REPOSITORY_NAME}" else cd "${BACKLOG_REPOSITORY_NAME}.git" git fetch --prune origin fi git push --mirror --prune codecommit cache: paths: - ./**/* - /root/.cache/pip/**/*
Buildspec の内容を解説しておきます。
parameter-store ブロックで指定している環境変数には、Systems Manager パラメータトアから取得して復号された値がセットされます。 この値は機密情報のため、ログに出力されてしまうとマズいですが、CodeBuild では自動的に **** のようにマスクされるため、安全に扱うことができます。
install フェーズで追加している git-remote-codecommit
は、AWS の一時的な認証情報を使用して CodeCommit にアクセスするために、Git を拡張するユーティリティです。
pre_build フェーズでは ssh-agent 動作させ、ソースリポジトリへアクセスするための SSH 秘密鍵を読み込ませています。
また、ssh-keyscan
コマンドで、接続先のサーバーの公開鍵を取得し、known_hosts ファイルを生成しています。
build フェーズでは、 ソースリポジトリ名.git というディレクトリの有無を確認し、初回実行時のようにディレクトリが存在しない場合は git clone --mirror
コマンドを実行し、ソースリポジトリのベアリポジトリ(作業ディレクトリを持たないリポジトリ)をローカルに作成します。
その後、 git remote add
コマンドで CodeCommit をリモートリポジトリとして登録し、 git push --mirror
コマンドで CodeCommit にリポジトリの内容を登録します。
build フェーズで ソースリポジトリ名.git というディレクトリが既に存在する場合は、前回実行時のキャッシュが復元されている状態のため、git fetch
コマンドでソースリポジトリの更新を取得します。
--prune
オプションを付与しているため、ソースリポジトリでブランチや Pull Request が削除された場合は、その状態も同期されます。
cache ブロックでは、次回実行時のデータ転送量を抑えて効率化するため、リポジトリの内容と、Python パッケージインストール時のキャッシュ領域を保存するように指定しています。
ビルドプロジェクトの設定に戻ります。 このビルドプロジェクトにはビルドアーティファクト(成果物)はありませんが、先の cache ブロックで指定したデータを保管するため、事前に作成しておいた S3 バケットでのキャッシュを有効化しておきます。
ここまで入力してビルドプロジェクトを作成したら、一旦手動でビルドを開始し、動作を確認してみましょう。
ビルドが正常に完了すると、CodeCommit リポジトリに Backlog リポジトリの内容が同期されているでしょう。
また、キャッシュ保管用の S3 バケットには、UUID が付与されたオブジェクトが生成されているはずです。
さらに、CloudWatch ロググループには、/aws/codebuild/<build-project-name>
というロググループが生成され、ビルド時のログが出力されていることが期待されます。
CloudWatch ロググループのログ保持期間は、デフォルトでは無期限になっているため、一定期間が経過した場合に自動削除するように設定しておくと良いでしょう。
ビルドがうまくいかなかった場合は、各設定が合っているか、出力されたログなどを頼りに確認してみてください。
API Gateway から CodeBuild を動作させる
CodeBuild が正常に動作することが確認できたら、次は API Gateway の設定をしていきます。
API Gateway に CodeBuild のプロジェクトを開始する権限を付与するため、API Gateway 用の IAM ロールを作成します。
信頼されたエンティティには、API Gateway(apigateway.amazonaws.com
)を指定します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRole", "Principal": { "Service": "apigateway.amazonaws.com" } } ] }
許可ポリシーでは、CodeBuild のプロジェクトを開始する権限を付与します。
<...>
の箇所は、自身の環境に合わせて書き換えてください。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "codebuild:StartBuild", "Resource": "arn:aws:codebuild:<aws-region>:<aws-account-id>:project/<codebuild-project-name>" } ] }
API Gateway のログを CloudWatch に出力する場合は、マネージドポリシーの AmazonAPIGatewayPushToCloudWatchLogs
を付与しておくと良いでしょう。
IAM ロールを作成したら、API Gateway で REST API を作成していきます。
リソースの /
直下に POST
メソッドを追加します。
追加した POST
メソッドの統合タイプで AWS サービス を選択し、以下のパラメータを設定していきます。
- AWS リージョン:CodeBuild でビルドプロジェクトを作成したリージョン
- AWS サービス:CodeBuild
- AWS サブドメイン:(指定しない)
- HTTP メソッド:POST
- アクションの種類:アクション名の使用
- アクション:StartBuild
- 実行ロール:上記で作成した API Gateway 用 IAM ロールの ARN
- コンテンツの種類:パススルー
- デフォルトタイムアウトの使用:✓
ここで、アクションに入力する内容は、API Gateway の API Reference から、該当するものを探しています(IAM ロール作成時の許可ポリシーで設定した Action とも適合していますね)。
このあと、セットアップした POST メソッドの編集画面から統合リクエストを修正するのですが、その前に StartBuild を呼び出すために必要なパラメータを確認します。
StartBuild の API Reference を確認すると、リクエストに必須のパラメータ(Required: Yes の項目)は projectName
のみです。
CloudShell を開き、以下の通り AWS CLI でデバッグモードを有効化(--debug
)して start-build
サブコマンドを実行し、その際に生成される HTTP リクエストヘッダーを確認します(パラメータが正しい場合は CodeBuild ビルドプロジェクトが実行されます)。
$ aws codebuild start-build \ --region ap-northeast-1 \ --project-name mirror-repository-sample \ --debug \ |& grep 'Making request for OperationModel'
ここで重要なのが、X-Amz-Target: CodeBuild_20161006.StartBuild
と Content-Type: application/x-amz-json-1.1
の部分です。
では、必要なパラメータが確認できたので、統合リクエストを修正していきます。
先の手順で確認した、CodeBuild StartBuild API を呼び出すために必要な 2 つのパラメータを、HTTP ヘッダー に追加します。
マッピング元の値は固定の文字列にしたいため、'
(シングルクォート)で囲みます。
マッピングテンプレート では、Content-Type: application/x-www-form-urlencoded
に対して、リクエスト本文として CodeBuild StartBuild API の必須パラメータより、{"projectName": "<build-project-name>"}
を設定します。
例によって、<build-project-name>
のところは自身のビルドプロジェクトの名前に置き換えてください。
ここまでの設定ができたら、API をデプロイします。
ステージエディター画面でデプロイされた API の URL を確認し、CloudShell から curl
コマンドを使って API の動作を確認します。
$ curl -XPOST "https://<rest-api-id>.execute-api.<aws-region>.amazonaws.com/<stage-name>" \ -H 'content-type: application/x-www-form-urlencoded' \ -d ''
デプロイした REST API から、CodeBuild ビルドプロジェクトが実行されていれば成功です🎉
あとは、デプロイした REST API のエンドポイントを Backlog の Git の設定から WebフックURL に登録し、Backlog の Git リポジトリへの操作が CodeCommit リポジトリに反映されるかを確認していきましょう。 新しいコミットの Push、ブランチの作成、プルリクエストの作成、マージ、ブランチの削除などの操作が期待通りに反映されるか、試してみてください。
なお、ここで構成した API Gateway REST API のエンドポイントはインターネット向けに公開されています。 不正アクセスのリスクを低減させるためにアクセス制限が必要であれば、AWS WAF を適用するなどの対応を検討してください。
また、Backlog の Git リポジトリは LFS (Large File Storage) に対応 していますが、本記事で紹介した仕組みだけでは、LFS オブジェクトを転送することができないため、ご注意ください。
CloudFormation で構築する
ここまで、(仕組みを理解するために)手作業で構築してきましたが、できれば IaC ツールを使って再利用可能なテンプレートとして記述し、必要な時にすぐに構築できるようにしておきたいところです。 ということで、AWS CloudFormation のテンプレートに起こしてみました🙌
先の構成では、CodeBuild で Backlog から取得した Git リポジトリのデータを S3 バケットでキャッシュしていましたが、リポジトリのサイズが大きい場合はキャッシュデータの転送コストが大きくなってしまう課題があります。 この課題に対応するため、Amazon EFS を利用し、CodeBuild のビルドプロジェクトから共有ファイルシステムをマウントし、そこにリポジトリの一時データを置くようにしています。
EFS を利用するため、CodeBuild ビルドプロジェクトを VPC 内で動作させるための設定を加えていますが、API Gateway + CodeBuild の基本的な構成は、本記事で解説したものに沿っているため、興味のある方はテンプレートファイルを眺めてみて、もし使えそうであれば使ってみてください。
AWSTemplateFormatVersion: "2010-09-09" Description: "AWS CodeCommit repository synchronized with Backlog" # SPDX-FileCopyrightText: 2022 Fenrir Inc. # SPDX-License-Identifier: Apache-2.0 Parameters: ProjectName: Description: "Project name" Type: "String" Default: "devops-demo" LogExpirationInDays: Description: "Number of days to keep logs" Type: "Number" Default: 30 BacklogSpaceKey: Description: "Backlog space key" Type: "String" AllowedPattern: '^[0-9a-zA-Z-]*$' BacklogDomainName: Description: "Backlog domain name" Type: "String" AllowedValues: - "backlog.jp" - "backlog.com" BacklogProjectKey: Description: "Code name of the Backlog project" Type: "String" BacklogRepositoryName: Description: "Git repository name of the Backlog project" Type: "String" BacklogUserCredentialKey: Description: "Path to the Systems Manager parameter store where the Backlog user credential is stored (must begin with /)" Type: "String" AllowedPattern: '^/.*' VpcId: Description: "VPC Id" Type: "AWS::EC2::VPC::Id" PrivateSubnetId1: Description: "1st private subnet id" Type: "AWS::EC2::Subnet::Id" PrivateSubnetId2: Description: "2nd private subnet id" Type: "AWS::EC2::Subnet::Id" CodeCommitRepositoryName: Description: "CodeCommit repository name" Type: "String" Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: { default: "General" } Parameters: - "ProjectName" - "LogExpirationInDays" - Label: { default: "Backlog" } Parameters: - "BacklogSpaceKey" - "BacklogDomainName" - "BacklogProjectKey" - "BacklogRepositoryName" - "BacklogUserCredentialKey" - Label: { default: "VPC" } Parameters: - "VpcId" - "PrivateSubnetId1" - "PrivateSubnetId2" - Label: { default: "AWS CodeCommit" } Parameters: - "CodeCommitRepositoryName" Resources: # CodeCommit repository CodeCommitRepository: Type: "AWS::CodeCommit::Repository" UpdateReplacePolicy: "Delete" DeletionPolicy: "Delete" Properties: RepositoryName: !Ref "CodeCommitRepositoryName" # S3 bucket for storing build cache BuildCacheBucket: Type: "AWS::S3::Bucket" Properties: BucketName: !Sub "${ProjectName}-build-cache-${AWS::AccountId}" OwnershipControls: { Rules: [ ObjectOwnership: "BucketOwnerEnforced" ]} # disable ACL BucketEncryption: { ServerSideEncryptionConfiguration: [ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" }]} PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true } LifecycleConfiguration: Rules: - Status: "Enabled" ExpirationInDays: 30 # delete current version after 1 month AbortIncompleteMultipartUpload: { DaysAfterInitiation: 7 } # Security Group for EFS SharedFileSystemSecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupName: !Sub "${ProjectName}-file-sharing" GroupDescription: !Sub "${ProjectName}-file-sharing" VpcId: !Ref "VpcId" SecurityGroupEgress: - CidrIp: "127.0.0.1/32" Description: "block default outbound" IpProtocol: "-1" Tags: [{ Key: "Name", Value: !Sub "${ProjectName}-file-sharing" }] # EFS shared file system SharedFileSystem: Type: "AWS::EFS::FileSystem" DeletionPolicy: "Delete" UpdateReplacePolicy: "Delete" Properties: PerformanceMode: "generalPurpose" ThroughputMode: "bursting" Encrypted: true BackupPolicy: { Status: "DISABLED" } FileSystemTags: [{ Key: "Name", Value: !Sub "${ProjectName}-${CodeCommitRepositoryName}" }] # EFS mount target MountTarget1: Type: "AWS::EFS::MountTarget" Properties: FileSystemId: !Ref "SharedFileSystem" SubnetId: !Ref "PrivateSubnetId1" SecurityGroups: [ !Ref "SharedFileSystemSecurityGroup" ] MountTarget2: Type: "AWS::EFS::MountTarget" Properties: FileSystemId: !Ref "SharedFileSystem" SubnetId: !Ref "PrivateSubnetId2" SecurityGroups: [ !Ref "SharedFileSystemSecurityGroup" ] # Security Group for CodeBuild BuildSecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupName: !Sub "${ProjectName}-codebuild" GroupDescription: !Sub "${ProjectName}-codebuild" VpcId: !Ref "VpcId" SecurityGroupEgress: - Description: "allow SSH access to internet" CidrIp: "0.0.0.0/0" IpProtocol: "tcp" FromPort: 22 ToPort: 22 - Description: "allow HTTPS access to internet" CidrIp: "0.0.0.0/0" IpProtocol: "tcp" FromPort: 443 ToPort: 443 Tags: [{ Key: "Name", Value: !Sub "${ProjectName}-codebuild" }] # Allow NFS communication from CodeBuild to EFS BuildSGEgressAllowNFSToSharedFileSystemSG: Type: "AWS::EC2::SecurityGroupEgress" Properties: Description: "allow NFS access to shared file system" GroupId: !GetAtt "BuildSecurityGroup.GroupId" DestinationSecurityGroupId: !GetAtt "SharedFileSystemSecurityGroup.GroupId" IpProtocol: "tcp" FromPort: 2049 ToPort: 2049 SharedFileSystemSGIngressAllowNFSFromBuildSG: Type: "AWS::EC2::SecurityGroupIngress" Properties: Description: "allow NFS access from codebuild" GroupId: !GetAtt "SharedFileSystemSecurityGroup.GroupId" SourceSecurityGroupId: !GetAtt "BuildSecurityGroup.GroupId" IpProtocol: "tcp" FromPort: 2049 ToPort: 2049 # IAM role for CodeBuild BuildRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub "${ProjectName}-codebuild" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: { Service: "codebuild.amazonaws.com" } Action: "sts:AssumeRole" Policies: - PolicyName: "Base" PolicyDocument: Version: "2012-10-17" Statement: # CloudWatch Logs - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*" # S3 BuildCacheBucket - Effect: "Allow" Action: - "s3:GetObject" - "s3:PutObject" Resource: !Sub "${BuildCacheBucket.Arn}/*" # CodeCommit - Effect: "Allow" Action: "codecommit:GitPush" Resource: !GetAtt "CodeCommitRepository.Arn" # Systems Manager Secure Parameter - PolicyName: "SSMSecureParameter" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "ssm:GetParameters" Resource: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${BacklogUserCredentialKey}" - Effect: "Allow" Action: "kms:Decrypt" Resource: "*" Condition: StringLike: kms:RequestAlias: "aws/ssm" # Required when accessing and debugging the build environment with Systems Manager Session Manager - PolicyName: "SSMSessionManager" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "ssmmessages:CreateControlChannel" - "ssmmessages:CreateDataChannel" - "ssmmessages:OpenControlChannel" - "ssmmessages:OpenDataChannel" Resource: "*" # Required when launching a build project in a VPC - PolicyName: "VPCAccess" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "ec2:CreateNetworkInterface" - "ec2:DescribeDhcpOptions" - "ec2:DescribeNetworkInterfaces" - "ec2:DeleteNetworkInterface" - "ec2:DescribeSubnets" - "ec2:DescribeSecurityGroups" - "ec2:DescribeVpcs" Resource: "*" - Effect: "Allow" Action: "ec2:CreateNetworkInterfacePermission" Resource: !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/*" Condition: StringEquals: ec2:Subnet: - !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateSubnetId1}" - !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateSubnetId2}" ec2:AuthorizedService: "codebuild.amazonaws.com" # CodeBuild project to mirror the Backlog Git repository # If you want to pause a build to debug, enable session connection from the "Advanced build overrides" option, add codebuild-breakpoint line to Buildspec and run MirrorRepositoryProject: Type: "AWS::CodeBuild::Project" Properties: Name: !Sub "${ProjectName}-mirror-backlog-repository" Description: "mirror git repository" ServiceRole: !GetAtt "BuildRole.Arn" Source: Type: "NO_SOURCE" BuildSpec: !Sub | version: 0.2 env: parameter-store: SRC_GIT_CREDENTIAL: "${BacklogUserCredentialKey}" phases: install: commands: - pip3 install git-remote-codecommit pre_build: commands: - eval $(ssh-agent) - ssh-add - <<< "$SRC_GIT_CREDENTIAL" - ssh-keyscan ${BacklogSpaceKey}.git.${BacklogDomainName} >> ~/.ssh/known_hosts build: commands: - cd "$CODEBUILD_REPO_DIR" - | set -eux if [ ! -e ${BacklogRepositoryName}.git ]; then git clone --mirror "${BacklogSpaceKey}@${BacklogSpaceKey}.git.${BacklogDomainName}:/${BacklogProjectKey}/${BacklogRepositoryName}.git" cd ${BacklogRepositoryName}.git git remote add codecommit codecommit::${AWS::Region}://${CodeCommitRepository.Name} else cd ${BacklogRepositoryName}.git git fetch --prune origin fi set +x - git push --mirror --prune codecommit # post_build: # artifacts: cache: paths: - /root/.cache/pip/**/* Environment: Type: "ARM_CONTAINER" Image: "aws/codebuild/amazonlinux2-aarch64-standard:2.0" ComputeType: "BUILD_GENERAL1_SMALL" PrivilegedMode: true # Access to EFS requires privileges VpcConfig: VpcId: !Ref "VpcId" Subnets: [ !Ref "PrivateSubnetId1", !Ref "PrivateSubnetId2" ] SecurityGroupIds: [ !GetAtt "BuildSecurityGroup.GroupId" ] TimeoutInMinutes: 5 QueuedTimeoutInMinutes: 10 FileSystemLocations: - Identifier: "repo_dir" # The environment variable CODEBUILD_<Identifier> points to a mount point Type: "EFS" Location: !Sub "${SharedFileSystem}.efs.${AWS::Region}.amazonaws.com:/" # efs-dns-name:/[directory-path] MountPoint: "/mnt/shared" Artifacts: { Type: "NO_ARTIFACTS" } Cache: { Type: "S3", Location: !Sub "${BuildCacheBucket.Arn}" } LogsConfig: { CloudWatchLogs: { Status: "ENABLED" }} ConcurrentBuildLimit: 1 Visibility: "PRIVATE" # CloudWatch log group to store build logs MirrorRepositoryProjectLogGroup: Type: "AWS::Logs::LogGroup" UpdateReplacePolicy: "Delete" DeletionPolicy: "Delete" Properties: LogGroupName: !Sub "/aws/codebuild/${MirrorRepositoryProject}" RetentionInDays: !Ref "LogExpirationInDays" # IAM role for API Gateway ServiceProxyRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub "${ProjectName}-mirror-repository-service-proxy" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: { Service: "apigateway.amazonaws.com" } Action: "sts:AssumeRole" Policies: - PolicyName: "Build" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "codebuild:StartBuild" Resource: !GetAtt "MirrorRepositoryProject.Arn" # REST API for webhooks to start CodeBuild project StartBuildAPI: Type: "AWS::ApiGateway::RestApi" Properties: Name: !Sub "${ProjectName}-start-build" EndpointConfiguration: { Types: [ "REGIONAL" ]} StartBuildMethod: Type: "AWS::ApiGateway::Method" Properties: RestApiId: !Ref "StartBuildAPI" ResourceId: !GetAtt "StartBuildAPI.RootResourceId" HttpMethod: "POST" ApiKeyRequired: false AuthorizationType: "NONE" Integration: Type: "AWS" IntegrationHttpMethod: "POST" Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:codebuild:action/StartBuild" Credentials: !GetAtt "ServiceProxyRole.Arn" RequestParameters: integration.request.header.Content-Type: "'application/x-amz-json-1.1'" integration.request.header.X-Amz-Target: "'CodeBuild_20161006.StartBuild'" RequestTemplates: application/x-www-form-urlencoded: !Sub | { "projectName": "${MirrorRepositoryProject}" } PassthroughBehavior: "NEVER" IntegrationResponses: [{ StatusCode: "200", ResponseTemplates: {}}] MethodResponses: [{StatusCode: "200", ResponseModels: { "application/json": "Empty" }}] StartBuildAPIDeployment: Type: "AWS::ApiGateway::Deployment" DependsOn: [ "StartBuildMethod" ] Properties: RestApiId: !Ref "StartBuildAPI" StageName: "prod" Outputs: WebhookURL: Description: "Webhook URL" Value: !Sub "https://${StartBuildAPI}.execute-api.${AWS::Region}.amazonaws.com/prod"
まとめ
以上、Backlog の Git リポジトリを AWS CodeCommit に効率良く連携する方法を検討し、実装してみました。 私たちのお客様は、ここで紹介した方法を参考に AWS 上で CI/CD のワークフローを構築し、DevOps 領域で抱えていた課題を解決されました。 引き続き GIMLE チームでは、お客様がクラウド領域で抱える課題に対して、デザインとテクノロジーで解決に取り組んでいきます。