こんにちは、エムスリーエンジニアの園田です。
この記事はAWS FargateでElixirのコンテンツ配信システムを動かしてみた (実装編) - エムスリーテックブログの続きです。
エムスリーでは医療・ヘルスケアサイト向けのコンテンツ配信システムであるChuoi
というサービスを運用しています。先日のポストで、ElasticBeanstalk
からFargate
に運用を切り替えたことについて書きました。
今回は前回に引き続きその実装編で、CodePipeline を利用したデプロイパイプラインの構築について書きます。
まずは構成のおさらいです。
デプロイ周りは以下のような構成です。
社内 Gitlab からの CI/CD パイプライン構築
先日の記事で述べたとおり、弊社ではソース管理にオンプレの Gitlab を使っており、 CodeBuild や CodePipeline のトリガとして利用できません。 そのため、Gitlab からは S3 へソースのアップロードのみを行い、以降の処理は CodePipeline で実行するという構成にしました。
デプロイパイプラインを構築した際の大まかな手順は以下の通りです。
- ビルドスクリプト (buildspec.yml) の実装
- ソース・中間成果物格納バケットの作成
- パイプライン用の IAM ロール作成
- CodePipeline の作成
- Source ステージの設定
- Build ステージの設定 (CodeBuild プロジェクトの作成)
- Deploy ステージの設定
- Gitlab CI のデプロイジョブを設定
ファイル・ディレクトリ構成
前回の記事にも記載しましたが、再掲します。 前回と違い、デプロイパイプラインと直接関係のないファイル類は省略しています。
. ├── dockerfiles/ │ ├── app/ │ ├── log/ │ ├── web/ │ ├── archive-and-upload-src.sh │ ├── build-docker-images.sh │ └── push-to-ecr.sh ├── .gitlab-ci.yml └── buildspec.yml
buildspec.yml の作成
CodeBuild で実行するジョブを定義する buildspec.yml を実装します。
buildspec.yml では以下のことを行います。
- Dockerイメージのビルド
- Dockerイメージを ECR に push
- imagedefinitions.json の生成
buildspec.yml にすべての処理を書くと非常に煩雑になるため、各処理を実行するためのスクリプトは buildspec.yml とは別のファイルとしています。
Dockerイメージのビルド
ファイル・ディレクトリ構成にある dockerfiles/build-docker-images.sh に実装します。内容は以下の通りです。
環境変数のMIX_ENV
とSECRET_KEY_BASE
は CodeBuild プロジェクトのビルド変数として設定されているものが渡されます。
SECRET_KEY_BASE
はParameter Store
に暗号化されて格納しており、ビルド実行時に復号化されて環境変数としてプロセスに渡されます。
echo Build started on `date` echo Building the Docker image... docker build -t "app:latest" \ -f dockerfiles/app/Dockerfile \ --build-arg MIX_ENV=${MIX_ENV} \ --build-arg SECRET_KEY_BASE=${SECRET_KEY_BASE} \ ./ docker build -t "web:latest" ./dockerfiles/web docker build -t "log:latest" ./dockerfiles/log echo Build completed on `date`
Dockerイメージを ECR に push
つづいて dockerfiles/push-to-ecr.sh の中身です。AWS_REGION
は CodeBuild のビルトイン変数なので指定する必要はありませんが、
AWS_ACCOUNT_ID
は CodeBuild のビルトイン変数ではないため、ビルド変数としてプロジェクトに設定する必要があります。
ECR_REPO_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" echo Logging in to Amazon ECR... $(aws ecr get-login --region ${AWS_REGION} --no-include-email) echo Pushing the Docker images... IMAGE_TAG=$(get_image_tag) for repo in app web log; do docker tag "${repo}:latest" "${ECR_REPO_URI}/${repo}:latest" docker tag "${ECR_REPO_URI}/${repo}:latest" "${ECR_REPO_URI}/${repo}:${IMAGE_TAG}" docker push "${ECR_REPO_URI}/${repo}:${IMAGE_TAG}" docker push "${ECR_REPO_URI}/${repo}:latest" done
上のサンプルのIMAGE_TAG
を取得する関数get_image_tag
は以下のような実装になっています。
get_image_tag() { if [ -e .git-commit-hash ]; then cat .git-commit-hash elif [ ! -z "$CODEBUILD_RESOLVED_SOURCE_VERSION" ]; then echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} else echo latest fi }
ソースのルートに.git-commit-hash
ファイルがあればその内容を、
なければ CodeBuild のビルトイン変数であるCODEBUILD_RESOLVED_SOURCE_VERSION
を、それもなければlatest
を返しています。
後述しますが、.git-commit-hash
ファイルは Gitlab CI から S3 にソースをプッシュする際に生成しています。
なお、 CodeCommit や Github をソースリポジトリとする場合、コミットハッシュは環境変数CODEBUILD_RESOLVED_SOURCE_VERSION
から取得可能です。
今回は Gitlab を利用するので、環境変数からは取得できないため、ファイルに書き出しています。
imagedefinitions.json の生成
以下の内容を dockerfiles/push-to-ecr.sh の末尾に追記します。
echo Writing image definitions file... cat <<__JSON__ > imagedefinitions.json [ {"name": "app", "imageUri": "${ECR_REPO_URI}/app:${IMAGE_TAG}"}, {"name": "web", "imageUri": "${ECR_REPO_URI}/web:${IMAGE_TAG}"}, {"name": "log", "imageUri": "${ECR_REPO_URI}/log:${IMAGE_TAG}"} ] __JSON__
CodePipeline のデプロイプロバイダにECS
を利用する場合、このimagedefinitions.json
が必要になります。
コンテナ名とイメージの対応を JSON で渡すことで、 CodePipeline が以下のことをやってくれます。
imagedefinitions.json
の通りにタスク定義
のコンテナイメージを変更し、新しいタスク定義
のリビジョンを作成 (コンテナイメージ以外のパラメータはそのまま)- 新しい
タスク定義
で起動するようにECSサービスを更新
なお、タスク定義の新しいリビジョンは、 現在サービスに適用されているタスク定義から 作成されます。 サービスに適用されていない最新のタスク定義があったとしても、そちらは利用されないのでご注意ください。
buildspec.yml
ビルドとECRプッシュを別ファイルに切り出したので、 buildspec.yml は非常にシンプルです。
version: 0.2 phases: build: commands: - dockerfiles/build-docker-images.sh post_build: commands: - dockerfiles/push-to-ecr.sh artifacts: files: - imagedefinitions.json
ポイントは、imagedefinitions.json
をartifacts
として保存することです。これをしないと CodePipeline がECS
にデプロイできません。
ここまでで AWS リソースを作成するための準備が整いました。次からは AWS の各リソースを作成していきます。
作成するAWSリソースの一覧
マネジメントコンソールでパイプラインを作成する場合に必要なリソースの一覧です。 マネジメントコンソールで作成する場合は CloudWatch Event Rule など一部のリソースが強制的に自動生成されるため、 Terraform で作成する場合と顔ぶれが異なるので注意が必要です。
- ソースおよびパイプラインの中間成果物を格納するバケット
- パイプライン用の IAM ロール (面倒なのでまとめて1つ)
- Parameter Store の SECRET_KEY_BASE パラメータ
- CodePipeline のパイプライン
- CodeBuild のプロジェクト
以下はマネジメントコンソール利用の場合に自動生成されるリソースです。 Terraform で作成する場合は以下のリソースも Terraform のテンプレートに含めます。
- ソースの更新を検知してパイプラインを起動する CloudWatch Event Rule
- 上記 CloudWatch Event Rule の IAM ロール
ソース・中間成果物格納バケットの作成
CodePipeline で S3 をトリガとする場合、以下のバケットが必要です。
Gitlab からソースをアップロードする S3 バケットを作成します。注意点は 1 点だけで、バージョニングを有効にするだけです。
バージョニングが無効な場合、 CodePipeline 実行時に エラーになります。作成時ではないので注意が必要です。
パイプラインの中間成果物を格納するバケットは、何も指定しなければ CodePipeline が自動生成しますが、Terraform フレンドリではないので作成したほうがいいです。
今回の場合はソース格納バケットと共用にしました。
パイプライン用の IAM ロール作成
パイプラインを構成する上で必要なIAM ロールは3つあります。
- ソースの変更を検知して CodePipeline を起動する IAM ロール (
codepipeline:StartPipelineExecution
) - CodeBuild のビルド実行 IAM ロール (成果物の取得・保存と ECR, Parameter Store に対するアクセス)
- CodePipeline 自体の IAM ロール (成果物の取得・保存と各ステージの実行権限)
最初のロールは、マネジメントコンソールで CodePipeline を構築する際に自動で作成されるため、今は作成しませんが、 Terraform で作成する場合は定義を実装する必要があります。
管理が煩雑にならないように、必要な権限を 1 つの IAM ロールにまとめました。
1 つのロールに複数の役割を持たせるのはアンチパターンですが、デリバリプロセス全体のロールということで、1 つにまとめても問題ないと判断しました。
すべての権限をまとめた IAM ロールのポリシーは以下のようになります。
まずは AssumeRolePolicy です。CodeBuild, CodePipeline の信頼関係を持ちます。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRole", "Principal": { "Service": [ "codebuild.amazonaws.com", "codepipeline.amazonaws.com" ] } } ] }
続いて CodeBuild のための IAM ロールポリシーです。 ECR に対するアクセスと、パラメータの参照、ソース取得、成果物の格納、ログの出力の権限が必要です。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}", "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}/*" ] }, { "Effect": "Allow", "Action": [ "logs:PutLogEvents", "logs:CreateLogStream", "logs:CreateLogGroup", "ecr:GetAuthorizationToken" ], "Resource": "*" }, { "Effect": "Allow", "Action": "ssm:GetParameters", "Resource": "arn:aws:codepipeline:ap-northeast-1:${AWS_ACCOUNT_ID}:parameter/SECRET_KEY_BASE" }, { "Effect": "Allow", "Action": "ecr:*", "Resource": "arn:aws:codepipeline:ap-northeast-1:${AWS_ACCOUNT_ID}:repository/*" } ] }
Parameter Store に KMS を利用している場合は、上記に加え KMS による復号化権限も必要です。
CodePipeline のための IAM ロールポリシーです。CodeBuild, CodeDeploy の実行等の権限が必要です。 Deploy プロバイダに ECS を利用するため、ECS に関する権限が必要です。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}", "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}/*" ] }, { "Effect": "Allow", "Action": [ "iam:PassRole", "elasticloadbalancing:*", "ecs:*", "codedeploy:*", "codebuild:*", "cloudwatch:*" ], "Resource": "*" } ] }
これらのインラインポリシー *1 を持った IAM ロールを作成して利用します。
Parameter Store の SECRET_KEY_BASE パラメータ
CodeBuild でビルド変数として利用する Parameter Store のパラメータは事前に設定しておく必要があります。
平文のパラメータであれば Terraform のテンプレートなどに含めれば問題ないですが、シークレットパラメータは Terraform 管理外にして CLI で事前に登録しておくのが楽です。
*2
aws ssm put-parameter \ --name "SECRET_KEY_BASE" --value "xxx......" \ --type "SecureString" --overwrite
CodePipeline のパイプライン
CodePipeline を作成することで、あわせて以下のリソースが作成されます。
- ソースバケットを監視してパイプラインを起動する
CloudWatch Event Rule
- CodeBuild プロジェクト
マネジメントコンソールで CodePipeline を作成する際に必要なパラメータは以下の通りです。
- ソースとなる S3 バケットと S3 オブジェクトのキー
- CodeBuild のビルド変数
- CodeBuild の IAM ロール (前段で作成済み)
- ECS クラスター名と ECS サービス名 (前回の記事で作成済み)
パイプラインの作成
マネジメントコンソールで CodePipeline の作成を行うと、まずはじめにパイプライン名称の入力を求められます。
パイプライン名称を入力して次のステップ
ボタンを押したら Source ステージの設定に続きます。
Source ステージの設定
ソースを取得するバケットとオブジェクトのキーを入力します。これによりソースを監視する CloudWatch Event Rule
と IAM ロールが自動生成されます。*3
以下を入力して次のステップ
ボタンを押します。
- ソースプロバイダ:
Amazon S3
- Amazon S3 の場所:
s3://${ソースバケット名}/${オブジェクトキー}
- 変更検出オプション:
変更が発生したときに Amazon CloudWatch イベントを使用してパイプラインを自動的に開始する (推奨)
Build ステージの設定と CodeBuild プロジェクトの作成
続いてビルドステージを設定します。ビルドステージでは CodeBuild プロジェクトの作成を行います。
- ビルドプロバイダ:
AWS CodeBuild
- 新しいビルドプロジェクトを作成
- プロジェクト名: CodeBuild のプロジェクト名称
- 説明: 任意の説明
- 環境イメージ:
AWS CodeBuild マネージド型イメージの使用
- OS:
Ubuntu
- ランタイム:
Docker
- バージョン:
aws/codebuild/docker:17.09.0
- ビルド仕様:
ソースコードのルートディレクトリの buildspec.yml を使用
- キャッシュ:
キャッシュなし
*4 - CodeBuild サービスロール: 前段で作成した IAM ロール
- VPC:
- VPC ID:
非 VPC
- VPC ID:
- アドバンスト
- 環境変数
上記を入力したらビルドプロジェクトの保存
ボタンを押して CodeBuild プロジェクトを作成したら、
次のステップ
ボタンを押してデプロイステージの設定に移ります。
Deploy ステージの設定
最後にデプロイステージを設定します。
イメージのファイル名
は、ECS
デプロイプロバイダの場合、デフォルトでimagedefinitions.json
を参照するようになります。
ファイル名を変更する場合は CodeBuild の buildspec.yml で artifacts
に定義したファイル名を入力してください。
動作確認
Gitlab にジョブを作成する前に、 CodePipeline の動作確認を行います。
アプリケーションコードを zip 圧縮して S3 の指定したパスにアップロードしてみます。
zip -r src.zip .dockerignore ./dockerfiles buildspec.yml \ mix.exs mix.lock ./config ./lib ./priv ./rel \ package.json tsconfig.json webpack.config.js yarn.lock ./ts/main
できあがった src.zip
をマネジメントコンソールからアップロードしました。
CodePipeline が起動して ECS へのデプロイが完了することを確認します。
以上で CodePipeline による ECS へのデプロイパイプライン構築は完了です。
Gitlab CI のデプロイジョブを設定
デプロイ IAM ユーザの作成
Gitlab に設定する IAM ユーザを作成します。
設定するポリシーは CodePipeline で監視しているバケットとオブジェクトに対するs3:PutObject
権限のみです。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::${SOURCE_ARTIFACT_BUCKET}/${SOURCE_OBJECT_KEY}" } ] }
Gitlab プロジェクトに IAM 認証情報を設定
Gitlab プロジェクトに AWS の認証情報を設定します。メニューから Settings
> CI / CD
> Variables
を選択して、
作成した IAM ユーザのAWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
を指定します。
アップロードスクリプトの実装
ファイル・ディレクトリ構成にある dockerfiles/archive-and-upload-src.sh にデプロイのスクリプトを実装しました。 やってることはシンプルで、これだけです。
- Git のコミットハッシュをファイルに出力
- テストコードなどを除いたファイルを zip 圧縮
- 圧縮したファイルを S3 にアップロード
SOURCES=" .dockerignore ./dockerfiles buildspec.yml mix.exs mix.lock package.json tsconfig.json webpack.config.js yarn.lock ./ts/main ./config ./lib ./priv ./rel " rm -f src.zip git rev-parse HEAD | cut -c 1-8 > .git-commit-hash zip -r src.zip .git-commit-hash $SOURCES aws s3api put-object \ --bucket $S3_BUCKET --key ${S3_OBJECT_KEY} \ --body src.zip
Gitlab CI のジョブを作成
.gitlab-ci.yml
にデプロイのマニュアルジョブを追加します。
trigger_pipeline: image: python variables: AWS_REGION: ap-northeast-1 S3_BUCKET: hogehoge-source-dev S3_OBJECT_KEY: hogehoge-src.zip before_script: - apt-get update && apt-get install -y zip git - pip install --upgrade awscli - aws configure set default.region ${AWS_REGION} script: - dockerfiles/archive-and-upload-src.sh when: manual
デプロイパイプラインの実行
Gitlab からマニュアルジョブを実行し、CodePipeline が正常終了すれば成功です。
次回は前回構築した Fargate 構成、および今回の CodePipeline を含めた Terraform テンプレートの説明をしたいと思います。
エンジニア募集中
エムスリーでは AWS の新サービスを試したいエンジニアを募集しています。 AWS に限らず、Firebase や GCP なども積極的に利活用しています。興味が湧いた方はぜひ Tech Talk やカジュアル面談にお越しください。 お問い合わせは以下フォームより、お待ちしております。
*1:ユーザ定義ポリシーを作成してそれをロールにアタッチするというのがベストプラクティスのようですが、私はそれを実践する気はありません。 作成したポリシーを複数ロールで利用することがほぼありえないのと、 Terraform などを利用する場合、インラインポリシーでも十分に管理可能であるからです。 ユーザ定義ポリシーは依存関係の複雑化を促し、削除可能なリソースなのかどうかの判断を迷わせるので、私は Disposable なインラインポリシーを好んで使っています。
*2:シークレットパラメータの Infrastructure as Code 上での扱いに関してはいろんな方法があると思いますが、 私は手運用にしてドキュメントに運用手順を記載する方法を取っています。 なぜなら、シークレットをコード化するためにシークレット情報の暗号化など複雑な仕組みを構築しても、たいていの場合それは有効に機能せず、 結局は手作業で登録や更新を行うことになることが多いからです。扱うシークレットが少ないのであればなおさらです。
*3:CodePipeline の作成が完了したタイミングで作成されます。