AWSの色んなサービスを意図的に使って、サンプルWebアプリを構築してみているのですが、今回は part3 ということで、「CodeBuild・CodePipelineを使ってデリバリーパイプラインを導入」していきたいと思います。

今回実現すること

  1. ビルド
    • CodeBuild でDocker イメージを作成して ECR にプッシュする
  2. デプロイ
    • CodeBuild で ECR 上の Docker イメージを EKS クラスタに、デプロイする
  3. パイプライン
    • v*.*.*形式のタグが GitHub リポジトリにプッシュされたら、ビルドを実行する
      • CodePipeline で、GitHub の変更を検知する
      • タグプッシュは検知されないので、branch プッシュをトリガーに、ビルドスクリプトにてgit describe --tagsでタグ名を取得して、タグを取得できなかったり、v*.*.*形式じゃない場合はエラーとする
    • ビルド後、「手動承認」をしたら、デプロイが実行される

ビルド

CodeBuild で Docker イメージを作成して ECR にプッシュする設定を行います。

ビルドプロジェクトの作成

CodeBuild コンソール > ビルドプロジェクトを作成する から以下の設定でビルドプロジェクトを作成します。

  • プロジェクトの設定
    • プロジェクト名: sample-api-build
  • ソース
    • ソースプロバイダ: GitHub
    • リポジトリ: GitHub アカウントのリポジトリ, OAuth を利用して使用して接続
    • リポジトリのURL: https://github.com/rinoguchi/aws_fullstack_sample_application
    • プライマリリソースのウェブフックイベント: なし
  • 環境
    • 環境イメージ: マネージド型イメージ
    • オペレーティングシステム: Amazon Linux 2
    • ランタイム: Standard
      • 特権付与: Docker イメージを構築するか、ビルドで昇格されたアクセス権限を取得するには、このフラグを有効にしますにチェック
    • イメージ: aws/codebuild/amazonlinux2-x86_64-standard:3.0
    • 環境タイプ: Linux
    • サービスロール: 新しいサービスロール
  • BuildSpec
    • ビルド仕様: buildspec ファイルを使用する
    • BuildSpecファイル名: api/buildspec-build.yml // あとでもう一つ buildspec ファイルを作るので名前を変えてます
  • アーティファクト
    • アーティファクトなし(Docker イメージを作成して、ECR にアップロードするため)
  • バッチ設定
    • 設定なし
  • ログ
    • 設定なし

環境変数設定

ECR にプッシュするために、IAM ユーザ(sample-admin ※ part2にて作成)のクレデンシャル情報をパラメータストアに、その他の変数を環境変数に設定します。

CodeBuild コンソール > ビルドプロジェクト > sample-api-build > 編集 > 環境 > 追加設定

  • パラメータの作成
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY
  • 環境変数
    • ECR_URL

buildspec-build.yml を作成

こちら を参考に、buildspec-build.ymlを作成します。

version: 0.2
phases:
  pre_build:
    commands:
      - IMAGE_TAG=$(git describe --tags)
      - |
        if [[ ! "${IMAGE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
          echo "tag name not matched. tag: ${IMAGE_TAG}";
          exit 1;
        fi
      - aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin ${ECR_URL}
  build:
    commands:
      - cd api # 対象フォルダに移動
      - docker build . -t sample-api:${IMAGE_TAG}
      - docker tag sample-api:${IMAGE_TAG} ${ECR_URL}/sample-api:${IMAGE_TAG}
      - docker push ${ECR_URL}/sample-api:${IMAGE_TAG}
  • コンソールで設定した環境変数等は、env配下にあらためて記載する必要はありません
  • タグ名をgit describe --tagsで取得して、v*.*.*の形式にマッチしているかをチェックしています
  • aws ecr get-login-password | docker loginの部分で、事前に AWS のログインパスワードを取得して、docker login しています
  • IAM ユーザは、part2の記事で作成したものを利用してます

動作確認

タグを作成して、プッシュします。

git tag -a v1.0.1 -m "add buildspec-build.yml"
git push origin v1.0.1

次に、CodeBuild コンソール > ビルドプロジェクト > sample-api-build > ビルド開始 からビルドを実行してみます。

無事に ECR にプッシュされました。

デプロイ

てっきり CodeDeploy が EKS に対応してるのかと思いきや、対応してないみたいなので、こちらも CodeBuild で実行することにしました。

EKS クラスタの作成・ノードグループの作成・初回 Pod への Docker コンテナのデプロイまでは、part2 の記事の通りやってある状態で、Pod に新しい Docker イメージをデプロイします。

ビルドプロジェクトの作成

デプロイ用にもう一つビルドプロジェクトを作成します。
設定内容はビルド用に作ったものとほぼ同じで以下の点のみ変更しました。

  • プロジェクト名: sample-api-deploy
  • BuildSpecファイル名: api/buildspec-deploy.yml
  • 特権付与: Docker イメージを構築するか、ビルドで昇格されたアクセス権限を取得するには、このフラグを有効にします チェックなし

環境変数設定

ビルド用と同じ環境変数を設定しました。(全て同じなので割愛)

buildspec-deploy.yml の作成

デプロイ用にもう一つ、buildspec-deploy.ymlファイルを作成します。

version: 0.2
phases:
  pre_build:
    commands:
      - IMAGE_TAG=$(git describe --tags)
  build:
    commands:
      - cd api # 対象フォルダに移動
      - aws eks update-kubeconfig --region us-west-2 --name sample-eks-cluster
      - sed -i -e "s/__IMAGE_TAG__/${IMAGE_TAG}/" ./api-deployment.yml
      - sed -i -e "s/__ECR_URL__/${ECR_URL}/" ./api-deployment.yml
      - kubectl apply -f ./api-deployment.yml
  • こちらもgit describe --tagsでタグ名を取得します
  • まず、kubeconfigを更新して、kubectl コマンドを実行できるようにしてます
  • 次に、podのデプロイ定義が書いてあるapi-deployment.yml内に記載されている__ECR_URL____IMAGE_TAG__部分を置換します
    • yaml ファイルの中身の抜粋。全文は part2 を参照ください。
      // 抜粋
      spec:
        template:
          spec:
            containers:
              - name: sample-api-container
                image: __ECR_URL__/sample-api:__IMAGE_TAG__
    • sed で、変数に置き換えているため、ダブルクォーテーション(")で囲む必要があります
  • 最後に、kubectl applyしてます

動作確認

タグを作成して、プッシュします。

git tag -a v1.0.2 -m "deploy to EKS"
git push origin v1.0.2

次に、CodeBuild コンソール > ビルドプロジェクト > sample-api-build > ビルド開始 からビルドを実行してみます。

ログを見ると、以下のように kubectl apply が成功していました。

// 抜粋
[Container] 2021/08/01 08:19:20 Entering phase POST_BUILD
[Container] 2021/08/01 08:19:20 Running command aws eks update-kubeconfig --region us-west-2 --name sample-eks-cluster
Added new context arn:aws:eks:us-west-2:xxx:cluster/sample-eks-cluster to /root/.kube/config
[Container] 2021/08/01 08:19:20 Running command sed -i -e "s/__IMAGE_TAG__/${IMAGE_TAG}/" ./api-deployment.yml
[Container] 2021/08/01 08:19:20 Running command sed -i -e "s/__ECR_URL__/${ECR_URL}/" ./api-deployment.yml
[Container] 2021/08/01 08:19:20 Running command kubectl apply -f ./api-deployment.yml
deployment.apps/sample-api-deployment configured
[Container] 2021/08/01 08:19:25 Phase complete: POST_BUILD State: SUCCEEDED

手元で kubectl describe pods で確認しても、以下のように、ちゃんと新しいタグバージョンのイメージがデプロイされていることがわかります。

Events:
  Type     Reason                  Age    From                     Message
  ----     ------                  ----   ----                     -------
  Normal   Pulling                 24s  kubelet                  Pulling image "xxx.dkr.ecr.us-west-2.amazonaws.com/sample-api:v1.0.2"
  Normal   Pulled                  22s  kubelet                  Successfully pulled image "xxx.dkr.ecr.us-west-2.amazonaws.com/sample-api:v1.0.2" in 1.894029857s
  Normal   Created                 21s  kubelet                  Created container sample-api-container
  Normal   Started                 21s  kubelet                  Started container sample-api-container

最後に、この WEB アプリが提供する API にアクセスしてみたところ、今回仕込んでおいた console.log が出力されていたので、問題なくデプロイできていることも確認できました。

kubectl logs -f sample-api-deployment-xxx-xxx
> codebuild installed. // 今回仕込んでおいたログ

パイプライン

最後に、GitHubの変更を検知したら自動でビルド+デプロイを行うようにします。
ただし、勝手にデプロイされるのも怖いので、デプロイの手前に「手動承認」のステージを入れて、人手で最終チェックを行えるようにしたいと思います。

パイプラインの作成

CodePipeline コンソール > パイプラインの作成 から以下の設定でパイプラインを作成します。

  • パイプラインの設定
    • パイプライン名: sample-api-pipeline
    • サービスロール: 新しいサービスロール
  • ソース(Source)
    • ソースプロバイダー: GitHub(バージョン2) ※ バージョン1だと Git のクローンを取得できないため、バージョン2必須
    • リポジトリ: rinoguchi/aws_fullstack_sample_application
    • ブランチ: main
    • 検出オプション: ソースコードの変更時にパイプラインを開始するにチェック
    • 出力アーティファクト形式: 完全クローン
  • 構築する(Build)
    • プロバイダー: AWS CodeBuild
    • プロジェクト名: sample-api-build
    • 環境変数: なし(CodeBuild側で設定済み)
    • ビルドタイプ: 単一ビルド
  • デプロイ(Deploy)
    • 本当はここで CodeBuild で作成したsample-api-deployを選択したいのですが、できないので「スキップ」。後で個別に追加します

手動承認とデプロイのステージを追加

CodePipeline コンソール > sample-api-pipeline >  編集 から、編集モードに入り「手動承認」と「デプロイ」の二つのステージを追加します。

  • 手動承認(Approve)
    • Build の下の+ステージを追加するからステージを追加
      • ステージ名: Approve
    • +アクショングループを追加するからアクショングループを追加
      • アクション名: Approve
      • アクションプロバイダー: 手動承認
  • デプロイ(Deploy)
    • Approveの下の+ステージを追加するからステージを追加
      • ステージ名: Deploy
    • +アクショングループを追加するからアクショングループを追加
      • アクション名: Deploy
      • 入力アーティファクト: SourceArtifact ※ git describe --tagsを実行するには Source ステージで作成した Git クローンが必要
      • アクションプロバイダー: AWS CodeBuild
      • プロジェクト名: sample-api-deploy
      • 環境変数: なし(CodeBuild側で設定済み)
      • ビルドタイプ: 単一ビルド

CodeBuildに権限付与

ここまでの設定で、CodePipeline から、二つの CodeBuild のプロジェクトを実行する設定になりましたが、以下のコメントの通り、このままだと権限が足りません。

完全なクローンオプションを使用するには、CodeBuild 実行ロールに、パイプラインで選択した GitHub 接続を介してクローンを実行するアクセス許可が必要です。
最小権限の原則により、これらのアクセス許可はデフォルトでは GitHub ポリシーに含まれません。

なので、こちら を参考に、二つの CodeBuild のそれぞれのロールに対して、ポリシーをアタッチしていきます。

まず、CodePipeline コンソール > sample-api-pipeline > Source のiアイコンから ConnectionArn をコピーします。

次に、IAM コンソール > ポリシー > ポリシーの作成 から、以下の設定でポリシーを作成します。

  • JSON
    {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "codestar-connections:UseConnection",
            "Resource": "arn:aws:codestar-connections:us-west-2:xxx:connection/xxx-xxx-xxx-xxx-xxx" // 先ほどコピーしたConnectionArn
        }
    ]
    }
  • ポリシー名: connect-to-github-policy

最後に、CodeBuild コンソール > ビルドプロジェクト > sample-api-build 及び sample-api-deploy > ビルドの詳細 > 環境 > サービスロール のリンクから、IAM ロールの画面を開いて、先程作成したconnect-to-github-policyロールをアタッチします。

動作確認

設定は完了したので、最終動作確認をやっていきます。

まず、ソースコードを修正して、変更を確認できるようにします。今回は、APIを呼び出した際にログを出力するようにしました。

app.get('/comments/:id', async (req: express.Request, res: express.Response) => {
    console.log('codepipelne installed.')
    // 省略
}

この内容で、コミットして、タグ作成して、プッシュしました。

git commit -a -m "install codepipeline"
git tag -a v1.0.3 -m "install codepipeline"
git push origin v1.0.3
git push origin main

CodePipeline の画面を確認したところ、無事、Source 及び Build ステージが実行されて、Approve ステージが保留になっていました。

レビューボタンから、手動承認すると後続の Deploy ステージが実行されました。

kubectl describe podsでログを確認したところ、無事新しいv1.0.3のイメージが Pod にデプロイされていることを確認できました。

Events:
  Type     Reason                  Age    From                     Message
  ----     ------                  ----   ----                     -------
  Normal   Pulling                 2m24s  kubelet                  Pulling image "xxx.dkr.ecr.us-west-2.amazonaws.com/sample-api:v1.0.3"
  Normal   Pulled                  2m22s  kubelet                  Successfully pulled image "xxx.dkr.ecr.us-west-2.amazonaws.com/sample-api:v1.0.3" in 2.562507659s
  Normal   Created                 2m21s  kubelet                  Created container sample-api-container
  Normal   Started                 2m21s  kubelet                  Started container sample-api-container

最後に、kubectl logs -fでログを表示しながら、curl http://xxx-xxx.us-west-2.elb.amazonaws.com/comments/1で API を呼び出してみたところ、無事、ソース変更後のログが出力されることも確認できました。

kubectl logs -f sample-api-deployment-xxx-xxx
> codepipelne installed. // 今回仕込んでおいたログ

さいごに

今回、CodeBuildとCodePipelineを使って、デリバリーパイプラインを導入してみました。

一応やりたかったことはだいたいできましたが、コミット=>タグ作成=>タグプッシュ=>ブランチプッシュの順に作業をする必要がある点を妥協しました。ブランチブッシュを先に実行すると、タグが存在しない状態でパイプラインが実行されタグ名を取得できずにエラーになってしまいます。トリガー周りは柔軟に設定できるようにしてほしい感じです。

あとは、設定が結構分かれてしまったのが微妙です。
GitLab CI、Bitbucket Pipeline、GitHub Actions なら一箇所で設定できそうなのに、AWS だと CodeBuild × 2 + CodePipeline と3箇所に分かれてしまいました。

上記の書簡の通り、今回は AWS のお勉強ということで、AWS のサービス縛りでこの構成になりましたが、他の選択肢がある状況なら自分としてはおそらく別の選択肢を選ぶ可能性が高いです。

また、全三回に分けて「AWS で Web アプリ構築」をやってきたのですが、その中で以下のようにいろんな AWS サービスに触れてみましたが、なんとなく使い方にも慣れてきました。

今回は大部分をコンソール上で設定したので、CloudFormation なり Terraform なりでコードに落とし込むことも勉強することが次の課題かな、と思いますので、気が向いたらやってみたいと思います。

トラブルシューティング

蛇足になりますが、設定に当たってトラブったところを紹介しておきます。

CodeBuild で Docker Daemon が動いてない

  • エラーメッセージ
    Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
  • 対処
    • ビルドプロジェクトを作成する際に、特権付与: Docker イメージを構築するか、ビルドで昇格されたアクセス権限を取得するには、このフラグを有効にしますにチェックを入れる

ECR に docker push できない

  • エラーメッセージ
    no basic auth credentials
  • 対処
    • docker push 前に、AWS クレデンシャル情報をもとに docker login しておく
    • 具体的には
      • AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYの環境変数を設定し
      • aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ECR_URL}を実行する

CodePipeline から実行すると、CodeBuild で Git コマンドエラー

  • エラーメッセージ
    [Container] 2021/08/01 08:40:22 Running command IMAGE_TAG=$(git describe --tags)
    fatal: not a git repository (or any parent up to mount point /codebuild)
    Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
  • 原因
    • CodePipeline のソースステージで、アクションプロバイダーをGitHub(バージョン1)にすると、出力アーティファクト形式はCodePipelineのデフォルト一択となり、Git クローンが作成されないため
  • 対処
    • CODEBUILD_SOURCE_VERSIONのような環境変数を利用することで回避できるような GIT 情報を取得したいだけであればそうする。
    • 上記で解決できないケースは、アクションプロバイダーをGitHub(バージョン2)にすると、出力アーティファクト形式に完全くローンの選択肢が出てくるのでそれを選択する

CodePipeline から実行すると、CodeBuild で権限エラー

  • エラーメッセージ
    authorization failed for primary source and source version xxxxxxx
  • 対処
    • CodeBuild 側に、CodePipeline で作成した GitHub クローンにアクセスする権限がないため
    • こちらの手順を実行する
Posted in: aws