Terraform : ECS on Fargate + CI/CD (GitHub Actions)
概要
業務でECS on Fargate + GitHub Actionsを使用したコンテナ構築を担当することになった際に読んだAWSコンテナ設計・構築[本格]入門の内容が非常に良かったため、書籍の構成をベースにしたECSコンテナ環境をTerraformで構築し、GitHub Actionsを用いてCI/CDを実装しました。
構成図
初期構成 実装
TerraformコードはGitHubに記載しています。本記事では一部を抜粋して紹介させていただきますので、詳細はGitHubをご確認ください。
├─ environments
│ ├─ prd
│ ├─ stg
│ └─ dev
│ ├─ main.tf
│ ├─ local.tf
│ ├─ variable.tf
│ └─ (terraform.tfvars)
│
└─ modules(各Moduleにmain.tf,variable.tf,output.tfが存在)
├─ alb_ingress(Public ALB関連のリソース)
├─ alb_internal(Private ALB関連のリソース)
├─ ec2(管理EC2関連のリソース)
├─ ecr(ECR関連のリソース)
├─ ecs_backend(Backend ECS関連のリソース)
├─ ecs_frontend(Frontend ECS関連のリソース)
├─ initializer(tfstate用S3バケット作成)
├─ network(VPC、Subnet、VPNなどネットワーク全般のリソース)
├─ rds(RDS関連のリソース)
└─ secrets_manager(Secrets Manager関連のリソース)
Public ALB + Frontend ECS
下記構成図の赤枠で囲ったフロントエンド部分から確認していきます。
【Public ALB】
Public ALBは80番の本稼働リスナーを持ち、ターゲットグループに関連付けられたECSコンテナの80番ポートに通信を転送する一般的な構成となっています。ALBを用いてECSコンテナ間で負荷分散するためには、ECS Serviceをターゲットグループに関連付けた上で、通信の転送先であるECSコンテナを指定する必要があります(本関連付けはaws_ecs_service
にて定義しています)。
# Define ingress ALB
resource "aws_lb" "ingress" {
name = "${var.common.env}-alb-ingress"
internal = false
load_balancer_type = "application"
subnets = var.network.public_subnet_for_ingress_ids
security_groups = [var.network.security_group_for_ingress_alb_id]
tags = {
Name = "${var.common.env}-alb-ingress"
}
}
# Define the listner for ingress ALB
resource "aws_lb_listener" "ingress" {
load_balancer_arn = aws_lb.ingress.arn
protocol = "HTTP"
port = "80"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.ingress.arn
}
}
# Define target group for ingress ALB
resource "aws_lb_target_group" "ingress" {
name = "${var.common.env}-tg-frontend"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = var.network.vpc_id
health_check {
protocol = "HTTP"
path = "/healthcheck"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
interval = 15
matcher = 200
}
}
# Define ECS service
resource "aws_ecs_service" "frontend" {
name = "${var.common.env}-ecs-frontend-service"
~~~~~~~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~
load_balancer {
target_group_arn = var.alb_ingress.alb_target_group_ingress_arn
container_name = "app"
container_port = 80
}
}
【Frontend ECS】
Frontend ECSは1タスク当たり1コンテナというシンプルな構成になっています。管理用EC2で事前に作成したFrontend用コンテナイメージを用いて、ECSタスク定義を作成しています。
アプリの設計は詳しく把握できていませんが、コンテナ定義においてPrivate ALBのURLをValueとして持った環境変数を定義することで、ECSコンテナが受け取った通信をPrivate ALBに転送することを実現しているようです。同様にして、DB接続に必要な機密情報もSecrets Managerから取得し、環境変数として定義することでECSコンテナに渡しています。
# Define ECS task definition
resource "aws_ecs_task_definition" "frontend" {
family = "${var.common.env}-frontend-def"
requires_compatibilities = ["FARGATE"]
cpu = 512
memory = 1024
network_mode = "awsvpc"
execution_role_arn = aws_iam_role.task_execution_role.arn
container_definitions = jsonencode([
{
name = "app"
image = "${var.common.account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/container-app-frontend:v1"
cpu = 256
memory = 512
essential = true
environment = [
{
name = "APP_SERVICE_HOST"
value = "http://${var.alb_internal.alb_internal_dns_name}"
},
{
name = "NOTIF_SERVICE_HOST"
value = "http://${var.alb_internal.alb_internal_dns_name}"
},
{
name = "SESSION_SECRET_KEY"
value = "41b678c65b37bf99c37bcab522802760"
},
]
secrets = [
{
name = "DB_HOST"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:host::"
},
{
name = "DB_NAME"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:dbname::"
},
{
name = "DB_USERNAME"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:username::"
},
{
name = "DB_PASSWORD"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:password::"
}
]
portMappings = [{ containerPort = 80 }]
"readonlyRootFilesystem" : true
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-region : "ap-northeast-1"
awslogs-group : aws_cloudwatch_log_group.frontend.name
awslogs-stream-prefix : "ecs"
}
}
}
])
}
# Define ECS cluster
resource "aws_ecs_cluster" "frontend" {
name = "${var.common.env}-frontend-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
}
# Define ECS service
resource "aws_ecs_service" "frontend" {
name = "${var.common.env}-ecs-frontend-service"
cluster = aws_ecs_cluster.frontend.arn
task_definition = aws_ecs_task_definition.frontend.arn
launch_type = "FARGATE"
platform_version = "1.4.0"
scheduling_strategy = "REPLICA"
desired_count = 2
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
deployment_controller {
type = "ECS"
}
enable_ecs_managed_tags = true
network_configuration {
subnets = var.network.private_subnet_for_container_ids
security_groups = [var.network.security_group_for_frontend_container_id]
assign_public_ip = false
}
health_check_grace_period_seconds = 120
load_balancer {
target_group_arn = var.alb_ingress.alb_target_group_ingress_arn
container_name = "app"
container_port = 80
}
}
Private ALB + Backend ECS
下記構成図の赤枠で囲ったバックエンド部分を確認します。
【Private ALB】
Private ALBは80番の本稼働リスナーの他に、10080番のテストリスナーを持っており、それに対応する形で本稼働用/テスト用ターゲットグループ(Blue/Green)が定義されています。初期構築時にはBlue側のターゲットグループとECS Serviceをaws_ecs_service
において関連付けており、Green側のターゲットグループにリソースは関連付けられていません。
# Define internal ALB
resource "aws_lb" "internal" {
name = "${var.common.env}-alb-internal"
internal = true
load_balancer_type = "application"
subnets = var.network.private_subnet_for_container_ids
security_groups = [var.network.security_group_for_internal_alb_id]
tags = {
Name = "${var.common.env}-alb-internal"
}
}
# Define the production listner for internal ALB
resource "aws_lb_listener" "internal_prod" {
load_balancer_arn = aws_lb.internal.arn
protocol = "HTTP"
port = "80"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.internal_blue.arn
}
}
# Define target group Blue for internal ALB
resource "aws_lb_target_group" "internal_blue" {
name = "${var.common.env}-tg-backend-blue"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = var.network.vpc_id
health_check {
protocol = "HTTP"
path = "/healthcheck"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
interval = 15
matcher = 200
}
}
# Define the test listner for internal ALB
resource "aws_lb_listener" "internal_test" {
load_balancer_arn = aws_lb.internal.arn
protocol = "HTTP"
port = "10080"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.internal_blue.arn
}
}
# Define target group Green for internal ALB
resource "aws_lb_target_group" "internal_green" {
name = "${var.common.env}-tg-backend-green"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = var.network.vpc_id
health_check {
protocol = "HTTP"
path = "/healthcheck"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
interval = 15
matcher = 200
}
}
# Define ECS service
resource "aws_ecs_service" "backend" {
name = "${var.common.env}-ecs-backend-service"
~~~~~~~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~~~~~~
load_balancer {
target_group_arn = var.alb_internal.alb_target_group_internal_blue_arn
container_name = "app"
container_port = 80
}
}
【Backend ECS】
Backend ECSにおいても、Backend用コンテナイメージを使用してタスク定義を作成するくらいで、基本的な構成はFrontend ECSと変わりません。唯一大きく異なる点として、デプロイタイプが挙げられます。Frontend ECSではECSのみで設定が完結する「ローリングアップデート」でしたが、Backend ECSではCode Deployを使用した「B/Gデプロイメント」を実装しています。CI/CDにより新しくデプロイしたECSタスクをGreen側ターゲットグループに関連付けることで、本稼働環境に影響を与えることなく新しいアプリケーションをテストすることができます。
注意点ですが、CI/CDによりタスク定義が更新されるため、実際のAWS環境とTerraformコード/tfstateファイルの間で、ECSサービスで指定するタスク定義が異なってしまいます。
具体的には「実際のAWS環境ではv1.1のタスク定義を指定しているのに、terraformコードではv1.0のタスク定義を指定している」という状況が発生します。この状態でterraform apply
を実行すると、CI/CDで更新した最新のタスク定義(v1.1)を用いたECSサービスから、terraformで定義されている旧タスク定義(v1.0)を用いたECSサービスへ巻き戻そうとします。
そこで、aws_ecs_service
においてignore_changes
を定義することで、terraformコードの内容がAWS環境に反映されないように設定する必要があります(参考)。
※「ECSサービスへ巻き戻そうとします」と記載しましたが、実際にはエラーが発生し、ECSサービスの更新はできません。deployment_controller
としてCode Deployを指定した際には、ECSサービスを更新するためにCodeDeploy:CreateDeployment
をコールする必要がある一方で、terraformにてaws_ecs_service
を更新する際にコールされるAPIはECS:UpdateService
であるためです。
# Define ECS task definition
resource "aws_ecs_task_definition" "backend" {
family = "${var.common.env}-backend-def"
requires_compatibilities = ["FARGATE"]
cpu = 512
memory = 1024
network_mode = "awsvpc"
execution_role_arn = aws_iam_role.task_execution_role.arn
container_definitions = jsonencode([
{
name = "app"
image = "${var.common.account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/container-app-backend:v1"
cpu = 256
memory = 512
essential = true
secrets = [
{
name = "DB_HOST"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:host::"
},
{
name = "DB_NAME"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:dbname::"
},
{
name = "DB_USERNAME"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:username::"
},
{
name = "DB_PASSWORD"
valueFrom = "${var.secrets_manager.secret_for_db_arn}:password::"
}
]
portMappings = [{ containerPort = 80 }]
"readonlyRootFilesystem" : true
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-region : "ap-northeast-1"
awslogs-group : aws_cloudwatch_log_group.backend.name
awslogs-stream-prefix : "ecs"
}
}
}
])
}
# Define ECS cluster
resource "aws_ecs_cluster" "backend" {
name = "${var.common.env}-backend-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
}
# Define ECS service
resource "aws_ecs_service" "backend" {
name = "${var.common.env}-ecs-backend-service"
cluster = aws_ecs_cluster.backend.arn
task_definition = aws_ecs_task_definition.backend.arn
launch_type = "FARGATE"
platform_version = "1.4.0"
scheduling_strategy = "REPLICA"
desired_count = 2
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
deployment_controller {
type = "CODE_DEPLOY"
}
enable_ecs_managed_tags = true
network_configuration {
subnets = var.network.private_subnet_for_container_ids
security_groups = [
var.network.security_group_for_backend_container_id
]
assign_public_ip = false
}
health_check_grace_period_seconds = 120
load_balancer {
target_group_arn = var.alb_internal.alb_target_group_internal_blue_arn
container_name = "app"
container_port = 80
}
lifecycle {
ignore_changes = [
task_definition
]
}
}
下記コードにて、B/GデプロイメントのためのCode Deploy関連リソースを作成しています。
Code Deployによるデプロイ詳細設定はデプロイグループ(aws_codedeploy_deployment_group
)において定義されています。load_balancer_info/target_group_pair_info
において指定される本稼働リスナーとテストリスナーに対する転送先ターゲットグループをCode Deployが適宜更新することで、コードの下に図示したような順序でB/Gデプロイメントを実現してくれます。
Terraformではデプロイグループの定義までをおこない、実際のデプロイ実施はGitHub ActionsによるCI/CD処理により実現します。これに関しては、CI/CD 実装編にて詳しく解説します。
# Define CodeDeploy application
resource "aws_codedeploy_app" "backend" {
compute_platform = "ECS"
name = "${var.common.env}-backend-app"
}
# Define CodeDeploy deployment group
resource "aws_codedeploy_deployment_group" "backend" {
app_name = aws_codedeploy_app.backend.name
deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
deployment_group_name = "${var.common.env}-ecs-backend-deployment-group"
service_role_arn = aws_iam_role.codedeploy.arn
deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type = "BLUE_GREEN"
}
ecs_service {
cluster_name = aws_ecs_cluster.backend.name
service_name = aws_ecs_service.backend.name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [var.alb_internal.alb_listener_internal_blue_arn]
}
test_traffic_route {
listener_arns = [var.alb_internal.alb_listener_internal_green_arn]
}
target_group {
name = var.alb_internal.alb_target_group_internal_blue_name
}
target_group {
name = var.alb_internal.alb_target_group_internal_green_name
}
}
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "STOP_DEPLOYMENT"
wait_time_in_minutes = 10
}
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
termination_wait_time_in_minutes = 60
}
}
}
CI/CD 実装
書籍ではAWSのCode Seriesを使用してCI/CDを構成していましたが、2024年7月にCode Commitに対する新規アクセスが終了してしまいました。そこで今回はGitHubをリポジトリとして利用し、GitHub ActionsによりCI/CD処理を実装しています。
├─ .github
│ └─ actions
│ └─ container-build
│ └─action.yml(コンテナイメージのビルド処理を記載したactionファイル)
│ └─ container-deploy
│ └─action.yml(ECSコンテナのデプロイ処理を記載したactionファイル)
│ └─ workflows
│ └─ build_deploy.yml(CI/CD処理の流れを記載したWorkflowファイル)
│
├─ backend_app(CI/CD対象となるバックエンドアプリ)
│
└─ appspec.yml(Code Deployによるデプロイ処理内容を記載)
今回の構成におけるGitHub ActionsによるCI/CDの全体像・各アクション/コマンドの概要は以下のようになっています。各アクション/コマンドの詳細は次項から確認していきます。
-
actions/checkout
:特定ブランチ(下図ではmain)をローカルリポジトリにcheckoutする -
aws-actions/configure-aws-credentials
:OpenID Connect Provider (OIDC Trust) から一時的な認証情報を取得する -
aws-actions/amazon-ecr-login
:ECRに対する認証処理をおこなう -
docker/metadata-action
:イメージIDとタグ名を取得する
(イメージID:リポジトリURI、タグ名:コミットハッシュ) -
docker/build-push-action
:イメージをビルドし、ECRにPushする -
aws ecs describe-task-definition
:タスク定義をGithub Runner上にダウンロードする -
amazon-ecs-render-task-definition
:ローカルにダウンロードしてきたタスク定義におけるコンテナイメージを更新する -
amazon-ecs-deploy-task-definition
:ローカルで更新したタスク定義をAWSに登録し、そのタスク定義を使用してECSサービスを更新する
Workflows
GitHub ActionsによるCI/CD処理を定義したWorkflowファイルはsteps
に対応する4つのアクション(リモートアクション×2, ローカルアクション×2)により構成されています。backend_appディレクトリにおける任意のgoファイルをPushすると、これらのアクションが実行されます。
-
actions/checkout@v4
:【CI/CD 全体像】におけるNo.1です。リモートリポジトリの作業対象ブランチを GitHub Hosted Runner 内に複製する操作であり、内部的には git fetch + git checkout を実行しています。デフォルトではワークフローを実行したブランチをcheckoutしますが、refパラメータを利用することで別のブランチをcheckoutすることも可能です。 -
aws-actions/configure-aws-credentials
:【CI/CD 全体像】におけるNo.2です。GitHub OIDC Providerから取得したJWTトークンを用いて署名認証することで、引数で指定したIAM Roleで定義されている権限を有する一時的な認証情報を取得します。IAM RoleにはECR・ECS・Code Deployに対する権限が必要なことに注意が必要です。
-
./.github/actions/container-build/action.yml
:【CI/CD 全体像】におけるNo.3~No.5をまとめて定義したローカルアクションです。コンテナイメージをビルドし、ECRにPushします。詳細はActions編にて確認します。 -
./.github/actions/container-deploy/action.yml
:【CI/CD 全体像】におけるNo.6~No.8をまとめて定義したローカルアクションです。新しいコンテナイメージを使用するようにタスク定義を更新し、新コンテナをデプロイします。詳細はActions編にて確認します。
name: Deploy container
run-name: Deploy container
on:
push:
paths:
- 'backend_app/**/*.go'
workflow_dispatch:
env:
ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ vars.ROLE_NAME }}
SESSION_NAME: gh-deploy-${{ github.run_id }}-${{ github.run_attempt }}
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4 # リポジトリをチェックアウト
- uses: aws-actions/configure-aws-credentials@v4 # AWSの認証情報を設定
with:
role-to-assume: ${{ env.ROLE_ARN }}
role-session-name: ${{ env.SESSION_NAME }}
aws-region: ap-northeast-1
- uses: ./.github/actions/container-build/ # コンテナイメージのビルド/ECRにPush
id: build
with:
ecr-repository-uri: ${{ secrets.ECR_REPOSITORY_URI }}
dockerfile-path: backend_app/
- uses: ./.github/actions/container-deploy/ # コンテナのデプロイ
with:
ecs-cluster: ${{ vars.ECS_CLUSTER_NAME }}
ecs-service: ${{ vars.ECS_SERVICE_NAME }}
task-definition: ${{ vars.TASK_DEFINITION_NAME }}
container-name: ${{ vars.CONTAINER_NAME }}
container-image: ${{ steps.build.outputs.container-image }}
codedeploy-application: ${{ vars.CODEDEPLOY_APPLICATION_NAME }}
codedeploy-deploygroup: ${{ vars.CODEDEPLOY_DEPLOYGROUP_NAME }}
# Define IAM role for Github Actions
resource "aws_iam_role" "github_actions" {
name = "${var.common.env}-role-for-github-actions"
assume_role_policy = data.aws_iam_policy_document.trust_policy_for_github_actions.json
}
# Define trust policy for Github Actions role
data "aws_iam_policy_document" "trust_policy_for_github_actions" {
statement {
effect = "Allow"
principals {
type = "Federated"
identifiers = ["arn:aws:iam::${var.common.account_id}:oidc-provider/token.actions.githubusercontent.com"]
}
actions = ["sts:AssumeRoleWithWebIdentity"]
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${var.github_actions.account_name}/${var.github_actions.repository}:*"]
}
}
}
# Define IAM policy for Github Actions
resource "aws_iam_policy" "policy_for_github_actions" {
name = "${var.common.env}-policy-for-github-actions"
policy = data.aws_iam_policy_document.policy_for_github_actions.json
}
data "aws_iam_policy_document" "policy_for_github_actions" {
statement {
sid = "GetAuthorizationToken"
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken"
]
resources = ["*"]
}
statement {
sid = "PushImageOnly"
effect = "Allow"
actions = [
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
]
resources = [var.repository]
}
statement {
sid = "RegisterTaskDefinition"
effect = "Allow"
actions = [
"ecs:RegisterTaskDefinition",
"ecs:DescribeTaskDefinition"
]
resources = ["*"]
}
statement {
sid = "UpdateService"
effect = "Allow"
actions = [
"ecs:UpdateServicePrimaryTaskSet",
"ecs:DescribeServices",
"ecs:UpdateService"
]
resources = [aws_ecs_service.backend.id]
}
statement {
sid = "PassRole"
effect = "Allow"
actions = [
"iam:PassRole"
]
resources = [aws_iam_role.task_execution_role.arn]
condition {
test = "StringLike"
variable = "iam:PassedToService"
values = ["ecs-tasks.amazonaws.com"]
}
}
statement {
sid = "DeployService"
effect = "Allow"
actions = [
"codedeploy:CreateDeployment",
"codedeploy:GetDeployment",
"codedeploy:GetDeploymentConfig",
"codedeploy:GetDeploymentGroup",
"codedeploy:RegisterApplicationRevision"
]
resources = [
aws_codedeploy_app.backend.arn,
aws_codedeploy_deployment_group.backend.arn,
"arn:aws:codedeploy:${var.common.region}:${var.common.account_id}:deploymentconfig:*"
]
}
}
# Associate IAM policies with Github Actions role
resource "aws_iam_role_policy_attachments_exclusive" "github_actions" {
role_name = aws_iam_role.github_actions.name
policy_arns = [
"arn:aws:iam::aws:policy/IAMReadOnlyAccess",
aws_iam_policy.policy_for_github_actions.arn
]
}
Actions
【./.github/actions/container-build/action.yml】
以下の【CI/CD 全体像】において赤枠で囲んだNo.3~No.5のアクションをまとめて定義したアクションである./.github/actions/container-build/action.yml
について各アクションの詳細を確認します。
-
aws-actions/amazon-ecr-login
:【CI/CD 全体像】におけるNo.3です。ECRにログインするためのアクションであり、内部的にdocker login
コマンドを実行しています。 -
docker/metadata-action
:【CI/CD 全体像】におけるNo.4です。イメージ名やタグなどのメタデータを生成するアクションです。ECRにイメージをPushするためにはイメージ名としてリポジトリURIを指定する必要があります。イメージタグにはコミットハッシュを使用しています。 -
docker/build-push-action
:【CI/CD 全体像】におけるNo.5です。docker/metadata-action
により定義したイメージ名/タグを基に、引数で渡したDocker Fileからコンテナイメージをビルドし、ECRにPushします。
name: Container Build
description: コンテナイメージをビルドし、ECRへプッシュします。
inputs:
ecr-repository-uri:
required: true
description: ECRリポジトリのURI
dockerfile-path:
required: true
description: Dockerfileのパス
# metadata-actionのoutputであるsteps.meta.outputs.tagsは「イメージ名:タグ」という形式
# ex) xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/my-repo:sha-yyyyyyy
outputs:
container-image:
value: ${{ steps.meta.outputs.tags }}
description: ビルドしたコンテナイメージ
runs:
using: composite
steps:
- uses: aws-actions/amazon-ecr-login@v2 # Amazon ECRへのログイン
- uses: docker/metadata-action@v5 # コンテナイメージのメタデータ生成
id: meta
with:
images: ${{ inputs.ecr-repository-uri }}
tags: type=sha
- uses: docker/build-push-action@v5 # コンテナイメージのビルドとプッシュ
with:
push: true
context: ${{ inputs.dockerfile-path }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
【./.github/actions/container-deploy/action.yml】
以下の【CI/CD 全体像】において赤枠で囲んだNo.6~No.8のアクションをまとめて定義したアクションである./.github/actions/container-deploy/action.yml
について各アクションの詳細を確認します。
-
aws ecs describe-task-definition
:【CI/CD 全体像】におけるNo.6です。現在のタスク定義を取得します。タスク定義を取得するためのリモートアクションは提供されていないため、AWS CLIを実行しています。 -
aws-actions/amazon-ecs-render-task-definition
:【CI/CD 全体像】におけるNo.7です。取得した現在のタスク定義を更新します。引数を用いて、タスク定義における変更箇所を識別します。container-name
で更新対象のコンテナ名を指定して、image
で更新後のコンテナイメージURIを定義します。 -
aws-actions/amazon-ecs-deploy-task-definition
:【CI/CD 全体像】におけるNo.8です。更新したタスク定義をECSに登録し、そのタスク定義を使用してECSサービスを更新することで、B/Gデプロイメントを実行します。
name: Container Deploy
description: ECSサービスを更新し、コンテナをデプロイします。
inputs:
ecs-cluster:
required: true
description: ECSクラスター
ecs-service:
required: true
description: ECSサービス
task-definition:
required: true
description: タスク定義
container-name:
required: true
description: コンテナ名
container-image:
required: true
description: コンテナイメージ
codedeploy-application:
required: true
description: CodeDeployアプリケーション
codedeploy-deploygroup:
required: true
description: CodeDeployデプロイグループ
runs:
using: composite
steps:
# タスク定義をGithub Hosted Runnerにて取得(ファイル名:task-def.json)
- run: |
aws ecs describe-task-definition --task-definition "${TASK_DEFINITION}" \
--query taskDefinition --output json > "${RUNNER_TEMP}/task-def.json"
env:
TASK_DEFINITION: ${{ inputs.task-definition }}
shell: bash
# Github Hosted Runnerにて取得したタスク定義(task-def.json)におけるコンテナイメージURIを更新する
# task-definitionで更新するタスク定義ファイルを指定・container-nameで更新するコンテナ名を指定・imageでコンテナイメージURIを指定
- uses: aws-actions/amazon-ecs-render-task-definition@v1
id: render
with:
task-definition: ${{ runner.temp }}/task-def.json
container-name: ${{ inputs.container-name }}
image: ${{ inputs.container-image }}
# ローカルで更新したタスク定義をAWS上に登録し、そのタスク定義を使用してECSサービスを更新する
- uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
cluster: ${{ inputs.ecs-cluster }}
service: ${{ inputs.ecs-service }}
task-definition: ${{ steps.render.outputs.task-definition }}
wait-for-service-stability: true
codedeploy-appspec: appspec.yml
codedeploy-application: ${{ inputs.codedeploy-application }}
codedeploy-deployment-group: ${{ inputs.codedeploy-deploygroup }}
動作確認
初期構築
【Public ALB】
【Frontend ECS】
【Private ALB】
初期構築時点では、本稼働リスナーとテストリスナーの両者において、Blue側のターゲットグループに通信を転送します。Green側のターゲットグループにはリソースは関連付けられていません。
【Backend ECS】
初期構築時点では、管理用EC2で事前に作成したBackend用コンテナイメージ(container-app-backend:v1
)を使用してECSタスクを作成しています。
【ECR】
【Code Deploy】
【アプリケーションに接続】
フロントエンドアプリケーションのトップページにアクセスすると、バックエンドアプリケーションからAPIレスポンスとして取得したHello world
が出力されることが確認できます。
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/uma-arai/sbcntr-backend/domain/model"
)
// HelloWorldHandler ...
type HelloWorldHandler struct {
}
// NewHelloWorldHandler ...
func NewHelloWorldHandler() *HelloWorldHandler {
return &HelloWorldHandler{}
}
// SayHelloWorld ...
func (handler *HelloWorldHandler) SayHelloWorld() echo.HandlerFunc {
body := &model.Hello{
Data: "Hello world",
}
return func(c echo.Context) error {
return c.JSON(http.StatusOK, body)
}
}
CI/CDによるB/Gデプロイメント実行
上記のhelloworld_handler.goにおいて、Hello world
という部分をHello world! CI/CD succeeded!
に変更し、Commit/Pushします。CI/CD処理が実行され、トップページにおいてもHello world! CI/CD succeeded!
という文字列が出力されることを確認します。
【ECSタスク定義更新】
helloworld_handler.goを編集/Commit/Pushをすると、CI/CD処理が実行されます。ECRにはコミットハッシュをタグとして持つ新たなBackend用コンテナイメージ(container-app-backend:sha-6004d56
)が作成され、このコンテナイメージを使用した新たなECSタスク定義も作成されます。
【テストトラフィック設定】
続いてCode Deployによるデプロイが実行されます。ステップ3において、テストリスナーの転送先がGreen側ターゲットグループに更新されます。Green側ターゲットグループにはcontainer-app-backend:sha-6004d56
を用いて新たに作成されたECSタスクが関連付けられています。
【本稼働トラフィック再ルーティング】
ステップ3において「トラフィックの再ルーティング」を押下すると、本稼働リスナーの転送先がGreen側ターゲットグループに更新されます。ステップ5の段階では、旧ECSタスクは削除されずに残っています。「デプロイを停止してロールバック」を押下することで、本稼働リスナーの転送先をBlue用ターゲットグループへとロールバックすることができます。
【元のタスクセット終了】
ステップ5において「元のタスクセットの終了」を押下することで、旧ECSタスクを削除し、Code DeployによるB/Gデプロイメントを完了します。
【アプリケーションに接続】
再びフロントエンドアプリケーションのトップページにアクセスすると、Hello world! CI/CD succeeded!
という文字列が出力されることを確認できます。以上で、GitHub Actionsを使用したCI/CD実行の動作確認は完了です!
最後に
コンテナ+CI/CDという構成は、引き続き需要が高まっていくと思うので、もっと詳しい内容も把握できるように頑張らないといけませんね...
参考
GiHub Actions用 IAM権限
Discussion