はじめに
ECSのデプロイやBlue/Greenアップデートがどんなものだったか、記憶が薄れていたので思い出す意味でも初学時に書いたterraformを書き直しました。
全容を確認しながら、リソースの関係をイメージできればなと思います。
概要
アーキテクチャ
publicサブネットに配置されたALBを通してprivateサブネットに配置されているECSタスクにアクセスする一般的な構成です。
少しでも節約するため、NAT Gatewayは1台構成としています。
フォルダ構成
.
├── main.tf
├── variable.tf
├── terraform.tfvars
├── vpc.tf
├── alb.tf
├── ecr.tf
├── ecs.tf
├── codebuild.tf
├── codedeploy.tf
├── codepipeline.tf
|
├── json/
| ├── container_definitions.json
|
├── modules/
| ├── iam_role/
| | ├── main.tf
| ├── security_group/
| ├── main.tf
terraformを見ながらリソースの関係を整理
ECSクラスタ周り
tfコード
main.tf
############################################################################
## terraformブロック
############################################################################
terraform {
# Terraformのバージョン指定
required_version = "~> 1.5.0"
# Terraformのaws用ライブラリのバージョン指定
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.31.0"
}
}
}
############################################################################
## providerブロック
############################################################################
provider "aws" {
# リージョンを指定
region = "ap-northeast-1"
}
- 特に変なことはしていない
- tfstateをS3に保存する設定はいれておくべきか
variable.tf
######################################
## terraform.tfvarsから変数取得
######################################
variable "resource_id_prefix" {}
variable "vpc_cidr_blok" {}
variable "public_subnet_cidr_bloks" {
type = list(string)
}
variable "private_subnet_cidr_bloks" {
type = list(string)
}
variable "my_ip_cidrblock" {
description = "Used to deny access from IPs outside your home"
}
variable "task_family_name" {}
variable "container_name" {}
variable "container_initial_build_path" {
description = "path of container initial build directory"
}
variable "branch_name" {}
variable "repository_name" {}
- 自宅wifi以外のアクセスをブロックするため、自宅wifiのIPを$my_ip_cidrblockで定義しています
- その他ハードコードしたくない部分を変数化しています
terraform.tfvars
resource_id_prefix = "test-ecs"
~(略)~
- variable.tfで定義したvarに値を代入します
vpc.tf
############################################################################
## VPC
############################################################################
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_blok
tags = {
Name = "${var.resource_id_prefix}-vpc"
}
}
############################################################################
## Public route table
############################################################################
# internet gatewayを作成
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.resource_id_prefix}-vpc-igw"
}
}
# public route tableを作成
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.resource_id_prefix}-rtb-public"
}
}
# public route tableの0.0.0.0/0にigwを関連付ける
resource "aws_route" "public" {
route_table_id = aws_route_table.public.id
gateway_id = aws_internet_gateway.igw.id
destination_cidr_block = "0.0.0.0/0"
}
############################################################################
## public subnet 1a
############################################################################
# public subnetを作成
resource "aws_subnet" "public_1a" {
availability_zone = "ap-northeast-1a"
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr_bloks[0]
tags = {
Name = "${var.resource_id_prefix}-subnet-public-1a"
}
}
# public subnet 1aにpublic route tableを関連付ける
resource "aws_route_table_association" "public_1a" {
subnet_id = aws_subnet.public_1a.id
route_table_id = aws_route_table.public.id
}
############################################################################
## public subnet 1c
############################################################################
# public subnetを作成
resource "aws_subnet" "public_1c" {
availability_zone = "ap-northeast-1c"
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr_bloks[1]
tags = {
Name = "${var.resource_id_prefix}-subnet-public-1c"
}
}
# public subnet 1cにpublic route tableを関連付ける
resource "aws_route_table_association" "public_1c" {
subnet_id = aws_subnet.public_1c.id
route_table_id = aws_route_table.public.id
}
############################################################################
## private route table 1a
############################################################################
# NATゲートウェイ用EIP
resource "aws_eip" "nat_gateway_1a" {
tags = {
Name = "${var.resource_id_prefix}-eip-nat-gw-1a"
}
}
# privateサブネット1a用NATゲートウェイ
resource "aws_nat_gateway" "nat_gateway_1a" {
allocation_id = aws_eip.nat_gateway_1a.id
subnet_id = aws_subnet.public_1a.id
tags = {
Name = "${var.resource_id_prefix}-nat-gw-1a"
}
}
# private 1aルートテーブル
resource "aws_route_table" "private_1a" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.resource_id_prefix}-rtb-private-1a"
}
}
# private 1aルートテーブルにNATゲートウェイをアタッチ
resource "aws_route" "private_1a" {
route_table_id = aws_route_table.private_1a.id
nat_gateway_id = aws_nat_gateway.nat_gateway_1a.id
destination_cidr_block = "0.0.0.0/0"
}
############################################################################
## private subnet 1a
############################################################################
# privateサブネット
resource "aws_subnet" "private_1a" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidr_bloks[0]
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = false
tags = {
Name = "${var.resource_id_prefix}-subnet-private-1a"
}
}
# privateサブネットにルートテーブルを登録
resource "aws_route_table_association" "private_1a" {
subnet_id = aws_subnet.private_1a.id
route_table_id = aws_route_table.private_1a.id
}
############################################################################
## private route table 1c
############################################################################
# private 1cルートテーブル
resource "aws_route_table" "private_1c" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.resource_id_prefix}-rtb-private-1c"
}
}
# 課金が怖いので1aのNATを参照
# private 1cルートテーブルにNATゲートウェイをアタッチ
resource "aws_route" "private_1c" {
route_table_id = aws_route_table.private_1c.id
nat_gateway_id = aws_nat_gateway.nat_gateway_1a.id
destination_cidr_block = "0.0.0.0/0"
}
############################################################################
## private subnet 1c
############################################################################
# privateサブネット
resource "aws_subnet" "private_1c" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidr_bloks[1]
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = false
tags = {
Name = "${var.resource_id_prefix}-subnet-private-1c"
}
}
# privateサブネットにルートテーブルを登録
resource "aws_route_table_association" "private_1c" {
subnet_id = aws_subnet.private_1c.id
route_table_id = aws_route_table.private_1c.id
}
- Publicサブネット1a,1cにはALBとNAT Gatewayが配置されます
- Privateサブネット1a,1cにはECSタスクが配置されます
- privateサブネットからpublicなECRへアウトバウンドが発生するので、NATが必要です
- アーキ図でも記載しましたが、aws_route.private_1cが1aのNATを向いています
- それ以外はふつう
modules/security_group/main.tf
variable "name" {}
variable "vpc_id" {}
variable "port" {}
variable "cidr_blocks" {
type = list(string)
}
resource "aws_security_group" "default" {
name = var.name
vpc_id = var.vpc_id
}
resource "aws_security_group_rule" "ingress" {
security_group_id = aws_security_group.default.id
type = "ingress"
from_port = var.port
to_port = var.port
protocol = "tcp"
cidr_blocks = var.cidr_blocks
}
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.default.id
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
output "aws_security_group_id" {
value = aws_security_group.default.id
}
- セキュリティグループを作成するmodule。教本のパクリです
- varの値によってingressにCIDRを指定するかセキュリティグループIDを指定するか分岐する作りにするか迷いました
- 個人的にterraformのファイルはリソースの設定表の側面もあると思っており、なるべくロジックは排除したかったので見送りました
alb.tf
############################################################################
## ALBにアタッチするSG
############################################################################
# http(80)用SG
module "http_sg" {
source = "./modules/security_group"
name = "${var.resource_id_prefix}-http-sg"
vpc_id = aws_vpc.main.id
port = 80
cidr_blocks = [var.my_ip_cidrblock]
}
# http-test(8080)用SG
module "http_test_sg" {
source = "./modules/security_group"
name = "${var.resource_id_prefix}-http-test-sg"
vpc_id = aws_vpc.main.id
port = 8080
cidr_blocks = [var.my_ip_cidrblock]
}
############################################################################
## ログ用バケット
############################################################################
resource "aws_s3_bucket" "alb_log" {
bucket = "${var.resource_id_prefix}-alb-log"
}
# ログの書き込みに使用されるアカウントIDをフェッチ
data "aws_elb_service_account" "alb_log" {}
# ポリシードキュメントを定義
data "aws_iam_policy_document" "alb_log" {
statement {
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["arn:aws:s3:::${aws_s3_bucket.alb_log.id}/*"]
principals {
type = "AWS"
identifiers = ["${data.aws_elb_service_account.alb_log.id}"]
}
}
}
# ALB(AWSアカウント)がS3バケットにログを保存できるようバケットポリシーを設定
resource "aws_s3_bucket_policy" "alb_log" {
bucket = aws_s3_bucket.alb_log.id
policy = data.aws_iam_policy_document.alb_log.json
}
############################################################################
## ALB本体
############################################################################
resource "aws_lb" "alb" {
name = "${var.resource_id_prefix}-alb"
load_balancer_type = "application"
internal = false
idle_timeout = 60
# 削除防止フラグ。テストのためfalse
enable_deletion_protection = false
# ALBの実態?子?インスタンスを配置するサブネットを指定
# ALBの子?インスタンスはここに配置される
# ロードバランシングする親?インスタンスと通信するために?publicサブネットに配置する
subnets = [
aws_subnet.public_1a.id,
aws_subnet.public_1c.id,
]
access_logs {
bucket = aws_s3_bucket.alb_log.id
enabled = true
}
security_groups = [
module.http_sg.aws_security_group_id,
module.http_test_sg.aws_security_group_id
]
}
# ALBのHTTPリスナー(prod)
resource "aws_lb_listener" "http_prod" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "これは「HTTP」です"
status_code = "200"
}
}
}
# ALBのHTTPリスナー(test)
resource "aws_lb_listener" "http_test" {
load_balancer_arn = aws_lb.alb.arn
port = "8080"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "これは「HTTP-test」です"
status_code = "200"
}
}
}
# LBのターゲットグループ(blue)
resource "aws_lb_target_group" "blue" {
name = "${var.resource_id_prefix}-blue-tg"
target_type = "ip"
vpc_id = aws_vpc.main.id
port = 80
protocol = "HTTP"
deregistration_delay = 300
health_check {
path = "/"
healthy_threshold = 5
unhealthy_threshold = 2
timeout = 5
interval = 30
matcher = 200
port = "traffic-port"
protocol = "HTTP"
}
depends_on = [aws_lb.alb]
}
# LBのターゲットグループ(green)
# codeDeployによるBlue/Greenデプロイ時に使用される
resource "aws_lb_target_group" "green" {
name = "${var.resource_id_prefix}-green-tg"
target_type = "ip"
vpc_id = aws_vpc.main.id
port = 80
protocol = "HTTP"
deregistration_delay = 300
health_check {
path = "/"
healthy_threshold = 5
unhealthy_threshold = 2
timeout = 5
interval = 30
matcher = 200
port = "traffic-port"
protocol = "HTTP"
}
depends_on = [aws_lb.alb]
}
# prodリスナーにblueターゲットグループへフォワードさせるためのルールを作成
resource "aws_lb_listener_rule" "prod" {
listener_arn = aws_lb_listener.http_prod.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
lifecycle {
ignore_changes = [
# target_groupはBlue/Greenデプロイで動的に変更されるため
action["target_group_arn"],
]
}
}
# testリスナーにgreenターゲットグループへフォワードさせるためのルールを作成
resource "aws_lb_listener_rule" "test" {
listener_arn = aws_lb_listener.http_test.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
lifecycle {
ignore_changes = [
# target_groupはBlue/Greenデプロイで動的に変更されるため
action["target_group_arn"],
]
}
}
- ECSにリクエストを行うALBを作成します。ALBが参照するのはあくまでターゲットグループで、ターゲットグループへECSタスクを配置するのはECSサービスまたはCodeDeployです
- ALBはターゲットグループに所属するインスタンス(今回はECSタスク)へのロードバランシング、ヘルスチェックを行ってくれます
- ALBの実態?インスタンスはpublicサブネットに配置する必要があった(L67~)。何故なのかわからずモヤっている(ELBのDNSを名前解決するとAWSっぽいIPが返ってくるから、やはりAWSの持つ親インスタンスとパブリックに通信しているということなのだろうか)
- aws_lb_listener_rule.prod/testではignore_changesステートメントを使用しています。コメントにある通り、ターゲットグループがBlue/Greenデプロイで変更されるためです
ecr.tf
############################################################################
## ECR
############################################################################
resource "aws_ecr_repository" "nginx" {
name = "${var.resource_id_prefix}-nginx-ecr-repository"
}
data "aws_ecr_authorization_token" "token" {}
# ECRにローカルからimageを初回pushする
# var.container_initial_build_pathでビルドするディレクトリを指定する
resource "null_resource" "image_push" {
provisioner "local-exec" {
command = <<-EOF
docker build ${var.container_initial_build_path} -t ${aws_ecr_repository.nginx.repository_url}:latest; \
docker login -u AWS -p ${data.aws_ecr_authorization_token.token.password} ${data.aws_ecr_authorization_token.token.proxy_endpoint}; \
docker push ${aws_ecr_repository.nginx.repository_url}:latest
EOF
}
}
- ECSタスク定義が参照するイメージを保存するリポジトリです
- ライフサイクル設定が必要かも
- ECSの初回deploy用にnull_resource.image_pushでローカルからECRへimageをpushしています
- 初回デプロイ用のタスク定義のimageをpublic ECRに向けても良かったか
modules/iam_role/main.tf
variable "name" {}
variable "policy" {}
variable "identifier" {}
resource "aws_iam_role" "default" {
name = var.name
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [var.identifier]
}
}
}
resource "aws_iam_policy" "default" {
name = var.name
policy = var.policy
}
resource "aws_iam_role_policy_attachment" "default" {
role = aws_iam_role.default.name
policy_arn = aws_iam_policy.default.arn
}
output "iam_role_arn" {
value = aws_iam_role.default.arn
}
output "iam_role_name" {
value = aws_iam_role.default.name
}
- IAMロールを作成するmodule。教本のパクリです(載せていいよね?怒られたら消します)
json/container_definitions.json
[
{
"name": "${container_name}",
"image": "${repository_uri}:latest",
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs-task",
"awslogs-group": "/ecs-task/example"
}
},
"portMappings": [
{
"protocol": "tcp",
"containerPort": 80
}
]
}
]
- 初回タスク定義作成のために用いるテンプレートjson
- 上述した通り、以後imageはCodebuild内で上書きされていくので、初回タスク定義の時点では「public.ecr.aws/nginx/nginx:stable-perl」などのpublic ECRを適当に設定しておいてもよい
ecs.tf
############################################################################
## ECSタスクロググループ
############################################################################
# ECSタスク内のコンテナのログ
resource "aws_cloudwatch_log_group" "for_ecs" {
name = "/ecs-task/example"
retention_in_days = 180
}
############################################################################
## ECSタスク実行ロール
## ECSタスク自体のロールではなく、ECRからpullしたりするECSサービスのロール
############################################################################
# ECS用AWSマネージドポリシーを取得
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
module "ecs_task_execution_role" {
source = "./modules/iam_role"
name = "${var.resource_id_prefix}-ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy = data.aws_iam_policy.ecs_task_execution_role_policy.policy
}
############################################################################
## ECSタスクSG
############################################################################
# ALBからのアクセスを受け付けるSG
resource "aws_security_group" "ecs_task" {
name = "${var.resource_id_prefix}-ecs-task-sg"
vpc_id = aws_vpc.main.id
}
resource "aws_security_group_rule" "ingress" {
security_group_id = aws_security_group.ecs_task.id
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
source_security_group_id = module.http_test_sg.aws_security_group_id
}
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.ecs_task.id
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
############################################################################
## ECSクラスタ
############################################################################
resource "aws_ecs_cluster" "cluster" {
name = "${var.resource_id_prefix}-ecs-cluster"
}
# ECSタスク定義
# ECSタスク全般の定義を行う
# コンテナ自体の設定はcontainer_definitionsで行う
resource "aws_ecs_task_definition" "task_def" {
family = var.task_family_name
cpu = "256"
memory = "512"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
container_definitions = templatefile("./json/container_definitions.json",{
container_name = var.container_name
repository_uri = aws_ecr_repository.nginx.repository_url
})
}
# ECSサービス定義
resource "aws_ecs_service" "service" {
name = "${var.resource_id_prefix}-ecs-service"
cluster = aws_ecs_cluster.cluster.arn
task_definition = aws_ecs_task_definition.task_def.arn
desired_count = 1
launch_type = "FARGATE"
platform_version = "1.4.0"
health_check_grace_period_seconds = 60
# codedeployでBlue/Greenデプロイさせる
deployment_controller {
type = "CODE_DEPLOY"
}
network_configuration {
# imageのpullでアウトバウンドが必ず発生するので、publicサブネットの場合はtrueにすること
# ただし、そもそもECSタスクをpublicサブネットに配置することはベストプラクティスに反する
# 簡単な検証以外ではprivateサブネットに設置し、NAT経由でアウトバウンドできるようにすること
# privateサブネットに配置する場合はfalseでよい
assign_public_ip = false
security_groups = [aws_security_group.ecs_task.id]
subnets = [
aws_subnet.private_1a.id,
aws_subnet.private_1c.id,
]
}
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name = var.container_name
container_port = 80
}
lifecycle {
ignore_changes = [
# load balancerで動的に変更されるため
desired_count,
# target_groupはBlue/Greenデプロイで動的に変更されるため
# load_balancerはkeyでアクセスできないため、objectごとignore
load_balancer,
# task_definitionはcodepipelineで動的に変更されるため
task_definition
]
}
}
- ECSクラスタ、サービス、タスク定義を設定しています
- ECSタスク・・・コンテナ群からなるインスタンスです。今回はnginxだけを動かしています
- ECSサービス・・・コンテナオーケストレーションを行ってくれるリソースです。タスクの実行や管理、死活監視などまるっとお世話してくれます
- ECSクラスタ・・・ECSサービスをまとめる単位です。サービス全体のInsights集計などを設定できますが今回はスルー
- aws_ecs_service.serviceにload_balancerステートメントが存在しますが、ALBと密結合しているわけではなく、ECSサービスはあくまでターゲットグループにタスクを配置しているだけです
- ALB同様、Blue/Greenで動的に変更される部分はignore_changesします
気を付けること
- ALB作成時に選択するsubnetにはpublic subnetを指定すること
- ignore_changesは忘れずに
メモ
- ECSサービスはNATを通じてECRからイメージをpullする(今回の構成だとこのためだけにNATが必要・・・。高額なのに微妙だよなー)
- クライアントにレスポンスを返却するのはALB
デプロイ用codeサービス周り
tfコード
codebuild.tf
############################################################################
## codebuild実行ロール
############################################################################
# codebuild用ロールポリシードキュメント
# 適当なのでいつか直すこと
data "aws_iam_policy_document" "codebuild" {
statement {
effect = "Allow"
resources = ["*"]
actions = [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:DescribeRepositories",
"ecr:ListImages",
"ecr:DescribeImages",
"ecr:BatchGetImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage",
]
}
}
# codebuild用ロール
module "codebuild_role" {
source = "./modules/iam_role"
name = "${var.resource_id_prefix}-codebuild"
identifier = "codebuild.amazonaws.com"
policy = data.aws_iam_policy_document.codebuild.json
}
############################################################################
## codebuildプロジェクト
############################################################################
resource "aws_codebuild_project" "build" {
name = "${var.resource_id_prefix}-build-project"
service_role = module.codebuild_role.iam_role_arn
source {
type = "CODEPIPELINE"
}
artifacts {
type = "CODEPIPELINE"
}
environment {
type = "LINUX_CONTAINER"
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/amazonlinux2-x86_64-standard:4.0"
# docker使用時はprivileged_mode設定にする
privileged_mode = true
# ビルド時に参照する環境変数(REPOSITORY_URL)
environment_variable {
name = "REPOSITORY_URL"
value = aws_ecr_repository.nginx.repository_url
type = "PLAINTEXT"
}
# ビルド時に参照する環境変数(TASK_FAMILY)
environment_variable {
name = "TASK_FAMILY"
value = var.task_family_name
type = "PLAINTEXT"
}
# ビルド時に参照する環境変数(EXECUTION_ROLE_ARN)
environment_variable {
name = "EXECUTION_ROLE_ARN"
value = module.ecs_task_execution_role.iam_role_arn
type = "PLAINTEXT"
}
# ビルド時に参照する環境変数(CONTAINER_NAME)
environment_variable {
name = "CONTAINER_NAME"
value = var.container_name
type = "PLAINTEXT"
}
}
cache {
type = "LOCAL"
modes = [
"LOCAL_DOCKER_LAYER_CACHE",
]
}
}
- CodePipelineでトリガーされ、ビルド処理を行います
- 具体的なビルド処理は以下のbuildspec.ymlを参照
buildspec.yml
version: 0.2
phases:
install:
commands:
- echo install started on `date`
- echo install finished on `date`
pre_build:
commands:
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $REPOSITORY_URL
- IMAGE_LATEST=$REPOSITORY_URL:latest
- IMAGE_CURRENT=$REPOSITORY_URL:$COMMIT_HASH
build:
commands:
- docker build -t $IMAGE_LATEST .
- docker tag $IMAGE_LATEST $IMAGE_CURRENT
- docker push $IMAGE_CURRENT
- docker push $IMAGE_LATEST
post_build:
commands:
- echo Rewriting task definitions file...
- envsubst < taskdef_template.json > taskdef.json
- echo Rewriting appspec file...
- envsubst < appspec_template.yaml > appspec.yaml
artifacts:
files:
- taskdef.json
- appspec.yaml
- imageのローカルビルド、およびECRへのpushを行った後、CodeDeployが参照するappspec、taskdefを出力します。固有の値は環境変数を通してenvsubstコマンドで注入されます
- docker runtimeは指定不要になったらしい・・・
appspec_template.yaml
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "${CONTAINER_NAME}"
ContainerPort: 80
PlatformVersion: "1.4.0"
taskdef_template.json
{
"executionRoleArn": "${EXECUTION_ROLE_ARN}",
"containerDefinitions": [
{
"name": "${CONTAINER_NAME}",
"image": "${IMAGE_CURRENT}",
"essential": true,
"portMappings": [
{
"hostPort": 80,
"protocol": "tcp",
"containerPort": 80
}
]
}
],
"requiresCompatibilities": [
"FARGATE"
],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512",
"family": "${TASK_FAMILY}"
}
- CodeDeployが参照する2つのファイルです
- IMAGE1_NAMEを使用せずcodebuild内でimageの指定を行います
codedeploy.tf
############################################################################
## codedeploy実行ロール
############################################################################
# codedeploy用AWSマネージドポリシーを取得
data "aws_iam_policy" "codedeploy_role_policy" {
arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}
module "codedeploy_role" {
source = "./modules/iam_role"
name = "${var.resource_id_prefix}-codedeploy"
identifier = "codedeploy.amazonaws.com"
policy = data.aws_iam_policy.codedeploy_role_policy.policy
}
############################################################################
## codedeployアプリケーション
############################################################################
resource "aws_codedeploy_app" "ecs_app" {
compute_platform = "ECS"
name = "${var.resource_id_prefix}-codedeploy-app"
}
# deployグループ作成
resource "aws_codedeploy_deployment_group" "ecs_deployment_group" {
deployment_group_name = "${var.resource_id_prefix}-deployment-group"
app_name = aws_codedeploy_app.ecs_app.name
deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
service_role_arn = module.codedeploy_role.iam_role_arn
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "STOP_DEPLOYMENT"
wait_time_in_minutes = 180
}
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.cluster.name
service_name = aws_ecs_service.service.name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.http_prod.arn]
}
test_traffic_route {
listener_arns = [aws_lb_listener.http_test.arn]
}
target_group {
name = aws_lb_target_group.blue.name
}
target_group {
name = aws_lb_target_group.green.name
}
}
}
}
- aws_codedeploy_deployment_groupでデプロイ時の挙動が定義されています
- CodeDeployはECSタスクセットを新規に作成後、prod_traffic_routeに指定したリスナーがターゲットにしていないtargetグループにタスクセットを配置し、test_traffic_routeに指定したリスナーのターゲットに設定します。
- test_traffic_route(今回は8080ポート)でテストが完了後は、prod_traffic_routeリスナーのターゲット設定の変更を行ってくれます
codepipeline.tf
############################################################################
## pipelineロール
############################################################################
# pipeline用ロールポリシー
# 適当なのでいつか直すこと
data "aws_iam_policy_document" "codepipeline" {
statement {
effect = "Allow"
resources = ["*"]
actions = [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetBucketVersioning",
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:UploadArchive",
"codecommit:GetUploadArchiveStatus",
"codecommit:CancelUploadArchive",
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
"codedeploy:*",
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:DescribeTasks",
"ecs:ListTasks",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService",
"iam:PassRole",
]
}
}
# moduleからロール作成
module "codepipeline_role" {
source = "./modules/iam_role"
name = "codepipeline"
identifier = "codepipeline.amazonaws.com"
policy = data.aws_iam_policy_document.codepipeline.json
}
############################################################################
## artifactストアS3
############################################################################
resource "aws_s3_bucket" "artifact" {
bucket = "test-itou-artifact-build"
}
############################################################################
## codepipeline
############################################################################
resource "aws_codepipeline" "pipeline" {
name = "${var.resource_id_prefix}-pipeline"
role_arn = module.codepipeline_role.iam_role_arn
artifact_store {
location = aws_s3_bucket.artifact.id
type = "S3"
}
stage {
name = "Source"
action {
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = 1
output_artifacts = ["Source"]
configuration = {
RepositoryName = var.branch_name
BranchName = var.repository_name
PollForSourceChanges = false
}
}
}
stage {
name = "Build"
action {
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = 1
input_artifacts = ["Source"]
output_artifacts = ["Build"]
configuration = {
ProjectName = aws_codebuild_project.build.id
}
}
}
stage {
name = "Deploy"
action {
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "CodeDeployToECS"
version = 1
# ステージ内で参照できるようinputsにbuild artifactsを指定
input_artifacts = ["Build"]
# 細かい設定
configuration = {
# 定義したリソースを指定
ApplicationName = aws_codedeploy_app.ecs_app.name
DeploymentGroupName = aws_codedeploy_deployment_group.ecs_deployment_group.deployment_group_name
# build artifactからtaskdefを取得
TaskDefinitionTemplateArtifact = "Build"
# build artifactからappspecを取得
AppSpecTemplateArtifact = "Build"
}
}
}
}
- 一連のデプロイ作業をpipeしてくれてます
- タスク定義のupdateをやってくれることに驚きました(なんとなくCodeDeployがしてくれてると思っていた)
気を付けること
- ロールは若干適当です。まっさらにしてポリシーエラーを1つずつ確認すべきだと思います
メモ
- それぞれのリソースの役割の認識はきちんと持つこと
- 例えば、タスク定義の更新は(まさかの)Codepipelineが行う、など・・・
終わりに
個人的に謎の記事です。
以前ECSのBlue/Greenデプロイをterraformで書いたとき、細かいところを微妙に納得しないままにしたな~と思い、そこらへんを込みで説明書的な記事を残したかったんですが、何が分からなかったか?を忘れてしまい、何を補足すれば良いのやら、コード見ればわかるじゃん状態です。
(ALBの挙動はいまだ気になりますが・・・)
気になっていること・確認したいことをメモ帳的にqiitaの下書きに残しておくのもありだと学習しました(謎結論)。