こんにちは。READYFORでSREとして働いている水本です。これはREADYFOR Advent Calendar 2020の3日目の記事になります。
現在READYFORでは、EC2利用からECS Fargate利用への移行のための検証・開発を進めています。その検証の中でBlue/GreenデプロイのGitHub Actionを用いての自動化や、トラフィックの切り替え前のテストのやり方について調べたので解説してみます。これから導入を考える人の助けになればと思います。
前提
- Terraform でAWSリソースを管理します
- GitHub ActionsでのECSのデプロイでは aws-actions/amazon-ecs-deploy-task-definitionを利用します
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: 本稼働トラフィックを置き換えタスクセットに再ルーティング中
で失敗し、ロールバックのデプロイが実行されたことを確認できました
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 さんの担当です。お楽しみに。