CodeDeployを利用したECSのデプロイをCLIから試す

2020.04.15

ECS ServiceへのBlue Greenデプロイをする場合にデプロイメントコントローラとしてECSではなく, CodeDeployを利用することが多いかと思います. 以前CodePipelineを利用したBlue Greenデプロイは試して実装しましたが, 今回はDocker ImageをECR にpushするところから, CodeDeployでdeploymentを作成するところまで全て手動で実行してみたいと思います.

今回は内部動作の理解とCIツールで何をすべきかの確認を目標として行っています. 最終的にはCIツールからCodeDeploy経由でデプロイできれば一番良いとは思っています.

概要

実装する内容は図の通りです.

この図の内容に沿ってどのような流れでデプロイをするかについて説明していきます.

  1. ECRリポジトリにDocker Imageをpushします. Docker Image TagとしてGitのCommit Hashを利用することでイメージを一意に特定できるようにします.
  2. ECS Task Definition を更新します. AWS CLIのオプションで全てパラメータを渡すのは大変なので手元に「taskdef.json」というファイルを作成し, CLI Skeltonとして利用します. またTask DefinitionではDocker ImageのURIを指定する必要があるので, 1の手順で作成されたURIを元にファイルの内容も合わせて更新します.
  3. S3バケットの「appspec.yaml」を更新します. CodeDeployでDeploymentを作成する際にS3バケットにあるAppspecを指定して作成します. なので事前に用意したS3バケットに2で作成したTask Definitionの新しいリビジョンを指定した「appspec.yaml」を保存します.
  4. CodeDeployのCreateDeploymentを実行します. 今までの内容を元にDeploymentを作成し, デプロイを実行します.

実装する内容については以上になります. 次は実際にTerraformを利用してリソースを作成していきます.

実装する部分について

下記リソースは今回Terraformで作成します.

  • ECS Service
  • S3 Bucket (AppSpecの保存)
  • CodeDeploy Application
  • CodeDeploy Deployment Group
  • IAM Role (CodeDeploy)

下記リソースはCodeDeployを利用したデプロイに必要ですが, 事前に準備しているものとします.

  • Security Group
  • ECS Cluster
  • ECS Task Definition
  • Application Load Balancer
  • Application Load Balancer Target Group (BlueとGreenで2つ)
  • Application Load Balancer Listener

Terraform を実装してみる

先ほど記載したリソースについてTerraformで書いていきます. Terraformは「v0.12.19」でAWS Providerは「v2.57.0」を利用しました. まずはCodeDeploy以外のリソースを作成していきます. なのでECS ServiceとS3 Bucketのみ書いていきます. ECS Serviceを作成する際に「deployment_controller」の設定で「type = "CODE_DEPLOY"」の指定のみ忘れずに行ってください. それ以外については適宜プレースホルダーを置換すると良いです.

main.tf

terraform {
  required_version = ">= 0.12"
}

provider "aws" {
  region = "ap-northeast-1"
}

/**
  * S3 Bucket for CodeDeploy
  */
resource "aws_s3_bucket" "bucket" {
  acl           = "private"
  force_destroy = true
}

/**
  * ECS Service
  */
resource "aws_ecs_service" "go_sv" {
  name            = "go_sv"
  cluster         = "CLUSTER_NAME"
  task_definition = "go-server"
  desired_count   = 0

  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 1
    base              = 1
  }
  deployment_controller {
    type = "CODE_DEPLOY"
  }
  load_balancer {
    target_group_arn = "LOAD_BALANCER_TARGET_GROUP_ARN"
    container_name   = "go-server"
    container_port   = 8080
  }
  network_configuration {
    assign_public_ip = true
    security_groups = [
      "sg-xxxxxxxxxxxxxxxxx"
    ]
    subnets = [
      "subnet-xxxxxxxxxxxxxxxxx",
      "subnet-yyyyyyyyyyyyyyyyy"
    ]
  }
}

次にCodeDeploy Application, Deployment GroupとIAM Roleを作成して準備が完了します. 「main.tf」に追記していく形で書いていきます. 今回はBlue Greenデプロイなので「deployment_config_name」に「CodeDeployDefault.ECSAllAtOnce」を指定して, 「deployment_style」の「deployment_type」で「BLUE_GREEN」を指定します. ECSがCodeDeployを利用したLinearデプロイとCanaryデプロイもサポートしたので, この辺りのパラメーターを変更することでBlue Greenデプロイ以外も実装が可能です. 先ほどと同様にプレースホルダーを置き換えたら準備完了です.

main.tf

/**
  * IAM Role for CodeDeploy
  */
data "aws_iam_policy_document" "codedeploy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["codedeploy.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "codedeploy" {
  name               = "ecs-pipeline-deploy"
  assume_role_policy = data.aws_iam_policy_document.codedeploy.json
}

resource "aws_iam_role_policy_attachment" "codedeploy" {
  role       = aws_iam_role.codedeploy.id
  policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}

resource "aws_codedeploy_app" "main" {
  compute_platform = "ECS"
  name             = "go-sv"
}

resource "aws_codedeploy_deployment_group" "main" {
  deployment_group_name  = "go-sv"
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
  app_name               = aws_codedeploy_app.main.name
  service_role_arn       = aws_iam_role.codedeploy.arn

  auto_rollback_configuration {
    enabled = true
    events = [
      "DEPLOYMENT_FAILURE"
    ]
  }
  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }
    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = 1
    }
  }
  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  ecs_service {
    cluster_name = "CLUSTER NAME"
    service_name = "go_sv"
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [
          "YOUR ALB LISTENER ARN"
        ]
      }
      target_group {
        name = "BLUE TARGET GROUP"
      }
      target_group {
        name = "GREEN TARGET GROUP"
      }
    }
  }
}

ここまで実装が完了したら, terraform apply でリソースを作成して準備は完了です.

CLI からデプロイを実行する

Terraformでリソースを作成して事前準備が完了したので実際にCLIからデプロイを行っていきます. 私の環境で試した内容なので適宜自分の環境に読み替えてください.

1. Docker ImageをビルドしてECRにpushする

Golangで書いたhttp ServerをビルドしてECRリポジトリにpushします. 今回はGit Commit Hashをレスポンスとして返すようにコードを記載しました. 参考までに今回利用したコードとDockerfileを掲載します.

main.go

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hi, This is b29c7aa!\n")
	})
	http.ListenAndServe(":8080", nil)
}

Dockerfile

FROM golang:latest

RUN mkdir /app
COPY . /app
WORKDIR /app
RUN go build -o main main.go

ENTRYPOINT ["/app/main"]
EXPOSE 8080

この内容を元にDocker ImageをビルドしてECRリポジトリにpushします. Dockerへのログインについては今まで「aws ecr get-login --no-include-email」でやっていましたが, 「get-login-password」というAPIが新しくできてかつ今後はこちらを推奨しているようなのでこちらを利用します.

# Build a image
$ docker build -t 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/[YOUR IMAGE NAME]:b29c7aa .

# Obtain password and login to Docker
$ aws ecr get-login-password | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/[YOUR IMAGE NAME]

# Push this image
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/[YOUR IMAGE NAME]:b29c7aa

2. Task Definitionを登録する

Docker Imageをpushしたことで新しいURIができたのでTask Definitionを登録して, 新しいリビジョンを作成します. 「containerDefinitions」にある「image」部分を先ほど作成したURIに書き換えます.

taskdef.json

{
  "family": "go-server",
  "networkMode": "awsvpc",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "cpu": "256",
  "memory": "512",
  "requiresCompatibilities": ["FARGATE"],
  "containerDefinitions": [
    {
      "name": "go-server",
      "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-sv:b29c7aa",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 8080,
          "hostPort": 8080
        }
      ]
    }
  ]
}

「taskdef.json」の更新が終わったらTask Definitionを登録します.

$ aws ecs register-task-definition \
  --cli-input-json file://path/to/taskdef.json

3. appspec.yaml を更新する

Terraformで作成したS3 バケットに「appspec.yaml」を格納します. 「appspec.yaml」にはTask Definitionを指定する箇所があるので, 先ほど作成したTask Definitionのリビジョンを元に「appspec.yaml」を更新します.

appspec.yaml

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: 'arn:aws:ecs:ap-northeast-1:123456789012:task-definition/go-server:[replace here to your revison]'
        LoadBalancerInfo:
          ContainerName: 'go-server'
          ContainerPort: 8080

「appspec.yaml」を更新したらS3バケットにオブジェクトをコピーします.

$ aws s3 cp /path/to/appspec.yaml s3://YOUR_BUCKET_NAME/appspec.yaml

4. Deploymentを作成してデプロイを実行する

ここまで準備ができたらあとはCodeDeployからデプロイを実行するだけです. Task Definitionを更新した時と同様に, CLI Skeltonを利用してDeploymentを作成します.

deployment.json

{
  "applicationName": "go-sv",
  "deploymentGroupName": "go-sv",
  "revision": {
    "revisionType": "S3",
    "s3Location": {
      "bucket": "YOUR_BUCKET_NAME",
      "key": "appspec.yaml",
      "bundleType": "YAML"
    }
  }
}

この内容を元にdeploymentを作成します.

$ aws deploy create-deployment \
  --cli-input-json file://ecs/deployment.json

デプロイ前はこのようなレスポンスが返っている状態でした.

CLIからコマンドを実行したためDeploymentが作成されました.

問題なくデプロイが完了しました.

ALBからのレスポンスも想定通り変化しています.

さいごに

CI/CDツールで自動で行っている内容を手動で行うことで少しでも内部動作の助けになればと思います. またこの内容を元に必要に応じてデプロイスクリプトを組んでいただければ幸いです.

参考資料