2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECSをBlue/Greenデプロイするイメージをterraformで掴んでみる

Last updated at Posted at 2024-01-06

はじめに

ECSのデプロイやBlue/Greenアップデートがどんなものだったか、記憶が薄れていたので思い出す意味でも初学時に書いたterraformを書き直しました。
全容を確認しながら、リソースの関係をイメージできればなと思います。

概要

アーキテクチャ

cap.png

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の下書きに残しておくのもありだと学習しました(謎結論)。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?