概要
AWSのコンソール上でCodePipelineを構築し、ECS FargateをBlue/Greenデプロイを使ってデプロイする方法について解説していきたいと思います
前提
- リージョンはap-northeast-1を使用
- VPC、ECS、RDS、ALBを作成済み
- ECRリポジトリ内にNginxとDjangoのDocker imageがあること
- Blue/Greenデプロイ用のターゲットグループとリスナーを設定済み
- DjangoのプロジェクトをECSへデプロイ済み
- 対象プロジェクトのGitHubリポジトリを作成済み
ディレクトリ構成
tree
・
├── backend # バックエンドのソースコード
├── codebuild
│ ├── buildspec_django.yml
│ └── buildspec_nginx.yml
├── codedeploy
│ └── appspec.yml
├── containers
│ ├── django
│ │ ├── Dockerfile.prd
│ │ └── entrypoint.prd.sh
│ └── nginx
│ ├── Dockerfile.prd
│ └── nginx.prd.conf
└── ecs
└── taskdef.json
Blue/Greenデプロイとは?
同じアプリケーションの異なるバージョンを実行している同一の環境間でトラフィックを移行することでデプロイする手法です
Blue/Greenデプロイをする際は新しい環境(Green)を古い環境(Blue)を一緒に起動させ、トラフィックを新しい環境(Green)に再ルーティングします
Blue/Greenデプロイをすることで問題の検出をロールバックする前に、新しいバージョンを監視およびテストできることに加え、アプリケーションの更新中のダウンタイムを最小限に抑えた上でダウンタイムとロールバックのリスクを軽減できます
CodePipelineの構築
CodePipelineではBlue/Greenデプロイを使用してコンテナをデプロイするパイプラインを構築できるのでその方法について今から解説します
パイプラインの設定
今回はパイプラインのバージョンをV1にします
また、CodePipelineを新規で作成するのでサービスロールも新規で作成します
ソースステージを追加
今回はGitHubのソースが対象ブランチにpushされたことをトリガーにCodePipelineを実行するよう設定します
ソースプロバイダはGitHub(バージョン2)を選択します
今回はmainブランチへpushしたらCodePipelineを実行するよう設定します
ビルドステージを追加
Dockerfileをビルドするためのステージを作成します
今回はAWS CodeBuildを選択します
新規でCodePipelineを作成しているのでAWS CodeBuildも同様に新規で作成するのでプロジェクトを作成する、を選択します
CodeBuildの作成
プロジェクト名を記載します
画像の通りの項目を選択します
環境の設定を行います
イメージに関してですがバージョンが古すぎると比較的新しいPythonのバージョンをサポートしてない時があるので今回はstandard:5.0を選択します
イメージのバージョンも常に最新のイメージを使用するようにします
追加設定の箇所で環境変数の設定を行います
必要になる環境変数は以下の通りです
環境変数 | 値 |
---|---|
DOCKERHUB_USER | 自身のDockerHubのユーザ名 |
DOCKERHUB_TOKEN | 自身のDockerHubのトークン |
DJANGO_DOCKERFILE_PATH | リポジトリ内のDjangoのDockerfileのパス |
NGINX_DOCKERFILE_PATH | リポジトリ内のNginxのDockerfileのパス |
ECR_DJANGO_REPOSITORY_URI | DjangoのDockerfileを格納しているECRリポジトリのパス |
ECR_NGINX_REPOSITORY_URI | NginxのDockerfileを格納しているECRリポジトリのパス |
AWS_DEFAULT_REGION | リージョン名(ap-northeast-1) |
AWS_DEFAULT_ACCOUNT | AWSのアカウントID |
DockerHubのアクセストークンの作成方法は以下の公式ドキュメントを参照してください
Buildspec
Buildspecを作成します
今回は複数のbuildspecファイルを使ってビルドするのでビルドコマンドの挿入を選択し、エディタに切り替えを選択します
後ほどCodeDeployの箇所で解説しますがIdentifierを元に入力アーティファクトを選択します
以下のようにbuildspecにDjango用とNginx用のymlファイルのパスを記載します
version: 0.2
batch:
build-list:
- identifier: BuildBackEndDjangoArtifact
buildspec: codebuild/buildspec_django.yml
- identifier: BuildBackEndNginxArtifact
buildspec: codebuild/buildspec_nginx.yml
バッチ設定
今回は複数のDockerfileを使って同時にBuildするのでバッチ設定を行います
新規でバッチサービスロールを作成します
ログ
デバッグの際に必要なのでCloudWatchでログが見れるよう設定します
バッチビルドを選択します
デプロイステージの追加
デプロイプロバイダーをAmazon ECS(ブルー/グリーン)に設定します
新規でCodePipelineを作成しているのでAWS CodeDeployも同様に新規で作成します
CodeDeploy
CodeDeployのアプリケーションを作成します
CodeDeployのアプリケーションを作成後、デプロイグループを作成します
CodeDeploy用のロールを作成します
今回私はCodeDeployRoleという名前で作成しています
AWSCodeDeployRoleForECSのポリシーをアタッチします
詳細は以下のとおりです
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"ecs:DescribeServices",
"ecs:CreateTaskSet",
"ecs:UpdateServicePrimaryTaskSet",
"ecs:DeleteTaskSet",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:ModifyListener",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:ModifyRule",
"lambda:InvokeFunction",
"cloudwatch:DescribeAlarms",
"sns:Publish",
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"iam:PassRole"
],
"Effect": "Allow",
"Resource": "*",
"Condition": {
"StringLike": {
"iam:PassedToService": [
"ecs-tasks.amazonaws.com"
]
}
}
}
]
}
環境設定ではクラスターとサービス名を選択します
LoadBalanceの設定では
- ロードバランサー本体
- 本稼働用とテスト用のリスナーポート
- ターゲットグループ
の選択を行います
デプロイ設定については今回は以下の通りにします
設定が終わったらデプロイグループを作成、ボタンを押します
CodeDeployの設定画面に戻ります
taskdef.jsonとappspec.ymlをまだ作成していないので今はBuildArtifactを選択した状態にします
また、CodePipelineをコンソールで作成するときは複数アーティファクトを指定できないので一旦そのままにします
- Source
- Build
- Deploy
ステージの設定が終わったらパイプラインを作成する、ボタンを押してCodePipelineを構築します
ファイルの作成
- DjangoとNginxのbuildspec.ymlファイル
- appspec.yml
- taskdef.json(タスク定義ファイル)
の3つを作成します
buildspecの作成
今回はcodebuildフォルダ配下にymlファイルを作成します
buildspecのphaseは
- pre_build
- build
- post_build
の3種類に分類されるので1つずつ説明します
pre_build
Dockerfileをbuildする前にECRにログインするフェーズです
ログインする際はCodeBuildで設定した
- DOCKERHUB_USER
- DOCKERHUB_TOKEN
の環境変数を使ってECRにログインします
また、ビルドの解決済みソースバージョンの識別子の前から7文字をIMAGE_TAGの変数に代入します
build
DJANGO_DOCKERFILE_PATH内にあるDockerfileをbuildし、タグ付けします
post_build
buildが完了したDockerfileをECRへpushし、imageDetail.jsonに書き込み処理を行います
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_DEFAULT_ACCOUNT.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- echo Logging in to Docker Hub...
- echo $DOCKERHUB_TOKEN | docker login -u $DOCKERHUB_USER --password-stdin
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- cd $CODEBUILD_SRC_DIR
- docker build -f $DJANGO_DOCKERFILE_PATH -t $ECR_DJANGO_REPOSITORY_URI:$IMAGE_TAG .
- docker tag $ECR_DJANGO_REPOSITORY_URI:$IMAGE_TAG $ECR_DJANGO_REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $ECR_DJANGO_REPOSITORY_URI:$IMAGE_TAG
- echo Writing imageDetail.json...
- printf '{"Version":"1.0","ImageURI":"%s"}' $ECR_DJANGO_REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
files:
- imageDetail.json
Nginx用のbuildspecファイルも同様に作成します
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_DEFAULT_ACCOUNT.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- echo Logging in to Docker Hub...
- echo $DOCKERHUB_TOKEN | docker login -u $DOCKERHUB_USER --password-stdin
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- cd $CODEBUILD_SRC_DIR
- docker build -f $NGINX_DOCKERFILE_PATH -t $ECR_NGINX_REPOSITORY_URI:$IMAGE_TAG .
- docker tag $ECR_NGINX_REPOSITORY_URI:$IMAGE_TAG $ECR_NGINX_REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $ECR_NGINX_REPOSITORY_URI:$IMAGE_TAG
- echo Writing imageDetail.json...
- printf '{"Version":"1.0","ImageURI":"%s"}' $ECR_NGINX_REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
files:
- imageDetail.json
appspecの作成
今回はcodedeployフォルダ配下にymlファイルを作成します
TaskDefinitionの箇所にを設定します
CodePipeline実行時にCodeDeployで設定したECSタスクのパスが自動的に代入されます
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "<TASK_DEFINITION>"
LoadBalancerInfo:
ContainerName: "web"
ContainerPort: "80"
taskdefの作成
今回はecsフォルダ配下にjsonファイルを作成します
ECSのタスク定義に以下の変数が入るようにします
- NGINX_IMAGE_NAME
- DJANGO_IMAGE_NAME
appspec.yml同様、CodeDeployの設定に追加します
{
"taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/my-project-dev-back-taskdef:18",
"containerDefinitions": [
{
"name": "web",
"image": "<NGINX_IMAGE_NAME>",
"cpu": 0,
"links": [],
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"essential": true,
"entryPoint": [],
"command": [],
"environment": [],
"environmentFiles": [],
"mountPoints": [
{
"sourceVolume": "tmp-data",
"containerPath": "/code/tmp"
}
],
"volumesFrom": [],
"secrets": [],
"dependsOn": [
{
"containerName": "app",
"condition": "START"
}
],
"dnsServers": [],
"dnsSearchDomains": [],
"extraHosts": [],
"dockerSecurityOptions": [],
"dockerLabels": {},
"ulimits": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-project/dev/back/nginx",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "my-project"
},
"secretOptions": []
},
"systemControls": []
},
{
"name": "app",
"image": "<DJANGO_IMAGE_NAME>",
"cpu": 0,
"links": [],
"portMappings": [
{
"containerPort": 8000,
"hostPort": 8000,
"protocol": "tcp"
}
],
"essential": true,
"entryPoint": [
"/usr/local/bin/entrypoint.prd.sh"
],
"command": [],
"environment": [],
"environmentFiles": [],
"mountPoints": [
{
"sourceVolume": "tmp-data",
"containerPath": "/code/tmp"
}
],
"volumesFrom": [],
"secrets": [
{
"name": "POSTGRES_NAME",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/POSTGRES_NAME"
},
{
"name": "POSTGRES_USER",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/POSTGRES_USER"
},
{
"name": "POSTGRES_PASSWORD",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/POSTGRES_PASSWORD"
},
{
"name": "POSTGRES_PORT",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/POSTGRES_PORT"
},
{
"name": "POSTGRES_HOST",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/POSTGRES_HOST"
},
{
"name": "SECRET_KEY",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/SECRET_KEY"
},
{
"name": "ALLOWED_HOSTS",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/ALLOWED_HOSTS"
},
{
"name": "AWS_DEFAULT_REGION_NAME",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/AWS_DEFAULT_REGION_NAME"
},
{
"name": "TRUSTED_ORIGINS",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/TRUSTED_ORIGINS"
},
{
"name": "DJANGO_SETTINGS_MODULE",
"valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/my-project/dev/DJANGO_SETTINGS_MODULE"
}
],
"dnsServers": [],
"dnsSearchDomains": [],
"extraHosts": [],
"dockerSecurityOptions": [],
"dockerLabels": {},
"ulimits": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-project/dev/back/django",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "my-project"
},
"secretOptions": []
},
"systemControls": []
}
],
"family": "my-project-dev-back-taskdef",
"taskRoleArn": "arn:aws:iam::XXXXXXXXXXXX:role/service-role/ECSTaskRole-my-project-dev",
"executionRoleArn": "arn:aws:iam::XXXXXXXXXXXX:role/service-role/ECSTaskExecutionRole-my-project-dev",
"networkMode": "awsvpc",
"revision": 18,
"volumes": [
{
"name": "tmp-data",
"host": {}
}
],
"status": "ACTIVE",
"requiresAttributes": [
{
"name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
},
{
"name": "ecs.capability.execution-role-awslogs"
},
{
"name": "com.amazonaws.ecs.capability.ecr-auth"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.17"
},
{
"name": "com.amazonaws.ecs.capability.task-iam-role"
},
{
"name": "ecs.capability.container-ordering"
},
{
"name": "ecs.capability.execution-role-ecr-pull"
},
{
"name": "ecs.capability.secrets.ssm.environment-variables"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
},
{
"name": "ecs.capability.task-eni"
}
],
"placementConstraints": [],
"compatibilities": [
"EC2",
"FARGATE"
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "512",
"memory": "1024",
"registeredAt": "2024-01-05T02:48:18.158Z",
"registeredBy": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/XXXXXXXXXXXX/XXXXXXXXXXXX",
"tags": [
{
"key": "ProjectName",
"value": "my-project"
},
{
"key": "Environment",
"value": "dev"
}
]
}
CodePipeline内のビルドとデプロイステージの修正
作成したCodePipelineへアクセスします
CodePipelineのステージを修正する際は編集する、を押します
出力アーティファクトに
- BuildBackEndDjangoArtifact
- BuildBackEndNginxArtifact
を指定します
出力アーティファクト名はidentifierと揃える必要があります
version: 0.2
batch:
build-list:
- identifier: BuildBackEndDjangoArtifact
buildspec: codebuild/buildspec_django.yml
- identifier: BuildBackEndNginxArtifact
buildspec: codebuild/buildspec_nginx.yml
入力アーティファクトはSourceArtifactに加えて
- BuildBackEndDjangoArtifact
- BuildBackEndNginxArtifact
を追加する必要があります
SourceArtifactを選択した上で
- タスク定義
- appspec
ファイルのパスを記載します
また、入力アーティファクトとタスク定義のプレースホルダー文字をDjangoとNginx別でそれぞれ記入したら完了です
実際にデプロイしてみよう!
変更内容をmainブランチにpushするとCodePipelineが実行されます
以下のようにパイプラインが実行できていれば成功です
参考