LoginSignup
0
0

More than 1 year has passed since last update.

CloudFormationでECSのBlue/Greenデプロイを構築(その2ープレースホルダ方法)

Last updated at Posted at 2021-06-22

概要

本文はAWS ECS(Fargate)のサービスをBlue/Greenにデプロイして、Code Pipelineからコンテナイメージやアプリを変更させ、CodeBuildでdocker buildし、CodeDeployにてBlueからGreenに切り替えさせる手順を紹介する。
AWS Code pipelineのFargate Blue/Greenのデプロイは、プレースホルダを利用するとCodeDeploy::BlueGreen フックを利用する2方法があるが、今回はプレースホルダを利用して実施する。CodeDeploy::BlueGreen フックを利用して実現するやり方は、前回の記事をご参照ください。

両方法の主な違いは、プレースホルダではCodeDeployの機能により、バージョン管理ツール (CodeCommitレポジトリ) に格納された taskdef.json のイメージURI と appspec.ymlのタスク定義のプレースホルダのみを更新してECS Blue/Greenデプロイを実行する。それ以外部分の更新は、Blue/Greenデプロイの管理外となる。

これに対して、CodeDeploy::BlueGreen フック方法ではTransform というAWSのマクロ機能を利用しECS の Blue/Green の環境を作成する。フック全体の広範な変換 (Transform) まで、テンプレートに対してカスタム処理を実行できるようになる。プレースホルダとは異なり、イメージURI以外のタスク定義の更新やサービスの設定更新も、Blue/Greenデプロイの管理範囲となる。

マクロの詳細は、下記のAWS CloudFormation マクロを使用例 - AWS CloudFormation をご参照下さい。

環境用意

以下のリソースは事前に用意して置く。
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile

実施手順

  1. ELB(ロードバランサー)、リスナー、ターゲットグループを作成して置く。ELBのリスナーとたーべっとグループは下記のように設定する。
    80/TCPのリスナー
    80/TCPのリスナーで利用されるターゲットグループ、ターゲット80|
    8080/TCPのリスナー
    8080/TCPのリスナーで利用されるターゲットグループ、ターゲット同じく80
  2. ECS環境を用意します。具体的は、ECSクラスタ、ECSサービス、ECSタスクを作成して置く。また、ECSサービスを作成する際、必ず“Blue/green deployment(powered by AWS CodeDeploy)”を選択する。

    phld-service-define.JPG

  3. ECS サービスを作成する際、作成されたCodeDeploy applicationとdeployment group名をメモしておく。このapplicationとdeployment groupを次にCodePipeline定義する時に必要である。applicationとdeployment groupを作れなかった、若しくは既存のサービスに追加するタスクまたはその他の原因でapplicationとdeployment groupがない場合は、“JSONからapplicationとdeployment groupを作成する”章をご参照ください。

    phld-service-codedeploy-app-grp.JPG

  4. ECS タスク定義から、作成されたタスクのJSONタブの全て内容を、taskdef.jsonファイルとしてローカルに保存する。また、保存する前、行"image": "xxxxxxxxxxxx.dkr.ecr.xxxxxx.amazonaws.com/xxxxxx:latest",を"image": ""に変更する。
    phld-task-json.JPG
    phld-task-image.JPG

  5. 作成されたtaskdef.jsonファイルをCodecommitレジストリにアップロードする。

  6. appspec.yml、buildspec.ymlファイルをCodecommitレジストリにアップロードする。appspec.yml のContainName、と buildspec.ymlファイルを実提供するサービスアプリにより変更してください。

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

buildspec.yml
version: 0.2
phases:
  pre_build:
    commands:
      - aws ecr get-login-password --region xxxxxx | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.xxxxxx.amazonaws.com
      - REPOSITORY_URI=xxxxxxxxxxxx.dkr.ecr.xxxxxx.amazonaws.com/xxxxxx
      - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)

  build:
    commands:
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG

  post_build:
    commands:
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
    files: imageDetail.json

検証方法

1.現用系(ブルー)とアプリ
Ubuntu18コンテナにイメージファイル。コンテナが実行されたらWebBrowserに“Hello World!”を表示される。

FROM ubuntu:18.04

# Install dependencies
RUN apt-get update && \
 apt-get -y install apache2

# Install apache and write hello world message
RUN echo 'Hello World!' > /var/www/html/index.html

# Configure apache
RUN echo '. /etc/apache2/envvars' > /root/run_apache.sh && \
 echo 'mkdir -p /var/run/apache2' >> /root/run_apache.sh && \
 echo 'mkdir -p /var/lock/apache2' >> /root/run_apache.sh && \ 
 echo '/usr/sbin/apache2 -D FOREGROUND' >> /root/run_apache.sh && \ 
 chmod 755 /root/run_apache.sh

EXPOSE 80

CMD /root/run_apache.sh

開発環境(グリーン)系:
Nginxコンテナにイメージファイル。コンテナが実行されたらWebBrowserに“Welcome to nginx!...”を表示される。

  1. Codepipelineスタックを作成する(pholder-pipeline.yml)

Fargate で Ununtu18 コンテナを動かして、ALB経由でHTTP 80番ポートでアクセスするの“HelloWorld!”メッセージを作成する。

pholder-pipeline.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with PlaceHolder
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev

  ECRName:
    Type: String
    Default: xxxxxxxx
  ContainerName:
    Type: String
    Default: xxxxxxxxxxxx
  CodeCommitRepositoryName:
    Type: String
    Default: xxxxxxxx
  CodeDeployAppName:
    Type: String
    Default: AppECS-xxxxxx-xxxxxx-service1
  CodeDeployGrpName:
    Type: String
    Default: DgpECS-xxxxxx-xxxxxx-service1

  NameTagPrefix:
    Type: String
    Default: xxxx
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.


# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # IAM Roles
  # ------------------------------------------------------------#
  # CodeWatchEventを実行できるIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub  cwe-${Service}-${Env}-xxxx-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CloudWatchEventsPipelineExecution
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: codepipeline:StartPipelineExecution
                Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}

  # CodeBuildに適用するIAMRole
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub codebuild-${Service}-${Env}-xxxx-role
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleCodeBuildAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                Resource: '*'
              - Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:GetRepositoryPolicy
                  - ecr:DescribeRepositories
                  - ecr:ListImages
                  - ecr:DescribeImages
                  - ecr:BatchGetImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                  - ecr:PutImage
                Resource: '*'

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub codepipeline-${Service}-${Env}-xxxx-role
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SamplePipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource: '*'
                Effect: Allow
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - codedeploy:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - elasticbeanstalk:*
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                  - rds:*
                  - sqs:*
                  - ecs:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource: '*'
                Effect: Allow

  # CodeDeployに適用するIAMRole
  CodeDeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'codedeploy.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      RoleName: !Sub codedeploy-${Service}-${Env}-xxxx-role

  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  # CloudWatchEventの実行ルール
  AmazonCloudWatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - CodeCommit Repository State Change
        resources:
          - Fn::Join:
              - ''
              - - 'arn:aws:codecommit:'
                - !Ref 'AWS::Region'
                - ':'
                - !Ref 'AWS::AccountId'
                - ':'
                - !Ref CodeCommitRepositoryName
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - master
      Targets:
        - Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
          RoleArn: !GetAtt CloudwatchEventRole.Arn
          Id: codepipeline-AppPipeline

  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      ServiceRole: !Ref CodeBuildServiceRole
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - $(aws ecr get-login --no-include-email)
                - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
            build:
              commands:
                - echo Build started on `date`
                - echo Building the Docker image...
                - docker build -t $REPOSITORY_URI:$IMAGE_TAG .
                - docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images...
                - docker push $REPOSITORY_URI:$IMAGE_TAG
                - echo Writing imageDetail json...
                - echo "{\"name\":\"${ContainerName}\",\"ImageURI\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}" > imageDetail.json
          artifacts:
            files: imageDetail.json
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
          - Name: ContainerName
            Value: !Ref ContainerName
          - Name: DOCKER_BUILDKIT
            Value: '1'
      Name: !Sub ${Service}-${Env}-xxxx-project1
#      Name: !Ref AWS::StackName

  # ------------------------------------------------------------#
  # CodePipeline 
  # ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub ${ServiceName}-xxxx-pipeline1
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: '1'
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepositoryName
                PollForSourceChanges: false
                BranchName: master
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: App
              OutputArtifacts:
                - Name: BuildOutput

        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CodeDeployToECS
              Configuration:
                AppSpecTemplateArtifact: App
                AppSpecTemplatePath: appspec.yml
                TaskDefinitionTemplateArtifact: App
                TaskDefinitionTemplatePath: taskdef.json
                ApplicationName: !Ref CodeDeployAppName
                DeploymentGroupName: !Ref CodeDeployGrpName
                Image1ArtifactName: BuildOutput
                Image1ContainerName: IMAGE1_NAME
              RunOrder: 1
              InputArtifacts:
                - Name: App
                - Name: BuildOutput
              Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelineDev:
    Description: Dev xxxx Pipeline
    Value: !Ref Pipeline
    Export:
      Name: xxxxPipeline

このテンプレートには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。

CloudFormationスタックを作成するコマンドは下記になる(phld.sh)。

phld.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi

CFN_TEMPLATE=$2
CFN_STACK_NAME=xxxx-pipeline
# テンプレートの実⾏
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM --parameter-overrides `cat parameters.phld`

./phld.sh deploy pholder-pipeline.yml

スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。

phld-blue-app.JPG

2. 開発系(グリーン)に切り替え
Dockfileerファイルを変更して、グリーン系のNginxコンテナを作り直す。CloudWatchでDocker変更を検知しデプロイが走る。その後、開発系のコンテナに切り替えます。
コンテナが実行されたらWebBrowserに“Welcome to nginx!...”を表示される。

CodeDeploy の グリーン系への切り替え過程は以下になる。

phld-deploy-switchover.JPG

3.開発系(グリーン)へのアクセス
開発系のコンテナに切り替え後、再び。ALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。切り替え途中でもALBへのアクセスは一定の確率(例90%)で開発系にルートされたことも確認できる。

phld-green-app.JPG

4.5分間のCodeDeploy TerminationWaitTimeInMinutes待ってから、ブルー系コンテナが停止され、コンテナにアクセスすると返事がありません。

(オプション)JSONからapplicationとdeployment groupを作成する

若しECS サービスが既に作成されて、CodeDeploy用のapplicationとdeployment groupが分からない場合は、aws cliからapplicationとdeployment groupを作れる。また、現在のCloudFormation(1.4.0バージョン)では、CodeDeployのapplicationも作れるが、deployment groupは作れません。

ECS Blue/Greenデプロイ用のアプリケーションを作成する。

$ aws deploy create-application \
--application-name AppECS-xxxxxx-service1 \
--compute-platform ECS

若しくは、CloudFormationからDeploy applicationを作る。

# Create Code deploy application
  CodeDeployApplication:
    Type: AWS::CodeDeploy::Application
    Properties:
      ApplicationName: !Ref CodeDeployAppName
      ComputePlatform: ECS

上記で作ったアプリケーションに紐付けるデプロイグループを作成する。JSONフォーマットの定義ファイルから作成する。

定義ファイルdeploy-grp-create.json:

deploy-grp-create.json
{
  "applicationName": "xxxxxxxx-app1",
  "deploymentGroupName": "xxxxxxxx-grp1",
  "deploymentConfigName": "CodeDeployDefault.ECSAllAtOnce",
  "serviceRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/xxxxxxxx-xxxxxxxx-role",
  "alarmConfiguration": {
    "enabled": false,
    "ignorePollAlarmFailure": false,
    "alarms": []
  },
  "autoRollbackConfiguration": {
    "enabled": true,
    "events": [
      "DEPLOYMENT_STOP_ON_ALARM",
      "DEPLOYMENT_FAILURE",
      "DEPLOYMENT_STOP_ON_REQUEST"
    ]
  },
  "deploymentStyle": {
    "deploymentType": "BLUE_GREEN",
    "deploymentOption": "WITH_TRAFFIC_CONTROL"
  },
  "blueGreenDeploymentConfiguration": {
    "terminateBlueInstancesOnDeploymentSuccess": {
      "action": "TERMINATE",
      "terminationWaitTimeInMinutes": 5
    },
    "deploymentReadyOption": {
      "actionOnTimeout": "CONTINUE_DEPLOYMENT",
      "waitTimeInMinutes": 0
    }
  },
  "loadBalancerInfo": {
    "targetGroupPairInfoList": [
      {
        "targetGroups": [
          {
            "name": "xxx-xxx-tg"
          },
          {
            "name": "xxx-xxx-tg2"
          }
        ],
        "prodTrafficRoute": {
          "listenerArns": [
            "arn:aws:elasticloadbalancing:xxxxxx:xxxxxxxxxxxx:listener/net/xxx-xxx-nlb/0b7c7c53ede509d4/0a8e14e46fa40eae"
          ]
        },
        "testTrafficRoute": {
          "listenerArns": [
            "arn:aws:elasticloadbalancing:xxxxxx:xxxxxxxxxxxx:listener/net/xxx-xxx-nlb/0b7c7c53ede509d4/9ebf467664c0f617"
          ]
        }
      }
    ]
  },
  "ecsServices": [
    {
      "serviceName": "xxx-xxx-service",
      "clusterName": "xxx-xxx-fargate"
    }
  ],
  "tags": [
    {
      "Key": "tag-key",
      "Value": "tag-value"
    }
  ]
}

--cli-input-json をオプション利用して定義ファイルを読み込み、デプロイグループを作成する。

$ create-grp.sh

#!/bin/bash
aws deploy create-deployment-group \
--application-name AppECS-xxxxxx \
--deployment-group-name DrpECS-xxxxxx \
--cli-input-json file://deploy-grp-create.json

所感

今回、CodeDeployのプレースホルダ方法を使って、簡単なWebアプリを作るによりCloudFormationのECS Blue/Greenデプロイを構築と検証した。ECS FargateのBule/Green CodeDeployが動くことを確認できました。CodeDeploy::BlueGreenフックの実現方法により、CodeDeployのプレースホルダ方法は、事前にECSサービスやALB、タスク、コンテナ等を作成しており、又は既存のECSサービスとの連携が可能で、実際の複雑な商用システムでは対応できると思います。

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