0
0

[Django+Nginx] CodePipelineを使ってECS FargateのBlue/Greenデプロイを実現しよう!

Last updated at Posted at 2024-01-30

概要

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を新規で作成するのでサービスロールも新規で作成します

スクリーンショット 2024-01-25 9.43.05.png

ソースステージを追加

今回はGitHubのソースが対象ブランチにpushされたことをトリガーにCodePipelineを実行するよう設定します
ソースプロバイダはGitHub(バージョン2)を選択します

スクリーンショット 2024-01-25 9.44.05.png

今回はmainブランチへpushしたらCodePipelineを実行するよう設定します
スクリーンショット 2024-01-25 9.44.23.png

ビルドステージを追加

Dockerfileをビルドするためのステージを作成します
今回はAWS CodeBuildを選択します
新規でCodePipelineを作成しているのでAWS CodeBuildも同様に新規で作成するのでプロジェクトを作成する、を選択します

スクリーンショット 2024-01-25 9.45.17.png

CodeBuildの作成

プロジェクト名を記載します
画像の通りの項目を選択します

スクリーンショット 2024-01-25 9.47.05.png

環境の設定を行います
イメージに関してですがバージョンが古すぎると比較的新しいPythonのバージョンをサポートしてない時があるので今回はstandard:5.0を選択します
イメージのバージョンも常に最新のイメージを使用するようにします

スクリーンショット 2024-01-25 9.48.53.png

新しいロールを作成します
スクリーンショット 2024-01-25 9.49.14.png

追加設定の箇所で環境変数の設定を行います

スクリーンショット 2024-01-25 9.54.40.png

必要になる環境変数は以下の通りです

環境変数
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ファイルを使ってビルドするのでビルドコマンドの挿入を選択し、エディタに切り替えを選択します

スクリーンショット 2024-01-25 10.04.37.png

スクリーンショット 2024-01-25 10.06.35.png

後ほど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するのでバッチ設定を行います
新規でバッチサービスロールを作成します

スクリーンショット 2024-01-25 10.21.16.png

ログ

デバッグの際に必要なのでCloudWatchでログが見れるよう設定します

スクリーンショット 2024-01-25 10.21.27.png

バッチビルドを選択します

スクリーンショット 2024-01-25 10.26.35.png

デプロイステージの追加

デプロイプロバイダーをAmazon ECS(ブルー/グリーン)に設定します
新規でCodePipelineを作成しているのでAWS CodeDeployも同様に新規で作成します

CodeDeploy

CodeDeployのアプリケーションを作成します

スクリーンショット 2024-01-25 10.27.10.png

CodeDeployのアプリケーションを作成後、デプロイグループを作成します

スクリーンショット 2024-01-27 10.38.02.png

デプロイグループ名とサービスロールを設定します
スクリーンショット 2024-01-27 10.41.08.png

CodeDeploy用のロールを作成します
今回私はCodeDeployRoleという名前で作成しています

スクリーンショット 2024-01-30 9.24.57.png

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の設定では

  • ロードバランサー本体
  • 本稼働用とテスト用のリスナーポート
  • ターゲットグループ

の選択を行います

スクリーンショット 2024-01-30 9.26.03.png

デプロイ設定については今回は以下の通りにします
設定が終わったらデプロイグループを作成、ボタンを押します

スクリーンショット 2024-01-30 9.26.16.png

CodeDeployの設定画面に戻ります
taskdef.jsonとappspec.ymlをまだ作成していないので今はBuildArtifactを選択した状態にします
また、CodePipelineをコンソールで作成するときは複数アーティファクトを指定できないので一旦そのままにします

スクリーンショット 2024-01-30 9.40.09.png

  • Source
  • Build
  • Deploy

ステージの設定が終わったらパイプラインを作成する、ボタンを押してCodePipelineを構築します

deploy-stage.png

ファイルの作成

  • 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の変数に代入します

image_tag.png

build

DJANGO_DOCKERFILE_PATH内にあるDockerfileをbuildし、タグ付けします

post_build

buildが完了したDockerfileをECRへpushし、imageDetail.jsonに書き込み処理を行います

codebuild/buildspec_django.yml
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ファイルも同様に作成します

codebuild/buildspec_nginx.yml
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タスクのパスが自動的に代入されます

codedeploy/appspec.yml
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の設定に追加します

ecs/taskdef.json
{
    "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のステージを修正する際は編集する、を押します

スクリーンショット 2024-01-30 11.45.52.png

まずはビルドステージを修正します
スクリーンショット 2024-01-30 11.47.10.png

出力アーティファクトに

  • BuildBackEndDjangoArtifact
  • BuildBackEndNginxArtifact

を指定します
出力アーティファクト名はidentifierと揃える必要があります

version: 0.2
batch:
  build-list:
    - identifier: BuildBackEndDjangoArtifact
      buildspec: codebuild/buildspec_django.yml
    - identifier: BuildBackEndNginxArtifact
      buildspec: codebuild/buildspec_nginx.yml

スクリーンショット 2024-01-30 14.44.29.png

次にデプロイステージを修正します
スクリーンショット 2024-01-30 11.46.40.png

入力アーティファクトはSourceArtifactに加えて

  • BuildBackEndDjangoArtifact
  • BuildBackEndNginxArtifact

を追加する必要があります

スクリーンショット 2024-01-30 14.48.51.png

SourceArtifactを選択した上で

  • タスク定義
  • appspec

ファイルのパスを記載します
また、入力アーティファクトとタスク定義のプレースホルダー文字をDjangoとNginx別でそれぞれ記入したら完了です

スクリーンショット 2024-01-30 14.49.03.png

実際にデプロイしてみよう!

変更内容をmainブランチにpushするとCodePipelineが実行されます
以下のようにパイプラインが実行できていれば成功です

スクリーンショット 2024-01-27 10.45.14.png

スクリーンショット 2024-01-27 10.46.08.png

スクリーンショット 2024-01-27 10.46.21.png

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0