Git submoduleを含むサービスをAWS CodePipelineで扱う

新しい職場もあっという間に半年経ちますが、おかげさまで元気で毎日忙しい日々を過ごしているAWS初心者のわたなべです。

AWS CodePipeline

現在いくつかの案件を並行して進めているのですが、その中で知人が在籍している某スタートアップのインフラ改善支援で利用しているサービスがAWS CodePipelineです。

aws.amazon.com

CodePipelineは、Jenkinsやcircleciのように、CI/CDを実現するためのサービスで、AWS謹製ということでAWSの他のサービスと連携しやすいのが特徴かと思います。

B/G デプロイメント

今回の要望として、ECS Fargeteを使用してBlue/Greenデプロイメントを実現するということで、aws-examplesにある以下のサンプルを参考にしています。

github.com

このリポジトリfargate ブランチがFargate上でB/Gデプロイメントを実現するサンプルで、ざっくり以下のような流れでB/Gデプロイメントを実現しています。

  • Githubの更新
  • コンテナのビルド
  • ECRにPush
  • Deploy先の検出
  • Fargateにデプロイ - Green側のTaskを更新
  • 承認
  • FargateタスクのSwitch

今回の背景

今回対象になるサービスは、以下のような形でいくつかのAPIをGit submoduleとして構成されています。

  • example-center

全体を管理する、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のイメージ一覧を取得しているので、 CodeServiceRollecr: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はあまり使ったことがなく、試行錯誤しているところなのでもっといい方法がないか含め引き続きいろいろ試してみます。