👏

CodeDeployでRails(Docker)をBlue/Greenデプロイする (CI/CDまでの道⑨)

2022/02/01に公開

はじめに

前回はCodeBuildを利用してCIを行いました。今回はタイトルにある通り、CodeDeployを利用してCDに挑戦していきます。いままでの記事の技術をすべて利用して完成させていきますので、説明があいまいな個所は以前の記事で詳しく話していますので合わせて確認してください。

作成に利用するリポジトリはこちら

CI/CDまでの道シリーズ

環境

  • wsl2 (ubuntu20.04)
  • docker 20.10.9
  • docker-compose 1.29.1
  • git 2.25.1
  • vscode

注意

このハンズオンでは以下の書籍を用います。

AWSではじめるインフラ構築入門 安全で堅牢な本番環境のつくり方

AWSのインフラ環境構築に利用します。書籍で説明が足りている部分については説明を割愛させていただきます。

本ハンズオンの構成は以下となります。

インフラ環境構築

VPC

書籍4.2通りに作成します。

サブネット

書籍4.3通りに作成します。

インターネットゲートウェイ

書籍4.4通りに作成します。

ルートテーブル

書籍4.6のsample-rt-publicのみを作成します。

セキュリティグループ

書籍4.7のsample-sg-elbをまず作成します。
そのあと第7回を参考にsample-sg-ecssample-sg-dbを作成します。

データベース

書籍8章通りに作成します。パスワードはpasswordで設定します。

ECR

第8回を参考にsample-railssample-nginxのリポジトリを作成します。

CodeDeploy

準備

まずは、CodeDeployを行うためにファイルを修正していきます。

最初のデプロイ時にDBを作成するのでentrypoint.shを修正します。

entrypoint.sh
#!/bin/sh
set -e
rm -f /myapp/tmp/pids/server.pid
bundle exec rails db:create
bundle exec rails db:migrate
bundle exec rails db:seed
exec "$@"

※ このスクリプトはCodeBuildtestのタイミングにはdb:createをコメントにしてください。

イメージをPushします。

NginxのDockerfileを修正します。

containers/nginx/Dockerfile
FROM nginx:alpine

# インクルード用のディレクトリ内を削除
RUN rm -f /etc/nginx/conf.d/*

# Nginxの設定ファイルをコンテナにコピー
# ADD nginx.conf /etc/nginx/conf.d/myapp.conf

# docker buildのときは以下のコメントをはずす
ADD /containers/nginx/nginx.conf /etc/nginx/conf.d/myapp.conf

# ビルド完了後にNginxを起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

第7回を参考にECRのコマンドでNginxをPushします。

Railsは2つ目のコマンドを以下に変更します。

$ docker build --target build  --no-cache -t sample-rails .

そのあと、本番で利用するイメージをsample-railsにPushします。実際に利用するイメージにはタグstagingをつけることにします。

$ docker build --target production  --no-cache -t sample-rails .
$ docker tag sample-rails:latest [アカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-rails:staging
$ docker push [アカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-rails:staging

config/credentials.yml.encを削除して、作成したcredentials.yml.encmaster.keyconfig/に配置してください。

CodeBuildに必要なファイル

このあとCodePipelineを利用するにあたって必要なファイルが2つあるのでカレントディレクトリに作成します。

まずはappspec.ymlを作成します。

appspec.yml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "nginx"
          ContainerPort: 80
        PlatformVersion: "LATEST"

次にtaskdef.jsonを作成します。このファイルは前回タスク定義したときの内容をymlファイルで書いたものとなります。

taskdef.json
{
	"family": "sample-ecs",
	"cpu": "256",
	"memory": "512",
	"networkMode": "awsvpc",
	"requiresCompatibilities": ["FARGATE"],
	"executionRoleArn": "arn:aws:iam::[アカウントID]:role/ecsTaskExecutionRole",
	"taskRoleArn": "arn:aws:iam::[アカウントID]:role/ecsTaskExecutionRole",
	"containerDefinitions": [
		{
			"name": "rails",
			"image": "[アカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-rails:staging",
			"portMappings": [
				{
					"hostPort": 3000,
					"protocol": "tcp",
					"containerPort": 3000
				}
			],
			"environment": [
				{
					"name": "DB_USERNAME",
					"value": "admin"
				},
				{
					"name": "DB_PASSWORD",
					"value": "password"
				},
				{
					"name": "DB_DATABASE",
					"value": "myapp"
				},
				{
					"name": "DB_HOST",
					"value": "[RDSのエンドポイント]"
				},
				{
					"name": "SECRET_KEY_BASE",
					"value": "[master.keyの値]"
				}
			],
			"essential": true
		},
		{
			"name": "nginx",
			"image": "[アカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-nginx",
			"portMappings": [
				{
					"hostPort": 80,
					"protocol": "tcp",
					"containerPort": 80
				}
			],
			"volumesFrom": [
				{
					"sourceContainer": "rails",
					"readOnly": true
				}
			],
			"essential": true
		}
	]
}

taskdef.jsonはそれぞれのアカウントIDとmaster.key、RDSエンドポイントで書き換えしてください。

この2つがCodePipelineCodeBuildを利用するのに必要になります。

Gitのリポジトリの作成

前回を参考にしてGitHubのリポジトリを作成してPushを行います。

ここではCICD_Road_zennとします。

IAMロールの作成

CodeDeployに利用するロールを作成します。

項目
ユースケースの選択 CodeDeploy(CodeDeploy ECS)
ロール名 CodeDeployRole

追加でポリシーをアタッチします。

AmazonS3FullAccessAmazonECS_FullAccessを追加しました。かなり強い権限になりますので実際に運用する場合は調べてみてください。

S3の作成

CodeDeployをCodePipelineで使う際には、ビルドで作成されるZipファイルの中にappspec.ymltaskdef.jsonが入った状態にしなければなりません。

そこでbuildspec.ymlでアーティファクトを設定して出力するZipファイルに2つのファイルが入るように修正を加えます。

buildspec.yml
version: 0.2

env:
  variables:
    DOCKER_BUILDKIT: "1"

phases:
  pre_build:
    commands:
      - echo Logiging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - docker pull $AWS_ECR_BUILD_REPOSITORY:latest
      - docker tag $AWS_ECR_BUILD_REPOSITORY:latest build:latest

      - IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)

  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build --target production --cache-from build:latest --build-arg SECRET_KEY_BASE=$SECRET_KEY_BASE --build-arg BUILDKIT_INLINE_CACHE=1 -t $IMAGE_NAME:$IMAGE_TAG .
      - docker build --target build --cache-from build:latest --build-arg SECRET_KEY_BASE=$SECRET_KEY_BASE --build-arg BUILDKIT_INLINE_CACHE=1 -t build:latest .
      - docker tag $IMAGE_NAME:$IMAGE_TAG $AWS_ECR_BUILD_REPOSITORY:$IMAGE_TAG
      - docker tag $IMAGE_NAME:$IMAGE_TAG $AWS_ECR_BUILD_REPOSITORY:staging
      - docker tag build:latest $AWS_ECR_BUILD_REPOSITORY:latest

  post_build:
    commands:
      - echo Build completed on `data`
      - echo Pushing the Docker image...
      - docker images
      - docker push $AWS_ECR_BUILD_REPOSITORY:latest
      - docker push $AWS_ECR_BUILD_REPOSITORY:$IMAGE_TAG
      - docker push $AWS_ECR_BUILD_REPOSITORY:staging
      - echo "[{\"name\":\"sample-rails\",\"imageUri\":\"${AWS_ECR_BUILD_REPOSITORY}:${IMAGE_TAG}\"}]" > imagedefinitions.json

artifacts:
  files:
    - imagedefinitions.json
    - appspec.yml
    - taskdef.json

また、Zipを保存するためのS3を用意してください。
ここではsample-deploy-22131としました。

ターゲットグループとロードバランサー

今回はCodeDeployでBlue/Greenデプロイをするのでそれを考慮してロードバランサーの設定をしていきます。

EC2のダッシュボードからターゲットグループを開いて以下を作成します。

項目名
choose target type IP addresses
Target group name BlueGreenTarget1
VPC sample-vpc
Health check path /test

ターゲット登録は行わないで大丈夫です。

次にロードバランサーを作成します。

項目名
Select load balancer type Application Load Balancer
Load balancer name sample-elb
VPC sample-vpc
Mappings ap-northeast-1a: sample-subnet-public01
ap-northeast-1c: sample-subnet-public02
Security group default
sample-elb
Listeners and routing Foward to: BlueGreenTarget1

タスク定義とFargate

初めは手作業でFargateを作成します。
ECSのタスク定義を作成します。第7回を参考に作成してください。

変更点はRailsに2つあります。

イメージを変更します。
タグをstagingにします。

Railsの環境変数にSECRET_KEY_BASEを追加して、値にmaster.keyの値を登録します。

クラスターも記事を参考に作成してください。

サービスはBluee/Greenデプロイに対応できるようにするためにすこし設定が変わります。

第7回との変更点だけを表に示します。

項目名
デプロイメントタイプ* Blue/Green デプロイメント (AWS CodeDeploy を使用)
CodeDeploy のサービスロール CodeDeployRole
nginx:80:80をロードバランサーに追加
プロダクションリスナーポート 80:HTTP
テストリスナーポート 新規作成: 8080
ターゲットグループ 1 の名前 BlueGreenTarget1
ターゲットグループ 2 の名前 新規作成: BlueGreenTarget2
Health check path /test

作成のタイミングでCodeDeployのデプロイメントも作成されます。既にある場合はエラーになるのでCodeDeployのアプリケーションとECSのサービスを削除してから再度作成してください。

起動確認

起動したタスクのパブリックIP/testでアクセスして起動できれば成功です。起動できない場合はCloudWatchのログをみて元のDockerfileや環境変数の設定を修正します。

CSSやJavascriptも問題ありませんでした。

リポジトリの更新

これからCodeDeployをしていくうえで、entrypoint.shdb:createを行うとエラーになってしまうので修正していきます。

entrypoinst.sh
#!/bin/sh
set -e
rm -f /myapp/tmp/pids/server.pid
# bundle exec rails db:create
# bundle exec rails db:migrate
# bundle exec rails db:seed
exec "$@"

今回はmigrateするようなものもないので合わせてコメントにしました。

この状態でリポジトリにPushして更新してください。

CodeBuild

第8回を参考にbuildspec.ymltestspec.ymlを動かすビルドプロジェクトを作成してください。

buildspec.ymlでビルドのプロジェクトを作成するときには、アーティファクトを設定します。

項目名
タイプ Amazon S3
バケット名 作成したバケット
アーティファクトのパッケージ化 Zip

テストのプロジェクトは記事通りで大丈夫です。

CodePipeline

先ほどサービスを作成した際にすでにCodeDeployは作成されているので、CodePipelineを作成します。

作成は第8回を参考に行いますが、今回はデプロイステージをスキップせずに設定します。

記事と変更する箇所のみ表に示します。

項目名
高度な設定:アーティファクトストア カスタムロケーション
バケット 作成したバケット
デプロイプロバイダー Amazon ECS(ブルー/グリーン)
AWS CodeDeploy アプリケーション名 AppECS-sample-cluster-sample-service
AWS CodeDeploy デプロイグループ DgpECS-sample-cluster-sample-service
Amazon ECS タスク定義 入力アーティファクトを選択する: BuildArtifact
taskdef.json: taskdef.json
AWS CodeDeploy AppSpec ファイル 入力アーティファクトを選択する: BuildArtifact
appspec.yml

パイプラインを作成して、テストステージも追加します。
そのあとで、変更をリリースして正しく動作するかを確かめます。

CodeDeployの「アプリケーション」→「デプロイの履歴」からBlue/Greenデプロイの状態を確認できます。

Greenが100%になったら「元のタスクセットの終了」をクリックして入れ替え完了です。

いくら待っても50%から進まない時

私はここでinstall step(50%)がずっと終わらない現象が起きました。
この現象はロードバランサーが疎通できていない、コンテナが正しく立ち上がっていないのどちらかが考えられます。

まずはECSの起動しているタスクのパブリックIPから正しくアクセスできるかを確認します。できない場合は、タスクの更新からCloud Watchの自動ログ出力にチェックをいれて、再度サービスを作成して起動してエラーの原因を突き止めます。

ロードバランサーの場合は、health check pathにアクセスして200OKが返ってきていないので先に進んでいないことが考えられます。今回の場合は/testしか用意していないため/にアクセスするとNginxエラー(404)になるので疎通が確認できないという自体になります。改めてロードバランサー、ターゲットグループ、ECSを作り直して設定を確かめると治るかと思います。

また、タスクの更新をECSサービスから行うことでCodeDeploy(Blue/Green)を動かすことも可能です。

ドメイン設定

第7回のドメインの取得、SSLサーバー証明書発行、Route53の設定の手順で行います。

今回はロードバランサーをドメインでアクセスできるようにする設定だけで大丈夫です。

おわりに

作成したものはこちらのリポジトリに用意しています。

ついに開発環境構築からCI/CDまで行うことができるようになりました!

第10回はCloud FormationによるIaCに挑戦して終わりたいと思います。

参考

GitHubで編集を提案

Discussion