25
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

READYFORAdvent Calendar 2020

Day 3

GitHub Actions を利用しての ECSのBlue/Greenデプロイとトラフィック切り替え前テストを実践

Last updated at Posted at 2020-12-03

READYFORアドベントカレンダー20201203.png

こんにちは。READYFORでSREとして働いている水本です。これはREADYFOR Advent Calendar 2020の3日目の記事になります。

現在READYFORでは、EC2利用からECS Fargate利用への移行のための検証・開発を進めています。その検証の中でBlue/GreenデプロイのGitHub Actionを用いての自動化や、トラフィックの切り替え前のテストのやり方について調べたので解説してみます。これから導入を考える人の助けになればと思います。

前提

ECSのBlue/Greenデプロイ

ECSのBlue/Green デプロイは、CodeDeploy 使用したBlue/Green デプロイが提供されていますのでそちらの方法を利用します。

Terraform

※ 動作検証に利用したTerraformリソース全体はmizu0/terraformに置いています。

Blue/Greenデプロイの利用にあたり、いくつか標準のローリングデプロイの設定から変更する必要がありますので、変更が必要な部分だけ説明していきます。

ALB

1つのALBと

resource "aws_lb" "ecs_bluegreen" {
  name                       = "ecs-bluegreen-test-lb"
  load_balancer_type         = "application"
...
}

2つのターゲットグループを用意します。

resource "aws_lb_target_group" "ecs_blue" {
  name                 = "ecs-blue-tg"
...
}

resource "aws_lb_target_group" "ecs_green" {
  name                 = "ecs-green-tg"
...
}

リスナーは、作成したターゲットグループの片方を設定(どちらでもOKだが今回はblue)します。
向き先がCodedeployによって切り替わってもTerraformの差分が出ないように、ignore_changesを設定します。

resource "aws_lb_listener" "ecs_bluegreen" {
  load_balancer_arn = aws_lb.ecs_bluegreen.arn
  port              = "80"
  protocol          = "HTTP"
...
}

resource "aws_lb_listener_rule" "ecs_bluegreen" {
  listener_arn = aws_lb_listener.ecs_bluegreen.arn
  priority     = 100
  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ecs_blue.arn
  }
  condition {
    path_pattern {
      values = ["/*"]
    }
  }

  lifecycle {
    # actionの状態は無視
    ignore_changes = [action]
  }
}

ECS Service

デプロイの設定をCODE_DEPLOYにし、ALBと同様に作成したターゲットグループの片方をload_balancerに設定します。
また向き先がCodedeployによって切り替わってもTerraformの差分が出ないようにignore_changesを設定します。


resource "aws_ecs_service" "test_service" {
...
  load_balancer {
    target_group_arn = aws_lb_target_group.ecs_blue.arn
    container_name   = "server"
    container_port   = 8000
  }

  deployment_controller {
    type = "CODE_DEPLOY"
  }

  lifecycle {
    ignore_changes = [task_definition, load_balancer]
  }
}

Codedeploy

Codedeployは以下のような設定なります。
load_balancer_info に作成したリスナーとデプロイによって切り替える2つのターゲットグループ(ecs_blue/ecs_green)を指定します。

resource "aws_codedeploy_app" "ecs" {
  compute_platform = "ECS"
  name             = "ecs"
}

resource "aws_codedeploy_deployment_group" "ecs_bluegreen" {
  app_name               = aws_codedeploy_app.ecs.name
  deployment_group_name  = "ecs_bluegreen"

  service_role_arn = aws_iam_role.ecs_codedeploy.arn
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" #すべてのトラフィックを同時に更新済み Amazon ECS コンテナに移行します。

...

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.ecs_bluegreen.arn]
      }

      target_group {
        name = aws_lb_target_group.ecs_blue.name
      }

      target_group {
        name = aws_lb_target_group.ecs_green.name
      }
    }
  }
}

GitHub Actions

※ 動作検証に利用したymlはこちらに置いています。

aws-actions/amazon-ecs-deploy-task-definition の設定に下記のように、Terraform で作成したECSやCodedeployのリソースを指定します。

...
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: test_service
          cluster: test
          codedeploy-appspec: aws/appspec.yaml
          codedeploy-application: ecs
          codedeploy-deployment-group: ecs_bluegreen

参照: https://github.com/aws-actions/amazon-ecs-deploy-task-definition#aws-codedeploy-support

 
appspec.yaml リポジトリに配置する必要がありますが、下記のように最低限の設定のみで構いません。
TaskDefinition部分はGitHub Actions実行時に自動で設定してくれます。

version: 1

Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "Placeholder: GitHub Actions will fill this in"
        LoadBalancerInfo:
          ContainerName: "server"
          ContainerPort: 8000
        PlatformVersion: "LATEST"

  
これらの設定を追加すると、GitHub Actions の実行ログに以下のように出力され、Codedeployを利用したBlue/Greenデプロイの自動化が実現できました。

Deployment started. Watch this deployment's progress in the AWS CodeDeploy console: 
https://console.aws.amazon.com/codesuite/codedeploy/deployments/d-INJJMI4H7?region=ap-northeast-1

トラフィック切り替え前のテスト

ここまでで、GitHubにPushしたら、Blue/Greenデプロイでサービスが更新されるようになりました。
次はBlueとGreenのトラフィックの切り替え前に新しく起動した側のターゲットグループにアクセスし、正常に動作しているかの自動テストが、Codedeployでは可能なのでその設定をしてみます。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/tutorial-ecs-deployment-with-hooks.html

テストリスナー

サービスで利用するポート以外のポートをテストに利用します。

ALB

テスト用のリスナーを作成し、Blue/Green用のALBに設定します。


resource "aws_lb_listener" "ecs_bluegreen_test" {
  load_balancer_arn = aws_lb.ecs_bluegreen.arn
  port              = "8080"
  protocol          = "HTTP"
...
}

リスナーは本番と同様、作成したターゲットグループの片方を設定(どちらでもOKだが今回はgreen)します。
向き先がCodedeployによって切り替わってもTerraformの差分が出ないように、ignore_changesを設定します。

resource "aws_lb_listener_rule" "ecs_bluegreen_test" {
  listener_arn = aws_lb_listener.ecs_bluegreen_test.arn
  priority     = 100
  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ecs_green.arn
  }
  condition {
    path_pattern {
      values = ["/*"]
    }
  }

  lifecycle {
    # actionの状態は無視
    ignore_changes = [
      action
    ]
  }
}

ALBのアクセス制限

テストリスナーは、名前の通りテスト用なのでInternetに公開したくはないため、VPC内とNAT GATEWAYからのアクセスのみ許可します。

※テストはAWS Lambdaで実行するため、Lambda=>テスト用のポートは疎通できる必要があります。しかし、どう設定していいかわからなかったため、以下の記事を参考にLambdaをVPC内のprivateサブネットに作成することで、NATのIPからLambdaのアクセスが来るようにし対応しました。
https://www.joyzo.co.jp/blog/2325

resource "aws_security_group" "ecs_alb_sg" {
  name        = "ecs-alb-sg"
  description = "Allow http inbound traffic"
  vpc_id      = data.terraform_remote_state.vpc.outputs.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = [data.terraform_remote_state.vpc.outputs.vpc_cidr]
  }

  # codedeploy の AfterAllowTestTraffic Hook用
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["${data.terraform_remote_state.vpc.outputs.nat_gateway_ip}/32"]
  }
...

Codedeploy

Codedeploy の設定は、test_traffic_routeの設定に作成したテストリスナーのリソースを追加するだけです。


resource "aws_codedeploy_deployment_group" "ecs_bluegreen" {
  app_name               = aws_codedeploy_app.ecs.name
  deployment_group_name  = "ecs_bluegreen"
...

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.ecs_bluegreen.arn]
      }

      test_traffic_route {
        listener_arns = [aws_lb_listener.ecs_bluegreen_test.arn]
      }

      target_group {
        name = aws_lb_target_group.ecs_blue.name
      }

      target_group {
        name = aws_lb_target_group.ecs_green.name
      }
    }
  }
}

AfterAllowTestTraffic フック

テストリスナーが新しく起動した側のターゲットグループに向いたタイミングで、テストを実行したいため、AfterAllowTestTrafficを利用します。

AfterAllowTestTraffic – テストリスナーが置き換えタスクセットにトラフィックを提供した後、タスクを実行するために使用します。この時点でのフック関数の結果により、ロールバックをトリガーできます。

テスト用のLambda

今回は単純にhttpリクエストを実行して、statusとbodyをチェックするというコードです。
実際には、APIであれば期待している値が返ってくるかというチェックしたり、UIであればpuppeteer等を利用してメインの導線だけでもちゃんと動いているかをチェックすればいいかと思っています。

validationTestResult の値が "Succeeded"の状態で、 PutLifecycleEventHookExecutionStatusを実行すると成功した判断され、Codedeployが次のステップに進みます。

func handler(event codedeploy.PutLifecycleEventHookExecutionStatusInput) error {
	deploymentID := event.DeploymentId
	lifecycleEventHookExecutionID := event.LifecycleEventHookExecutionId
	validationTestResult := "Failed"
	sess := session.Must(session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Region: aws.String("ap-northeast-1"),
		},
	}))
	cd := codedeploy.New(sess)

	resp, _ := http.Get("<ALBのドメイン名>:8080/hello-world")

	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)

	if resp.StatusCode == 200 && string(body) == "Hello World!" {
		validationTestResult = "Succeeded"
		fmt.Println("Succeeded")
	} else {
		fmt.Println("Failed")
	}

	output, _ := cd.PutLifecycleEventHookExecutionStatus(&codedeploy.PutLifecycleEventHookExecutionStatusInput{
		DeploymentId: deploymentID,
		LifecycleEventHookExecutionId:lifecycleEventHookExecutionID,
		Status: aws.String(validationTestResult),
	})
	fmt.Println(output)

	return nil
}

 

テスト用のLambdaなので、更新はアプリケーションのデプロイと同じタイミングでできるのが望ましいです。
そのため同じリポジトリにコードを配置して、GitHub Actions のECSデプロイの前にテスト用Lambdaも自動更新するようにしました。
(テストコードに変更があったときだけ、更新されるようにしたほうが良いと思います)

      - name: Update lambda
        run: |
          GOOS=linux go build -o test verifytest/main.go
          zip test.zip test
          aws lambda update-function-code --function-name ecs-bluegreen-verifytest --zip-file fileb://test.zip

apppspec.yaml

appspec.yamlの AfterAllowTestTrafficに作成したLambda関数名を指定するだけです。

...

Hooks:
  - AfterAllowTestTraffic: "ecs-bluegreen-verifytest"

テスト失敗時のロールバックの動作確認

まずは平常時は普通に成功することを確認

AfterAllowTestTraffic フックも成功している

次に、アプリケーション側のコードをテストに失敗するように変更して、GitHubにPush。
すると、ステップ 3: 本稼働トラフィックを置き換えタスクセットに再ルーティング中で失敗し、ロールバックのデプロイが実行されたことを確認できました:tada:

AfterAllowTestTraffic フックで失敗していることも確認できました。

まとめ

GitHub Actions の aws-actions/amazon-ecs-deploy-task-definition を利用してのBlue/Greenデプロイの自動化や、トラフィック切り替え前のテスト実施は設定をいくつか追加するだけで実現できることがわかりました。

Codedeployには、今回利用した、すべてのトラフィックを同時に移行するCodeDeployDefault.ECSAllAtOnce以外にも、最初に10%移行して残りの90%は5分後に切り替えるCodeDeployDefault.ECSCanary10percent5Minutes など複数のデプロイ設定が用意されているので、シーンに応じて色々な設定を切り替えができるのが良いなと思っています。

明日12月4日は、READYFORサービスのPMである @yohei_eto さんの担当です。お楽しみに。

25
8
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
25
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?