Github + CodeBuild + CodePipelineを利用したFargateのデプロイフローをTerraformで構築する


インフラエンジニアの寺岡です。
今回はFargateに対するアプリケーションのデプロイのお話です。
Code兄弟と言われていたりしますが
AWSでは各種サービスに対してデプロイを行う際に便利なサービスがいくつかあります。

今回はその中のCodeBuildとCodePipelineを利用して
Fargateに対してデプロイするパイプラインをTerraformで作成したのでコードを共有します。

Terraformのバージョンは「v0.12.24」です。
参考になされる場合はご注意ください。

今回構築したもの

以下の様になっています。

VPCはPublicとDMZとPrivateの3層構造にし
PublicサブネットにはALBとNatGatewayを
DMZサブネットにFargateのタスクを起動させてALBのターゲットグループに紐づけています。
デプロイのパイプラインの要のCodeBuildとCodePipelineをもう少し見てみましょう。
簡単に図示すると以下の様になります。

流れとしては非常にシンプルで

  • GithubとWebHookで連携し、GitへのPushイベントを検知してCodePipelineを自動実行する
  • Githubからソースコードを取得しCodeBuildでDockerイメージをビルドしてECRにPushする
  • ECRにPushしたイメージをFargateからPullして新しいタスクを立ち上げる
  • 立ち上げたタスクをALBのターゲットグループに登録する
  • 元から紐づいていた古いタスクをターゲットグループから除外する
  • 古いタスクを削除する

となっています。
デプロイが実行されると新旧のFargateのタスクが入れ替えるということですね。
ではさっそくTerraformのコードを見ていきましょう。

ディレクトリ構成

$ tree
.
├── alb.tf
├── buildspec.yml
├── codebuild.tf
├── codepipeline.tf
├── docker
│   ├── nginx
│   │   ├── Dockerfile
│   │   └── conf
│   │       ├── default.conf
│   │       └── nginx.conf
│   └── sites
│       └── index.html
├── docker-compose.yml
├── fargate.tf
├── github.tf
├── iam.tf
├── provider.tf
├── roles
│   ├── codebuild_assume_role.json
│   ├── codebuild_build_policy.json
│   ├── codepipeline_assume_role.json
│   ├── codepipeline_pipeline_policy.json
│   ├── fargate_task_assume_role.json
│   └── fargate_task_execution_policy.json
├── s3.tf
├── secrets
│   └── github_personal_access_token
├── securitygroup.tf
├── ssm.tf
├── tasks
│   └── container_definitions.json
├── terraform.tfstate
├── terraform.tfstate.backup
├── variables.tf
└── vpc.tf

7 directories, 28 files

ProviderとVariable

TerraformのProviderとVariableを書いておきます。

provider.tf

provider "aws" {
  access_key = var.access_key
  secret_key = var.secret_key
  region     = var.region

  assume_role {
    role_arn = var.role_arn
  }
}

provider "github" {
  token        = aws_ssm_parameter.github_personal_access_token.value
  organization = "Teraoka-Org"
}

variables.tf

####################
# Provider
####################
variable "access_key" {
  description = "AWS Access Key"
}

variable "secret_key" {
  description = "AWS Secret Key"
}

variable "role_arn" {
  description = "AWS Role Arn"
}

variable "region" {
  default = "ap-northeast-1"
}

VPC

VPCを作成します。

vpc.tf

####################
# VPC
####################
resource "aws_vpc" "vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-fargate-deploy"
  }
}

####################
# Subnet
####################
resource "aws_subnet" "public_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}a"
  cidr_block              = "10.0.10.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-deploy-public-1a"
  }
}

resource "aws_subnet" "public_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}c"
  cidr_block              = "10.0.11.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-deploy-public-1c"
  }
}

resource "aws_subnet" "dmz_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}a"
  cidr_block              = "10.0.20.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-deploy-dmz-1a"
  }
}

resource "aws_subnet" "dmz_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}c"
  cidr_block              = "10.0.21.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-deploy-dmz-1c"
  }
}

resource "aws_subnet" "private_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}a"
  cidr_block              = "10.0.30.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-deploy-private-1a"
  }
}

resource "aws_subnet" "private_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}c"
  cidr_block              = "10.0.31.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-deploy-private-1c"
  }
}

####################
# Route Table
####################
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "route-fargate-deploy-public"
  }
}

resource "aws_route_table" "dmz" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "route-fargate-deploy-dmz"
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "route-fargate-deploy-private"
  }
}

####################
# IGW
####################
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "igw-fargate-deploy"
  }
}

####################
# NATGW
####################
resource "aws_eip" "natgw" {
  vpc = true

  tags = {
    Name = "natgw-fargate-deploy"
  }
}

resource "aws_nat_gateway" "natgw" {
  allocation_id = aws_eip.natgw.id
  subnet_id     = aws_subnet.public_1a.id

  tags = {
    Name = "natgw-fargate-deploy"
  }

  depends_on = [aws_internet_gateway.igw]
}

####################
# Route
####################
resource "aws_route" "public" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
  depends_on             = [aws_route_table.public]
}

resource "aws_route" "dmz" {
  route_table_id         = aws_route_table.dmz.id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.natgw.id
  depends_on             = [aws_route_table.dmz]
}

####################
# Route Association
####################
resource "aws_route_table_association" "public_1a" {
  subnet_id      = aws_subnet.public_1a.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_1c" {
  subnet_id      = aws_subnet.public_1c.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "dmz_1a" {
  subnet_id      = aws_subnet.dmz_1a.id
  route_table_id = aws_route_table.dmz.id
}

resource "aws_route_table_association" "dmz_1c" {
  subnet_id      = aws_subnet.dmz_1c.id
  route_table_id = aws_route_table.dmz.id
}

resource "aws_route_table_association" "private_1a" {
  subnet_id      = aws_subnet.private_1a.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private_1c" {
  subnet_id      = aws_subnet.private_1c.id
  route_table_id = aws_route_table.private.id
}

Security Group

セキュリティグループを作成します。

securitygroup.tf

####################
# Security Group
####################
resource "aws_security_group" "alb" {
  name        = "alb-sg"
  description = "for ALB"
  vpc_id      = aws_vpc.vpc.id
}

resource "aws_security_group" "fargate" {
  name        = "fargate-sg"
  description = "for Fargate"
  vpc_id      = aws_vpc.vpc.id
}

#####################
# Security Group Rule
#####################
resource "aws_security_group_rule" "allow_http_for_alb" {
  security_group_id = aws_security_group.alb.id
  type              = "ingress"
  protocol          = "tcp"
  from_port         = 80
  to_port           = 80
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "allow_http_for_alb"
}

resource "aws_security_group_rule" "from_alb_to_fargate" {
  security_group_id        = aws_security_group.fargate.id
  type                     = "ingress"
  protocol                 = "tcp"
  from_port                = 80
  to_port                  = 80
  source_security_group_id = aws_security_group.alb.id
  description              = "from_alb_to_fargate"
}

resource "aws_security_group_rule" "egress_alb" {
  security_group_id = aws_security_group.alb.id
  type              = "egress"
  protocol          = "-1"
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "Outbound ALL"
}

resource "aws_security_group_rule" "egress_fargate" {
  security_group_id = aws_security_group.fargate.id
  type              = "egress"
  protocol          = "-1"
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "Outbound ALL"
}

ALB

ALBを作成します。

alb.tf

####################
# ALB
####################
resource "aws_lb" "alb" {
  name               = "alb-fargate-deploy"
  internal           = false
  load_balancer_type = "application"
  security_groups = [
    aws_security_group.alb.id
  ]
  subnets = [
    aws_subnet.public_1a.id,
    aws_subnet.public_1c.id
  ]
}

####################
# Target Group
####################
resource "aws_lb_target_group" "alb" {
  name                 = "fargate-deploy-tg"
  port                 = "80"
  protocol             = "HTTP"
  target_type          = "ip"
  vpc_id               = aws_vpc.vpc.id
  deregistration_delay = "60"

  health_check {
    interval            = "10"
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = "4"
    healthy_threshold   = "2"
    unhealthy_threshold = "10"
    matcher             = "200-302"
  }
}

####################
# Listener
####################
resource "aws_lb_listener" "alb" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb.arn
  }
}

Dockerコンテナの準備

Fargateの構築に入る前にデプロイするコンテナ用のDockerfileを作成しておきます。
タスクの起動確認を行うため事前にECRに手動でPushしておきましょう。

Dockerfile

FROM nginx:alpine

COPY ./docker/nginx/conf/default.conf /etc/nginx/conf.d/
ADD ./docker/nginx/conf/nginx.conf /etc/nginx/
COPY ./docker/sites/index.html /var/www/html/

EXPOSE 80

default.conf

server {
    listen 80 default_server;

    server_name localhost;
    index index.php index.html index.htm;

    location / {
        root /var/www/html;
    }
}

nginx.conf

user nginx;
worker_processes auto;
pid /run/nginx.pid;
error_log /dev/stdout warn;

events {
  worker_connections  1024;
}

http {
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent"';
  server_tokens off;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /dev/stdout main;
  include /etc/nginx/conf.d/*.conf;
  open_file_cache off;
  charset UTF-8;
}

index.html

<html>

<body>
    <p>terraform-fargate-deploy</p>
</body>

</html>

docker-compose.yml

version: "3"

services:
  nginx:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    image: fargate-deploy-nginx
    ports:
      - "80:80"

IAM

FargateでCode系のサービスで利用するIAMロールを用意しておきます。

iam.tf

####################
# IAM Role
####################
resource "aws_iam_role" "fargate_task_execution" {
  name               = "role-fargate-task-execution"
  assume_role_policy = file("./roles/fargate_task_assume_role.json")
}

resource "aws_iam_role" "codebuild_service_role" {
  name               = "role-codebuild-service-role"
  assume_role_policy = file("./roles/codebuild_assume_role.json")
}

resource "aws_iam_role" "codepipeline_service_role" {
  name               = "role-codepipeline-service-role"
  assume_role_policy = file("./roles/codepipeline_assume_role.json")
}

####################
# IAM Role Policy
####################
resource "aws_iam_role_policy" "fargate_task_execution" {
  name   = "execution-policy"
  role   = aws_iam_role.fargate_task_execution.name
  policy = file("./roles/fargate_task_execution_policy.json")
}

resource "aws_iam_role_policy" "codebuild_service_role" {
  name   = "build-policy"
  role   = aws_iam_role.codebuild_service_role.name
  policy = file("./roles/codebuild_build_policy.json")
}

resource "aws_iam_role_policy" "codepipeline_service_role" {
  name   = "pipeline-policy"
  role   = aws_iam_role.codepipeline_service_role.name
  policy = file("./roles/codepipeline_pipeline_policy.json")
}

codebuild_assume_role.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

codebuild_build_policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:GetAuthorizationToken",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Resource": [
                "*"
            ],
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "*"
            ],
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "ssm:GetParameters",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DescribeDhcpOptions",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeVpcs",
                "ec2:CreateNetworkInterfacePermission"
            ],
            "Resource": "*"
        }
    ]
}

codepipeline_assume_role.json

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "codepipeline.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
}

codepipeline_pipeline_policy.json

{
    "Statement": [
        {
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Condition": {
                "StringEqualsIfExists": {
                    "iam:PassedToService": [
                        "cloudformation.amazonaws.com",
                        "elasticbeanstalk.amazonaws.com",
                        "ec2.amazonaws.com",
                        "ecs-tasks.amazonaws.com"
                    ]
                }
            }
        },
        {
            "Action": [
                "codecommit:CancelUploadArchive",
                "codecommit:GetBranch",
                "codecommit:GetCommit",
                "codecommit:GetUploadArchiveStatus",
                "codecommit:UploadArchive"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codedeploy:CreateDeployment",
                "codedeploy:GetApplication",
                "codedeploy:GetApplicationRevision",
                "codedeploy:GetDeployment",
                "codedeploy:GetDeploymentConfig",
                "codedeploy:RegisterApplicationRevision"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "elasticbeanstalk:*",
                "ec2:*",
                "elasticloadbalancing:*",
                "autoscaling:*",
                "cloudwatch:*",
                "s3:*",
                "sns:*",
                "cloudformation:*",
                "rds:*",
                "sqs:*",
                "ecs:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "lambda:InvokeFunction",
                "lambda:ListFunctions"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "opsworks:CreateDeployment",
                "opsworks:DescribeApps",
                "opsworks:DescribeCommands",
                "opsworks:DescribeDeployments",
                "opsworks:DescribeInstances",
                "opsworks:DescribeStacks",
                "opsworks:UpdateApp",
                "opsworks:UpdateStack"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeStacks",
                "cloudformation:UpdateStack",
                "cloudformation:CreateChangeSet",
                "cloudformation:DeleteChangeSet",
                "cloudformation:DescribeChangeSet",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:SetStackPolicy",
                "cloudformation:ValidateTemplate"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Action": [
                "devicefarm:ListProjects",
                "devicefarm:ListDevicePools",
                "devicefarm:GetRun",
                "devicefarm:GetUpload",
                "devicefarm:CreateUpload",
                "devicefarm:ScheduleRun"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "servicecatalog:ListProvisioningArtifacts",
                "servicecatalog:CreateProvisioningArtifact",
                "servicecatalog:DescribeProvisioningArtifact",
                "servicecatalog:DeleteProvisioningArtifact",
                "servicecatalog:UpdateProduct"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:ValidateTemplate"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:DescribeImages"
            ],
            "Resource": "*"
        }
    ],
    "Version": "2012-10-17"
}

fargate_task_assume_role.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

fargate_task_execution_policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

Fargate

Fargateの構築を行います。

fargate.tf

####################
# ECR
####################
resource "aws_ecr_repository" "nginx" {
  name = "fargate-deploy-nginx"
}

####################
# Cluster
####################
resource "aws_ecs_cluster" "cluster" {
  name = "cluster-fargate-deploy"

  setting {
    name  = "containerInsights"
    value = "disabled"
  }
}

####################
# Task Definition
####################
resource "aws_ecs_task_definition" "task" {
  family                = "task-fargate-nginx"
  container_definitions = file("tasks/container_definitions.json")
  cpu                   = "256"
  memory                = "512"
  network_mode          = "awsvpc"
  execution_role_arn    = aws_iam_role.fargate_task_execution.arn

  requires_compatibilities = [
    "FARGATE"
  ]
}

####################
# Service
####################
resource "aws_ecs_service" "service" {
  name            = "service-fargate-deploy"
  cluster         = aws_ecs_cluster.cluster.arn
  task_definition = aws_ecs_task_definition.task.arn
  desired_count   = 1
  launch_type     = "FARGATE"

  load_balancer {
    target_group_arn = aws_lb_target_group.alb.arn
    container_name   = "nginx"
    container_port   = "80"
  }

  network_configuration {
    subnets = [
      aws_subnet.dmz_1a.id,
      aws_subnet.dmz_1c.id
    ]
    security_groups = [
      aws_security_group.fargate.id
    ]
    assign_public_ip = false
  }
}

container_definitions.json

[
    {
        "name": "nginx",
        "image": "485076298277.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-deploy-nginx:latest",
        "essential": true,
        "portMappings": [
            {
                "containerPort": 80,
                "hostPort": 80
            }
        ]
    }
]

CodeBuild

CodeBuildの構築を行います。

codebuild.tf

resource "aws_codebuild_project" "project" {
  name         = "project-fargate-deploy"
  description  = "project-fargate-deploy"
  service_role = aws_iam_role.codebuild_service_role.arn

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "aws/codebuild/standard:2.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true

    environment_variable {
      name  = "AWS_DEFAULT_REGION"
      value = "ap-northeast-1"
    }

    environment_variable {
      name  = "AWS_ACCOUNT_ID"
      value = "485076298277"
    }

    environment_variable {
      name  = "IMAGE_REPO_NAME_NGINX"
      value = "fargate-deploy-nginx"
    }

    environment_variable {
      name  = "IMAGE_TAG"
      value = "latest"
    }
  }

  source {
    type            = "GITHUB"
    location        = "https://github.com/beyond-teraoka/fargate-deploy-test.git"
    git_clone_depth = 1
    buildspec       = "buildspec.yml"
  }

  vpc_config {
    vpc_id = aws_vpc.vpc.id

    subnets = [
      aws_subnet.dmz_1a.id,
      aws_subnet.dmz_1c.id
    ]

    security_group_ids = [
      aws_security_group.fargate.id,
    ]
  }
}

S3

CodePipelineのアーティファクトを保存するS3バケットを作成しておきます。

s3.tf

resource "aws_s3_bucket" "pipeline" {
  bucket = "s3-fargate-deploy"
  acl    = "private"
}

SSM

CodePipelineでGitHubのリポジトリからソースコードを取得する際に
Githubの個人アクセストークンを利用します。
秘匿情報のためSSMのパラメータストアで管理します。

ssm.tf

####################
# Parameter
####################
resource "aws_ssm_parameter" "github_personal_access_token" {
  name        = "github-personal-access-token"
  description = "github-personal-access-token"
  type        = "String"
  value       = file("./secrets/github_personal_access_token")
}

valueの部分は./secrets以下のファイルの内容を読み込んでいます。

github_personal_access_token

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

CodePipeline

CodePipelineを構築します。

codepipeline.tf

resource "aws_codepipeline" "pipeline" {
  name     = "pipeline-fargate-deploy"
  role_arn = aws_iam_role.codepipeline_service_role.arn

  artifact_store {
    location = aws_s3_bucket.pipeline.bucket
    type     = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "ThirdParty"
      provider         = "GitHub"
      version          = "1"
      output_artifacts = ["source_output"]

      configuration = {
        Owner                = "Teraoka-Org"
        Repo                 = "fargate-deploy-test"
        Branch               = "master"
        OAuthToken           = aws_ssm_parameter.github_personal_access_token.value
        PollForSourceChanges = "false"
      }
    }
  }

  stage {
    name = "Build"

    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["source_output"]
      output_artifacts = ["build_output"]
      version          = "1"

      configuration = {
        ProjectName = aws_codebuild_project.project.name
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "ECS"
      input_artifacts = ["build_output"]
      version         = "1"

      configuration = {
        ClusterName = aws_ecs_cluster.cluster.arn
        ServiceName = aws_ecs_service.service.name
        FileName    = "imagedef.json"
      }
    }
  }
}

resource "aws_codepipeline_webhook" "webhook" {
  name            = "webhook-fargate-deploy"
  authentication  = "GITHUB_HMAC"
  target_action   = "Source"
  target_pipeline = aws_codepipeline.pipeline.name

  authentication_configuration {
    secret_token = aws_ssm_parameter.github_personal_access_token.value
  }

  filter {
    json_path    = "$.ref"
    match_equals = "refs/heads/{Branch}"
  }
}

Github

Github側にWebHookの設定を追加します。

github.tf

resource "github_repository_webhook" "webhook" {
  repository = "fargate-deploy-test"

  configuration {
    url          = aws_codepipeline_webhook.webhook.url
    content_type = "json"
    insecure_ssl = true
    secret       = aws_ssm_parameter.github_personal_access_token.value
  }

  events = ["push"]
}

buildspec

これで必要なファイルは以上です。
CodeBuildでどのようなビルド処理を実行するのかはbuildspec.ymlに書きます。
今回はDockerイメージをビルドしてECRにPushするための記述をします。
また、このファイルはGitリポジトリ内のルートディレクトリに保存しておく必要があります。

buildspec.yml

---
version: 0.2

phases:
  pre_build:
    commands:
      - IMAGE_URI_NGINX=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME_NGINX
      - IMAGE_URI_PHPFPM=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME_PHPFPM
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
  build:
    commands:
      - docker-compose build
      - docker tag fargate-deploy-nginx:$IMAGE_TAG $IMAGE_URI_NGINX
  post_build:
    commands:
      - docker push $IMAGE_URI_NGINX:$IMAGE_TAG
      - echo '[{"name":"nginx","imageUri":"__URI_NGINX__"}]' > imagedef.json
      - sed -ie "s@__URI_NGINX__@${IMAGE_URI_NGINX}:${IMAGE_TAG}@" imagedef.json
artifacts:
  files:
    - imagedef.json

Terraformを実行すると?

必要なリソースが作成されてALBのエンドポイントにブラウザでアクセスすると
以下のようなページが表示されます。

デプロイを実行してみる

index.htmlの内容を「terraform-fargate-deploy-test」に書き換えてGitにPushします。
すると以下のようにWebHook経由でCodePipelineが自動実行されます。

GitHubからソースコードを取得しようとしています。

完了するとCodeBuildの処理が実行され始めます。

ちゃんと実行されていますね。
実行が完了すると以下のようにECRにDockerイメージが保存されていることが確認できます。

CodeBuildの実行が終わるとFargateへのデプロイが始まります。

Fargateのタスクの数をみると1つ増えて合計2つになっています。

ターゲットグループの状態を見てみましょう。

タスクが2つ紐づいていて古い方を外そうとしていますね。
外れた後にもう一度ALB経由でアクセスしてみましょう。

ちゃんと更新されていますね。

CodePipelineも最初から最後まで正常に完了しました。

まとめ

いかがでしたでしょうか。
Fargateも含めてですがコンテナをプロダクション環境で扱うときは
一般的にCI/CDと言われているようにどのようにデプロイを行っていくのかを考えることが非常に重要です。
今回はAWSのCode系のサービスを複数連携させてみましたが
非常に扱いやすいサービスなので皆様も是非試してみてください。


この記事をかいた人

About the author

寺岡佑樹

2016年新卒入社、現在5年目。
SREとして、社内の運用業務の仕組みの検討・実装をしつつ
社外で技術的な登壇を行うエバンジェリスト的な側面も持つ。

・GitHub
https://github.com/nezumisannn

・登壇経歴
https://github.com/nezumisannn/my-profile

・発表資料(SpeakerDeck)
https://speakerdeck.com/nezumisannn