Git submoduleを含むサービスをAWS CodePipelineで扱う
新しい職場もあっという間に半年経ちますが、おかげさまで元気で毎日忙しい日々を過ごしているAWS初心者のわたなべです。
AWS CodePipeline
現在いくつかの案件を並行して進めているのですが、その中で知人が在籍している某スタートアップのインフラ改善支援で利用しているサービスがAWS CodePipelineです。
CodePipelineは、Jenkinsやcircleciのように、CI/CDを実現するためのサービスで、AWS謹製ということでAWSの他のサービスと連携しやすいのが特徴かと思います。
B/G デプロイメント
今回の要望として、ECS Fargeteを使用してBlue/Greenデプロイメントを実現するということで、aws-examplesにある以下のサンプルを参考にしています。
このリポジトリの fargate
ブランチがFargate上でB/Gデプロイメントを実現するサンプルで、ざっくり以下のような流れでB/Gデプロイメントを実現しています。
- Githubの更新
- コンテナのビルド
- ECRにPush
- Deploy先の検出
- Fargateにデプロイ - Green側のTaskを更新
- 承認
- FargateタスクのSwitch
今回の背景
今回対象になるサービスは、以下のような形でいくつかのAPIをGit submoduleとして構成されています。
- example-center
- foo-api
- ....
全体を管理する、example-centerの更新をトリガーに配下のAPI群をBuild => Deployしたいのですが、AWS CodePipelineではgit submoduleはサポートされていません。
パイプラインのエラー: GitHub ステージ自分のソースは Git サブモジュールが含まれているが、AWS CodePipeline 初期化されていません
上記FAQに、 可能な変更: 別スクリプトの一部として直接 GitHub リポジトリをクローンすることを検討してください。
と書かれているので実際に試してみました。
今回やってみたこと
git submodule updateで行けるだろうとおもっていたのですが、Buildコンテナには .git
ディレクトリが含まれていないので無理でした。仕方がないので pre_build
フェーズで以下のような形でsubmoduleを一つづつcloneすることにしました
- git clone "https://$GITHUB_TOKEN@github.com/bar/foo-api.git"
実際のビルド処理、pushする処理及びartifactとして次の処理に渡すbuild.jsonは、以下のようなスクリプトで生成するようにしてみました。このスクリプトでは、変更されていないsubmoduleのビルドを省くために、コンテナのタグにgitのhashを使うよう変更しています。
#!/bin/bash # usage # create-build-script.sh REPOSITORY_URL REPOSITORY_NAME services=( "foo-api" # 実際には複数のサブモジュールを定義している ) declare -A tags=() echo "#!/bin/bash" > /tmp/build.sh echo "#!/bin/bash" > /tmp/push.sh for service in "${services[@]}" ; do if [ ! -d "./${service}" ]; then echo "[${service}] Service directory not found" continue fi hash=`git -C ./${service} log --pretty=%H | head -n 1` echo "[${service}] Current tag => $2:${hash}" result=`aws ecr list-images --repository-name $2 | grep $hash | wc -l` if [ $result -eq 0 ]; then echo "docker build --tag $1:${hash} ./${service}" echo "echo \"docker build --tag $1:${hash} ./${service}\"" >> /tmp/build.sh echo "docker build --tag $1:${hash} ./${service}" >> /tmp/build.sh echo "echo \"docker push $1:${hash}\"" >> /tmp/push.sh echo "docker push $1:${hash}" >> /tmp/push.sh else echo "[${service}] Latest version image exists, build process was skipped." fi tags[$service]=$hash done for key in "${!tags[@]}"; do printf '%s\0%s\0' "$key" "${tags[$key]}" done | jq -Rs ' split("\u0000") | . as $a | reduce range(0; length/2) as $i ({}; . + {($a[2*$i]): ($a[2*$i + 1]|fromjson? // .)})' > /tmp/build.json
このスクリプトで使用している jq
ですが標準のビルド環境でインストールすると1.3という少し古いバージョンがインストールされるので、 install
フェーズで強引にjq-1.5をインストールしています。
また、スクリプトではawsコマンドを使用して、ecrのイメージ一覧を取得しているので、 CodeServiceRoll
に ecr:ListImages
権限を追加する必要があります。
...
そして、最終的なBuild処理の定義はこんな感じです。(実際に使用しているものからは少し変えています...)
CodeBuildProject: Type: AWS::CodeBuild::Project Properties: Artifacts: Type: CODEPIPELINE Source: Type: CODEPIPELINE BuildSpec: | version: 0.1 phases: install: commands: - apt-get update && apt-get -y install python-pip git wget jq - pip install --upgrade python - pip install --upgrade awscli - wget https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 - chmod +x jq-linux64 - mv jq-linux64 $(which jq) pre_build: commands: - printenv - git clone "https://$GITHUB_TOKEN@github.com/bar/foo-app.git" # - echo -n "$CODEBUILD_LOG_PATH" > /tmp/build_id.out # - printf "%s:%s" "$REPOSITORY_URI" "$(cat /tmp/build_id.out)" > /tmp/build_tag.out # - printf '{"tag":"%s"}' "$(cat /tmp/build_id.out)" > /tmp/build.json - $(aws ecr get-login) - bash create-build-shript.sh "$REPOSITORY_URI" "$REPOSITORY_NAME" build: commands: # - docker build --tag "$(cat /tmp/build_tag.out)" . - bash /tmp/build.sh - cat /tmp/build.json post_build: commands: # - docker push "$(cat /tmp/build_tag.out)" - bash /tmp/push.sh artifacts: files: /tmp/build.json discard-paths: yes Environment: ComputeType: "BUILD_GENERAL1_SMALL" Image: "aws/codebuild/docker:1.12.1" Type: "LINUX_CONTAINER" EnvironmentVariables: - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} - Name: REPOSITORY_NAME Value: !Ref Repository - Name: GITHUB_TOKEN Value: !Ref GitHubToken Name: !Sub ${AWS::StackName}-codebuildproject ServiceRole: !Ref CodeBuildServiceRole
この設定で作成される build.json
はサンプルとは異なりますのでこのデータを受け取る deployer.py
の該当箇所も修正する必要があります。
def get_build_artifact_id(build_id): ... snip print(objbuild['foo-api']) """サンプルでは 'tag' になっているので変更 return objbuild['foo-api']
実際は複数のsubmoduleをデプロイする場合には、他にもいろいろと修正が必要ですが、ひとまずこのような形でgit submoduleに対応できそうです。
AWSはあまり使ったことがなく、試行錯誤しているところなのでもっといい方法がないか含め引き続きいろいろ試してみます。