📦

Bitbucket Pipelines から CD の一部を CodeBuild へ移行した

2021/10/02に公開

某プロジェクトにて、それまで Bitbucket Pipelines にて CI/CD を運用して約半年、小問題は何度か発生したものの CI/CD としては致命的に止まる・詰まることなくやっていました。しかし、とうとう exceed memory limitで Fail する [1] 頻度が高くなり、ECR イメージの build・push に関しては AWS CodeBuild へ移行することにしました。

この記事では、その CodeBuild 移行を実現する際のポイントを共有します。
なお、過程で呟いてきたことは sogaoh monthly 2021-09 (2) の Bitbucket Pipelines から 一部 CD を CodeBuild へ に畳んで置いてあります。(リンクへは 新しいタブを開く or 新しいウィンドウで開く で進むのが良さそうです)

Bitbucket Pipelines での3処理

ECR イメージの build・push を、Bitbucket Pipelines では以下の3処理にて行っていました。

  1. CI/CD用リポジトリ (以降 adminリポジトリ と言います) の submodule として配置してした、コードリポジトリの clone
  2. コードリポジトリのフロントエンド Vue.js を npm run production 相当のスクリプトでビルド(少しカスタマイズした npm run pack が実行コマンド)
  3. フロントエンド・バックエンドの docker イメージを build して Amazon ECR に push する

exceed memory limit の解決

Bitbucket Pipelines では 2. の処理で最大に使える 7128 MB のメモリを越えてしまい Fail するようになってしまいました(逆に厄介なことにたま〜に成功する。。)。
CodeBuild への移行先ではこれを上回る 15 GB のメモリを積んでいる Linux large を選択しました。[2]

メモリ不足問題つまり 2. に関してはこれだけで課題解決できましたが、むしろ 1. 3. を完遂するのに苦戦しました。。

カスタムイメージの使用とコードリポジトリの clone

上述3処理のうち 1. のところに関するポイントを書きます。
AWS CodeBuild には標準でマネージド型イメージが用意されてもいますが、Bitbucket Pipelines で動かしていたことができれば良かった、というか フロントエンド Vue.js のビルドができれば良くて、マネージド型イメージへの nvm インストール等を新たにしたくないと思ったので、これまで使用していた ECR にある docker イメージ(以降、amzn2-builder と書きます)を使うことにしました。

ECR の Permissions 調整

CodeBuild のプロジェクトをマネコンで新規作成し、ひとまずビルドしてみたのですが、予想通り一発で成功はせず、どういうエラーかを見ると amzn2-builder を pull できなかったようでした。
エラーメッセージで検索した情報から、ECR 側に Permissions を設定する必要があるとわかり[3]、以下のような IAM ポリシーを設定しました。

CodeBuildAccess : 最初はUIで作り、サービスプリンシパルも足しておこうと json を調整

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "CodeBuildAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root",
        "Service": "codebuild.amazonaws.com"
      },
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"
      ]
    }
  ]
}

Bitbucket Cloud へのSSH接続設定

これで amzn2-builder は pull できたものの、次のコケポイントは

submodule として配置してした、コードリポジトリの clone

でした。submodule add しようとしたのですがコードリポジトリ側のブランチを自由に選択したいこともあり、そうなるとなかなかうまくいかない[4] ので、普通に git clone してくるようにしました。
そこで当然といえば当然なのですが認証が必要になり、個人的には避けたかったのですが自分のアプリパスワードで接続するようにしました[5]
この点、将来的には人のアカウントに紐づいていない形に直す等していかないと自分がいなくなったときにコケるので追い追い考えないと、です。
ともあれ、接続設定しないと先に進まないので、CodeBuild にはパラメータストアに書き込んだ自分のアプリパスワードを参照させました。ただこれだけでは git clone は成功せず、CodeBuild 稼働コンテナに SSH 接続設定を埋め込んでおく必要があり、事例を探した結果 AWS CodePipelineでサブモジュールを使う | きいちログ に「おおこれは」と思えた buildspec.yml サンプルを見つけ、以下のような設定を入れた結果、git clone に成功しました。

Bitbucket への SSH鍵接続設定埋め込み buildspec.yml サンプル

${BRANCH} は後述しますが CodeBuild環境変数 の1つ

buildspec.yml (一部だし [〜] のところは加工もしている)

version: 0.2

env:
  git-credential-helper: yes
  variables:
    remote_origin: "bitbucket.org"
    remote_user: "[ore-no-user-id]"
    node_version: "v14.17.5"     # このバージョンでビルドする
  parameter-store:
    ssh_key: "[hoge-git_ssh-key]"
    aws_access_key: "[powerful_user_access_key]"   # あとで使う
    aws_secret_key: "[powerful_user_secret_key]"   # あとで使う

phases:
  install:
    commands:
      - mkdir -p ~/.ssh
      - echo "$ssh_key" > ~/.ssh/ssh_key
      - chmod 600 ~/.ssh/ssh_key
      - ssh-keygen -F "$remote_origin" || ssh-keyscan "$remote_origin" >> ~/.ssh/known_hosts
      - eval "$(ssh-agent -s)"
      - ssh-add ~/.ssh/ssh_key
      - aws configure set aws_access_key_id "$aws_access_key"       # あとで活きる
      - aws configure set aws_secret_access_key "$aws_secret_key"   # あとで活きる
  pre_build:
    commands:
      # - cd [ソースを集めたディレクトリ] && git clone -b ${BRANCH} "$remote_user"@"$remote_origin":[organization]/[repository].git
# ・・・

CodeBuild環境で dockerd を動かし続ける

話は 2. のVue.js ビルドを飛ばして、おそらくこの記事で一番伝わってほしい、3. フロントエンド・バックエンドの docker イメージを build して Amazon ECR に push する で躓いていたのをクリアーするのに必要なポイントを書いていきます。

↓になったときは「なにぃ〜」と思いましたが、

https://twitter.com/sogaoh/status/1440607804549525507

トラブルシューティング[6]含めよくよく探したところ、2つほど有効そうな対策事例を発見し[7][8]、buildspec.yml に以下のように記述を加えてみたものの・・・

buildspec.yml (抜粋)
# ・・・
  post_build:
    commands:
      - nohup /usr/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 &>/var/log/docker.log &
      - timeout 60 sh -c "until docker info; do echo .; sleep 1; done"

# ・・・

artifacts:
  files:
    - /var/log/docker.log

まだダメで、一時はカスタムイメージでやるのを諦めかけたのですが、そうしてる間に AWSサポート に問い合わせていた Case へ回答が来ました。そのヒントでやりたいことを実現できたので、感謝の意を込めて明記しておきます。回答内容としては次項の 2. と 3. が主旨でした。

Docker-in-Docker で動かすポイント3点

ここまでで以下3つのうち 1.2. までは設定できていたのですが、 3. が欠けていました。

  1. 特権付与する
    • CodeBuild のUIでも Docker イメージを構築するか、ビルドで昇格されたアクセス権限を取得するには、このフラグを有効にします。 と説明書きがあり、必要そうだと思っていたので最初からONにしました。
  2. Dockerデーモンが動いている状態にする
  3. VOLUME /var/lib/docker する

この 3. をさっそくカスタムイメージに適用しビルドし直し、ECR へ push して CodeBuild を再実行したところついに build・push 成功。
なお push の前には ECR へのログインが必要で、以下の aws ecr コマンドが実行できるように、上述の SSH鍵接続設定埋め込み buildspec.yml サンプルaws configure してありました。

Makefile に整備した ECR ログイン自動実行ワンライナー
Makefile抜粋

ecr-login:
	aws ecr get-login-password --region $(AWS_REGION) | docker login --username AWS --password-stdin $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com

codebuild-[ECR-Update]-service-role (default+α)

書き忘れていたなと思って後で追加した節なのですが、UI で CodeBuild プロジェクトを作ったときに AWS 側で作ってくれる IAM ロールに、ポリシー json をいくらか足していたと思います。最終的にだいたい以下のような内容となりました。

codebuild-[ECR-Update]-service-role
CodeBuildBasePolicy([〜] のところは加工してあります)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:logs:ap-southeast-1:[123456789012]:log-group:/aws/codebuild/[ECR-Update]",
                "arn:aws:logs:ap-southeast-1:[123456789012]:log-group:/aws/codebuild/[ECR-Update]:*"
            ],
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::codepipeline-[AWS_REGION]-*",
                "arn:aws:s3:::[adminリポジトリ名]-codebuild-artifacts/*"
            ],
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "codebuild:CreateReportGroup",
                "codebuild:CreateReport",
                "codebuild:UpdateReport",
                "codebuild:BatchPutTestCases",
                "codebuild:BatchPutCodeCoverages"
            ],
            "Resource": [
                "arn:aws:codebuild:[AWS_REGION]:[123456789012]:report-group/[ECR-Update]-*"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:ssm:[AWS_REGION]:[123456789012]:parameter/*"
            ],
            "Action": [
                "ssm:GetParameters",
                "ssm:GetParameter",
                "secretsmanager:GetSecretValue",
                "kms:Decrypt"
            ]
        }
    ]
}

Bitbucket Pipelines から CodeBuild を起動する

ここまででだいたいやりたいことはできたのですが、「誰でもデプロイできる」を狙いに Bitbucket Pipelines を仕込んでいたので、この「CodeBuild 版 ECR イメージ build・push」もこれまでと変わらないインターフェースで使えるように、aws codebuild CLI から kick する方法を探りました。
すると程なく aws codebuild start-build で起動できそうとわかり、試してみたところ成功。しかし起動したらすぐレスポンスが返って来てしまい、どこを見れば状況が分かるのかが確認しずらかったので、

  • CodeBuild の完了を待つようにしたい
  • CodeBuild のURLリンクを出すようにしたい

に挑みました。結果を言うと両方とも実現できて、最終的に以下のような bitbucket pipelines の step となりました。

CodeBuild完了を待つ阪 bitbucket-pipelines.yml

variables は CodeBuild の環境変数に設定しておく必要があります。

bitbucket-pipelines.yml(一部だし [〜] のところは加工もしている)

# ・・・

pipelines:
  custom:

# ・・・

    ecr-update:
      - variables:
          - name: ENVIRONMENT # 環境
          - name: BRANCH      # コードリポジトリのブランチ
          - name: IDENTIFIER  # 識別子:ECRイメージのタグに使用する
          - name: SIDE        # Blue/Green Deployment のどちら側か
          - name: SOURCE      # adminリポジトリのブランチ
      - step: &code-build-run
          name: "Call CodeBuild [ECR-Update] project"
          size: 2x
          script:
            - aws codebuild start-build
              --project-name "[ECR-Update]"
              --source-version "$SOURCE"
              --privileged-mode-override
              --environment-variables-override
              "[{\"name\":\"ENVIRONMENT\",\"value\":\"$ENVIRONMENT\",\"type\":\"PLAINTEXT\"},
              {\"name\":\"BRANCH\",\"value\":\"$BRANCH\",\"type\":\"PLAINTEXT\"},
              {\"name\":\"IDENTIFIER\",\"value\":\"$IDENTIFIER\",\"type\":\"PLAINTEXT\"},
              {\"name\":\"SIDE\",\"value\":\"$SIDE\",\"type\":\"PLAINTEXT\"}]" > start-build.json
            - CODEBUILD_BUILD_ID=$(cat start-build.json| jq -cr '.[].id')
            # ↓で CodeBuild のURLを Bitbucket Pipelines のコンソールに出すことに成功
            - echo "https://[AWS_REGION].console.aws.amazon.com/codesuite/codebuild/[AWS_ACCOUNT_ID]/projects/[ECR-Update]/build/$CODEBUILD_BUILD_ID/?region=[AWS_REGION]"
            # ↓2行の1行シェル書き出しと実行については次に説明
            - echo "CODEBUILD_BUILD_ID=\$1; SLEEP_NUM=1; while true; do BUILD_STATUS=\$(aws codebuild batch-get-builds --ids \${CODEBUILD_BUILD_ID} | jq -cr '.[][].buildStatus'); echo \"\${CODEBUILD_BUILD_ID} \${BUILD_STATUS} \${SLEEP_NUM}\"; if [ \${BUILD_STATUS} != \"IN_PROGRESS\" ]; then echo \"\${CODEBUILD_BUILD_ID} \${BUILD_STATUS}\"; if [ \${BUILD_STATUS} != \"SUCCEEDED\" ]; then exit 1; fi; break; fi; let SLEEP_NUM=\${SLEEP_NUM}+1; sleep 10; done" > while_in_progress.sh
            - bash ./while_in_progress.sh $CODEBUILD_BUILD_ID
            - pipe: atlassian/slack-notify:2.0.0
              variables:
                WEBHOOK_URL: $WEBHOOK_URL_DEV
                MESSAGE: "**FINISH ecr-update** branch=[$BRANCH], identifier=[$IDENTIFIER], side=[$SIDE], environment=[$ENVIRONMENT], source-version=[$SOURCE]"
          services:
            - docker
          artifacts:
            - start-build.json
            - while_in_progress.sh

# ・・・

CodeBuild の完了を待つ1行シェル書き出しと実行

CodeBuildを待つやつ - sasasin/aws-codebuild-wait-build.sh を大いに参考にさせてもらい、aws codebuild batch-get-builds を利用したシェルを作り検証しました。
だいたい良さそうな感じだったのでこれを一度 bitbucket-pipelines.yml の step に突っ込んだのですが何度かシンタックスエラーとなっては直し、そしてビルドが失敗しているのに Bitbucket Pipelines の結果としては成功になってしまっているのを手直しして、以下のようなシェルとなりました。

改行を入れるなどして見やすくした1行シェル
while_in_progress.sh

CODEBUILD_BUILD_ID=\$1;
SLEEP_NUM=1;
while true;
do
  BUILD_STATUS=\$(aws codebuild batch-get-builds --ids \${CODEBUILD_BUILD_ID} | jq -cr '.[][].buildStatus');
  echo \"\${CODEBUILD_BUILD_ID} \${BUILD_STATUS} \${SLEEP_NUM}\";
  if [ \${BUILD_STATUS} != \"IN_PROGRESS\" ]; then
    echo \"\${CODEBUILD_BUILD_ID} \${BUILD_STATUS}\";
    if [ \${BUILD_STATUS} != \"SUCCEEDED\" ]; then
      exit 1;
    fi;
    break;
  fi;
  let SLEEP_NUM=\${SLEEP_NUM}+1;
  sleep 10;
done

工夫したり調整したところを列挙しておくと、

  • $" は全てエスケープが必要
  • 改行を表現するのは ;
  • 変数の定義は ${〜} で囲まず、参照箇所では ${〜} で囲む

といったところでしょうか。
これを1行で表現し echo コマンドで while_in_progress.sh に書き出し、それを bash ./while_in_progress.sh $CODEBUILD_BUILD_ID で呼び出すと、いい感じに完了を待ってくれて、CodeBuild側のビルドが失敗だったらちゃんと失敗したと通知してくれます。

補足

実はこの機構、Production デビューはまだで、諸事情あってカスタムイメージを差し替えて急遽実施しようとしたら VOLUME /var/lib/docker を入れ忘れてたり特権付与のチェックを入れてなかったりをやってしまいました。
という失敗を経て整理し直したので、いい完成度になっているはずです。

脚注
  1. Read more about service memory limits -> Atlassian Support / Bitbucket / Resources / Build, test, and deploy with Pipelines / Databases and service containers ↩︎

  2. AWS / ドキュメント / AWS CodeBuild / ユーザーガイド / ビルド環境のコンピューティングタイプ ↩︎

  3. AWS / ドキュメント / AWS CodeBuild / ユーザーガイド / CodeBuild の Amazon ECR サンプル以下のいずれかに該当する場合は、AWS CodeBuild が Docker イメージをビルド環境にプルできるように、Amazon ECR のイメージリポジトリにアクセス許可を割り当てる必要があります。 と書いてあるのを見つけた ↩︎

  4. 単に自分の git submodule 力が足りなかったのかもしれません ↩︎

  5. AWS / ドキュメント / AWS CodeBuild / ユーザーガイド / CodeBuild でソースプロバイダにアクセスする #Bitbucket アプリのパスワード ↩︎

  6. https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/troubleshooting.html ↩︎

  7. AWS CodeBuildカスタムDockerイメージを使ってビルドする - Speaker Deck (slide=14) ↩︎

  8. 「CodeBuild に用意されている Docker イメージ」で AWS CodeBuild 上での docker-compose のインストール作業を省略する - Beeeat’s log ↩︎

Discussion