[AWS] ECS: Blue Green ๋ฐฐํฌ ๊ตฌ์ฑ with Terraform
ECS๋ก ์๋ฒ๋ฅผ ๊ตฌ์ฑํ๊ฒ ๋๋ฉด, ๊ธฐ๋ณธ์ ์ผ๋ก๋ rolling update๋ก ๋ฐฐํฌ๋ฅผ ํ๊ฒ ๋๋ค.
์ฌ๊ธฐ์๋ ์กฐ๊ธ ๋ฌธ์ ๊ฐ ์๋ค. ๋ฐฐํฌ๋ฅผ ํ๊ณ ๊ตฌ๋ฒ์ ๋
ธ๋๊ฐ ๋ด๋ ค๊ฐ๊ธฐ ์ ๊น์ง๋ ๊ตฌ๋ฒ์ ๊ณผ ์ ๋ฒ์ ๋
ธ๋๊ฐ ๊ณต์กดํ ์ ์๋ค๋ ๊ฒ์ด๋ค.
์ด๊ฑธ ํด๊ฒฐํ๋ ค๋ฉด blue-green ๋ฐฐํฌ ์ ๋ต์ ์ฌ์ฉํด์ผ ํ๋ค.
CodeDeploy๋ฅผ ์ฌ์ฉํ๊ณ target group์ ๋น๋กฏํ ๋ช๊ฐ์ง ๊ตฌ์ฑ์ ์ถ๊ฐํ๋ฉด ์ด๋ฅผ ๋ฌ์ฑํ ์ ์๋ค.
์ด๋ฐ ๊ตฌ์กฐ๋ค.
AWS์์๋ target group์ ์ด์ฉํด์ blue์ green์ ๊ตฌํํ๋ค.
ํ์๋ฒ์งธ ๋ฐฐํฌํ ๋๋ blue๋ก ๋ฐฐํฌํ๊ณ , ์ง์๋ฒ์งธ ๋ฐฐํฌํ ๋๋ green์ผ๋ก ๋ฐฐํฌํ๋ฉด์, ๋ก๋๋ฐธ๋ฐ์ ๋ฆฌ์ค๋๋ง ๋์ ๋ง๋ target group์ ๊ฐ๋ฆฌํค๊ฒ ์ค์์นญํ๋ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์๋ terraform์ ์ด์ฉํด์ ๊ตฌ์ฑํด๋ณด๊ฒ ๋ค.
์ ์ฒด ์ฝ๋๋ ๋ด ๊ฐ์ธ ๋ ํฌ์งํ ๋ฆฌ์ ์๋ค.
https://github.com/myyrakle/terraform/tree/master/aws/ecs-server/blue-green
๋จผ์ ํ๊ฒ๊ทธ๋ฃน์ 2๊ฐ ์ธํ
ํด์ผ ํ๋ค. rolling update์์๋ ํ๋๋ฉด ์ถฉ๋ถํ์ง๋ง, ์ฌ๊ธฐ์๋ 2๊ฐ๊ฐ ํ์ํ๋ค.
๋์ผํ๊ฒ ๊ตฌ์ฑํด์ฃผ๋ฉด ๋๋ค.
// ๋ก๋๋ฐธ๋ฐ์ฑ์ ์ฌ์ฉํ ๋์ ๊ทธ๋ฃน
resource "aws_lb_target_group" "target_group_blue" {
name = "${local.resource_id}-blue"
port = var.target_group_port
protocol_version = var.target_group_protocol_version
protocol = var.target_group_protocol
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
path = var.healthcheck_uri
interval = var.healthcheck_interval
protocol = var.target_group_protocol
healthy_threshold = 5
unhealthy_threshold = 2
timeout = 20
}
tags = local.tags
}
// Blue Green ๋ฐฐํฌ์ ์ฌ์ฉํ ๋์ ๊ทธ๋ฃน
resource "aws_lb_target_group" "target_group_green" {
name = "${local.resource_id}-green"
port = var.target_group_port
protocol_version = var.target_group_protocol_version
protocol = var.target_group_protocol
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
path = var.healthcheck_uri
interval = var.healthcheck_interval
protocol = var.target_group_protocol
healthy_threshold = 5
unhealthy_threshold = 2
timeout = 20
}
tags = local.tags
}
๊ทธ๋ฆฌ๊ณ ๋ฆฌ์ค๋๋ฅผ 2๊ฐ ์ค์ ํด์ค๋ค.
ํ๋๋ ํ๋ก๋์
๋ฐฐํฌ์ฉ, ํ๋๋ ํ
์คํธ์ฉ์ด๋ค.
// HTTP ๋ฆฌ์ค๋
resource "aws_lb_listener" "http_test_listener" {
load_balancer_arn = aws_lb.loadbalancer.arn
port = "8080"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group_green.arn
}
lifecycle {
ignore_changes = [default_action]
}
tags = local.tags
}
<br>
// HTTPS ๋ฆฌ์ค๋
resource "aws_lb_listener" "https_listener" {
load_balancer_arn = aws_lb.loadbalancer.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group_blue.arn
}
lifecycle {
ignore_changes = [default_action]
}
tags = local.tags
}
SSL์ ์ ์ฉํ์ง ์์ ๊ฑฐ๋ผ๋ฉด 443๋ง 80์ผ๋ก ๋ฐ๊ฟ์ค๋ค.
๊ทธ๋ฆฌ๊ณ lifecycle์ ์ด์ฉํด์ default_action์ ์ฒ์ ์์ฑ ์ดํ์๋ ๊ฑด๋ค์ง ์๋๋ก ํ๋ค.
์ ๊ธฐ์ target_group_arn์ด ๋ฐฐํฌํ ๋๋ง๋ค ๊ณ์ ๋ฐ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
๋ค์์ ๋ฆฌ์ค๋๋ค.
// HTTP ๋ฆฌ์ค๋
// ํธ๋ํฝ ํ
์คํธ
resource "aws_lb_listener" "http_test_listener" {
load_balancer_arn = aws_lb.loadbalancer.arn
port = "8080"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group_green.arn
}
lifecycle {
ignore_changes = [default_action]
}
tags = local.tags
}
<br>
// HTTPS ๋ฆฌ์ค๋
resource "aws_lb_listener" "https_listener" {
load_balancer_arn = aws_lb.loadbalancer.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.target_group_blue.arn
}
lifecycle {
ignore_changes = [default_action]
}
tags = local.tags
}
๋ฆฌ์ค๋์๋ ํธ๋ํฝ ํ
์คํธ์ฉ ๋ฆฌ์ค๋ ํ๋์ ๋ฉ์ธ ๋ฆฌ์ค๋ ํ๋๋ฅผ ๋๋ค.
์ ๊ธฐ์๋ ๋ฉ์ธ ๋ฆฌ์ค๋๋ฅผ https ํ๋๋ง ๋๋๋ฐ, ssl์ ์ฐ์ง ์์ ๊ฑฐ๋ผ๋ฉด http๋ก ๋ฐ๊พธ๋ฉด ๋๋ค.
https์ http๋ฅผ ์ ๋ถ ๋ฆฌ์ค๋ํ๊ณ ์ถ๋ค๋ฉด, ๋ค์๊ณผ ๊ฐ์ด ๋ฆฌ๋ค์ด๋ ํธ์ฉ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ๋ค.
// HTTP ๋ฆฌ์ค๋
resource "aws_lb_listener" "http_listener" {
load_balancer_arn = aws_lb.loadbalancer.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
tags = local.tags
}
๊ทธ๋ฆฌ๊ณ ์๋น์ค์๋ ์ค์ ์ ์ถ๊ฐํด์ค์ผ ํ๋ค.
๋ฐฐํฌ๋ฅผ codedeploy์์ ๊ด๋ฆฌํ ์ ์๋๋ก ํ๋ ๊ตฌ๋ฌธ์ ์ฝ์
ํด์ค๋ค.
deployment_controller {
type = "CODE_DEPLOY"
}

๋ฐฐํฌ ์ ์ฑ ์ ๊ฒฐ์ ํ deploy config๋ฅผ ํ๋ ์ถ๊ฐํด์ฃผ๊ณ
resource "aws_codedeploy_deployment_config" "config_deploy" {
deployment_config_name = local.resource_id
compute_platform = "ECS"
traffic_routing_config {
type = "AllAtOnce" // ํ๋ฒ์ ๊ต์ฒด
}
}
deploy group์ ์ ์ํ๋ค.
์ด๊ฒ blue-green ๋ฐฐํฌ๋ฅผ ์ฃผ๊ดํ ๋
์์ด๋ค.
// blue-green ๋ฐฐํฌ๋ฅผ ์ํ code deploy group
resource "aws_codedeploy_deployment_group" "deployment_group" {
app_name = aws_codedeploy_app.deploy.name
deployment_config_name = aws_codedeploy_deployment_config.config_deploy.deployment_config_name
deployment_group_name = local.resource_id
service_role_arn = aws_iam_role.codedeploy_role.arn
// ์คํจ์ ๋กค๋ฐฑ
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT"
wait_time_in_minutes = 0
}
// green ๋ฐฐํฌ ์ฑ๊ณต์ blue ์ธ์คํด์ค๋ฅผ 5๋ถ ํ์ ์ญ์ ํฉ๋๋ค.
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
termination_wait_time_in_minutes = 5
}
}
deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type = "BLUE_GREEN"
}
ecs_service {
cluster_name = aws_ecs_cluster.ecs_cluster.name
service_name = aws_ecs_service.ecs_service.name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.https_listener.arn]
}
test_traffic_route {
listener_arns = [aws_lb_listener.http_test_listener.arn]
}
target_group {
name = aws_lb_target_group.target_group_blue.name
}
target_group {
name = aws_lb_target_group.target_group_green.name
}
}
}
}
์ ๋ฌ๋ฉด prod_traffic_route์ test_traffic_route์ ๋ค์ด๊ฐ ๋ฆฌ์ค๋๋ฅผ ์๋์ผ๋ก ๊ต์ฒดํด์ค๋ค.
๊ทธ๋ฆฌ๊ณ codepipeline์ deploy ์คํ ์ ์์ ํ๋ค.
stage {
name = "Deploy"
action {
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "CodeDeployToECS"
input_artifacts = ["Build"]
version = "1"
run_order = 1
configuration = {
ApplicationName = aws_codedeploy_app.deploy.name
DeploymentGroupName = aws_codedeploy_deployment_group.deployment_group.deployment_group_name
TaskDefinitionTemplateArtifact = "Build"
TaskDefinitionTemplatePath = "taskdef.json"
AppSpecTemplateArtifact = "Build"
AppSpecTemplatePath = "appspec.json"
}
}
}
provider๋ฅผ ECS์์ CodeDeployToECS๋ก ๋ณ๊ฒฝํ๊ณ , configuration์ ๋ช๊ฐ์ง ์ค์ ์ด ์ถ๊ฐ๋๋ค.
๊ทธ๋์ ์์
์ ์์ appspec์ ์ด์ ๋จ๊ณ์ธ build์์ ๋ฐ์์์ผ ํ๋๋ฐ... ๋ ์ด๊ฑธ ํ๋ก์ ํธ ์ฝ๋์ ๋ฃ๊ณ ์ถ์ง ์์๋ค.
๊ทธ๋์ terraform์ local ๋ณ์๋ก ๋๋ ค๋ฐ๊ณ


codebuild ํ๊ฒฝ๋ณ์์ ๋ฃ์ด์

buildspec์์ emitํด์ฃผ๊ฒ ํ๋ค.
๋ด๊ฐ ์ฌ์ฉํ buildspec ์ ์ฒด ์ฝ๋๋ค.
version: 0.2
phases:
pre_build:
commands:
- echo $TaskDefinition > taskdef.json
- echo $AppSpec > appspec.json
- printf '{"imageURI":"%s"}' "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG" > imageDetail.json
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG .
- docker tag $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CODEBUILD_BUILD_NUMBER
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CODEBUILD_BUILD_NUMBER
artifacts:
files:
- 'appspec.json'
- 'taskdef.json'
- 'imageDetail.json'
secondary-artifacts:
DefinitionArtifact:
files:
- appspec.yaml
- taskdef.json
- imageDetail.json
appspec์ ๊ฒฝ์ฐ์๋ ์ํ๋ค๋ฉด hooks์ ์ฌ์ฉํด์ ๋ ๋ง์ ๊ฒ์ฆ์ ์ํํ ์ ์๋ค.
ํด์, ์ต์ด๋ก ๋ฐฐํฌ๋ฅผ ํด์ ๋ก๋๋ฐธ๋ฐ์์ ๋๋ฉ์ธ๋ ๋ถ์ด๋ฉด

์ด๋ ๊ฒ ์ ๋ฐ ๊ฒ์ด๊ณ

์ถ๊ฐ๋ก ๋ ๋ฐฐํฌ ํธ๋ฆฌ๊ฑฐ๋ฅผ ๊ฑธ๋ฉด

๋์๊ทธ๋ฃน๋ ์๋์ผ๋ก ๋ฐ๋๊ณ


ํ๋ฒ์ ํธ๋ํฝ ์ ํํ๋ฉด์


์ ๋ฐ๋ ๊ฒ์ด๋ค.

์ด๊ฑธ ์ ์์ฉํ๋ฉด carary ๋ฐฐํฌ ๊ฐ์ ๊ฒ๋ ๊ตฌํํ ์ ์๋ค.
์ฐธ์กฐ
https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/create-blue-green.html
https://docs.aws.amazon.com/ko_kr/AWSCloudFormation/latest/UserGuide/blue-green.html
https://faun.pub/aws-ecs-blue-green-deployment-setup-using-terraform-b56bb4f656ea
https://letsmake.cloud/bluegreen-fargate
https://repost.aws/questions/QUGByFL5XzT26SA7lTCz3htQ/questions/QUGByFL5XzT26SA7lTCz3htQ/codepipeline-codedeploy-blue-green-ecs-insufficient-permissions?
https://arnav40.medium.com/deploy-nodejs-on-ecs-fargate-using-terraform-with-support-for-blue-geen-deployment-using-codedeploy-a750c6214153