はじめに
TerraformでIacをしながら、GitHubActionsと連携してブルー/グリーンデプロイを実行したいと思います。
codesourceでecrを監視しない理由は、最初のデブロイでブルー/グリーンデプロイを実行させないためです。
そのため、GitHubActionsでcodecommitにクローンしブルー/グリーンデプロイを実行するように設計しています。
以下が今回の構成図になります。
以下がレポジトリーになります。
ブルー/グリーンデプロイとは
ブルーグリーンデプロイメントとは現状の本番環境(ブルー)とは別に新しい本番環境(グリーン)を構築した上で、ロードバランサーの接続先を切り替えるなどして新しい本番環境をリリースする運用方法のこと。
中略
ブルーグリーンデプロイメントの利点の1つは、システムのダウンタイムを短くできる点である。現状の本番環境を停止させて変更して戻す従来の運用方法とは違ってシステムをほとんど止める必要が無く、可用性を高められる。
以下、参照
アプリ編
terraformでデブロイした直後、画面を青くし、
ブルー/グリーンデプロイ後に画面を緑にします。
FROM nginx:latest
COPY . /usr/share/nginx/html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body style="background-color: #b2d8ff;">
<div style=" line-height: 200px;height: 200px;text-align: center;">
<p style=" line-height: 1.5;display: inline-block;vertical-align: middle;">
blue!!!!!!!!!!
</p>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body style="background-color: #b2ffb2;">
<div style=" line-height: 200px;height: 200px;text-align: center;">
<p style=" line-height: 1.5;display: inline-block;vertical-align: middle;">
green!!!!!!!!!!
</p>
</div>
</body>
</html>
GitHubActions編
arnを取得
以下の記事を参考にarnをコピーし、GitHubActionsにAWS_IAM_ROLE_ARNを登録します。
GitHubActions
GitHubActionsでcodecommitにクローンするのが前半
後半はecrにイメージをpushします。
name: ecr push image
on:
push:
branches: ["main"]
paths:
- nginx/**
defaults:
run:
shell: bash
permissions:
id-token: write
contents: read
env:
AWS_REGION: ap-northeast-1
AWSWEB_IDENTITY_TOKEN_FILE: /tmp/awscreds
CODECOMMIT_REPO_URL: "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/newworld-repository"
jobs:
aws-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout.
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: AWS Configure Credentials.
uses: aws-actions/configure-aws-credentials@v1-node16
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
role-session-name: githubActionsAession
- name: Configure git
run: |
git config --global credential.helper '!aws codecommit credential-helper $@'
git config --global credential.UseHttpPath true
- name: Push to AWS CodeCommit
run: |
git remote add codecommit ${{ env.CODECOMMIT_REPO_URL }}
git push codecommit main:main -f
# ecr
- uses: aws-actions/amazon-ecr-login@v1
id: login-ecr
- name: build and push docker image to ecr
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: newworld
run: |
docker build ./nginx --tag ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:latest
docker push ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:latest
Terraform編
elb
target_group_arnには、blueを指定します。codedeployが自動でgreenに変えてくれます。
resource "aws_lb_listener" "main" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
}
resource "aws_lb_target_group" "blue" {
name = "${var.app_name}-blue-tg"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = var.main-vpc-id
health_check {
interval = 300
path = "/"
port = 80
protocol = "HTTP"
timeout = 120
unhealthy_threshold = 10
matcher = "200-299"
}
}
resource "aws_lb_target_group" "green" {
name = "${var.app_name}-green-tg"
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = var.main-vpc-id
health_check {
interval = 300
path = "/"
port = 80
protocol = "HTTP"
timeout = 120
unhealthy_threshold = 10
matcher = "200-299"
}
}
ecs
taskdefinitionはcodebuild実行時、buildspec.yml取得して再利用します。
resource "aws_ecs_service" "app-service" {
name = "${var.app_name}-blue-service"
cluster = aws_ecs_cluster.app-cluster.id
task_definition = aws_ecs_task_definition.app-definition.arn
desired_count = 1
network_configuration {
security_groups = [var.app-sg-id]
subnets = var.subnet-group-id-list
assign_public_ip = true
}
load_balancer {
target_group_arn = var.elb-tg-blue-arn
container_name = var.image_name
container_port = 80
}
deployment_controller {
type = "CODE_DEPLOY"
}
lifecycle {
ignore_changes = [desired_count, task_definition, load_balancer]
}
}
resource "aws_ecs_task_definition" "app-definition" {
family = "${var.app_name}-definition"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
execution_role_arn = var.ecs-iam-role-arn
network_mode = "awsvpc"
container_definitions = jsonencode([
{
name = "${var.image_name}"
image = "${data.aws_caller_identity.self.account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/${var.image_name}:latest"
cpu = 256
memory = 512
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
}
]
}
])
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}
}
CodePileline
CodePilelineでは次のステージにアーティファクトを引き渡す時、S3 バケットを経由させます。
Sourceステージのoutputはs3内のSourceArtiディレクトリーに格納されます。
Buildステージのoutputはs3内のBuildArtifディレクトリーに格納されます。
resource "aws_codepipeline" "codepipeline" {
name = "${var.app_name}-pipeline"
role_arn = aws_iam_role.codepipeline_role.arn
artifact_store {
location = aws_s3_bucket.codepipeline_bucket.bucket
type = "S3"
}
stage {
name = "Source"
action {
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
output_artifacts = ["SourceArtifact"]
version = "1"
configuration = {
RepositoryName = aws_codecommit_repository.codecommit_repository.repository_name
BranchName = "main"
OutputArtifactFormat = "CODE_ZIP"
}
}
}
stage {
name = "Build"
action {
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
input_artifacts = ["SourceArtifact"]
output_artifacts = ["BuildArtifact"]
version = "1"
configuration = {
ProjectName = aws_codebuild_project.main.name
}
}
}
stage {
name = "Deploy"
action {
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "CodeDeployToECS"
input_artifacts = ["BuildArtifact"]
version = "1"
configuration = {
ApplicationName = var.app_name
DeploymentGroupName = aws_codedeploy_deployment_group.main.deployment_group_name
TaskDefinitionTemplateArtifact = "BuildArtifact"
TaskDefinitionTemplatePath = "taskdef.json"
AppSpecTemplateArtifact = "BuildArtifact"
AppSpecTemplatePath = "appspec.yml"
Image1ArtifactName = "BuildArtifact"
Image1ContainerName = "IMAGE1_NAME"
}
}
}
}
resource "aws_codebuild_project" "main" {
name = "${var.app_name}-codebuild"
description = "codebuild_project for ${var.app_name}"
service_role = aws_iam_role.codebuild-role.arn
artifacts {
type = "CODEPIPELINE"
}
source {
type = "CODEPIPELINE"
buildspec = "terraform/template/buildspec.yml"
}
environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
type = "LINUX_CONTAINER"
privileged_mode = true
environment_variable {
name = "AWS_ACCOUNT_ID"
value = data.aws_caller_identity.self.account_id
}
environment_variable {
name = "ECR_REPOSITORY"
value = var.image_name
}
environment_variable {
name = "ECS_TASK_DEFINITION_ARN"
value = var.ecs-task-definition-arn
}
}
}
resource "aws_codecommit_repository" "codecommit_repository" {
repository_name = "${var.app_name}-repository"
description = "This is the ${var.app_name} App Repository"
}
cloudwatchでcodecommitを監視していないと、eventが発火しません。
resource "aws_cloudwatch_event_rule" "image_push" {
name = "codecommit_push"
role_arn = aws_iam_role.cwe_role.arn
event_pattern = jsonencode({
source = ["aws.codecommit"]
detail-type = ["CodeCommit Repository State Change"]
resources = ["${aws_codecommit_repository.codecommit_repository.arn}"]
detail = {
event = ["referenceCreated", "referenceUpdated"]
referenceType = ["branch"]
referenceName = ["main"]
}
})
}
resource "aws_cloudwatch_event_target" "codepipeline" {
rule = aws_cloudwatch_event_rule.image_push.name
target_id = "${var.app_name}-Image-Push-Codepipeline"
arn = aws_codepipeline.codepipeline.arn
role_arn = aws_iam_role.cwe_role.arn
}
設定ファイル
version: 0.2
env:
variables:
AWS_REGION: ap-northeast-1
IMAGE_TAG: latest
phases:
pre_build:
commands:
- REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY
build:
commands:
- $(aws ecs describe-task-definition --task-definition $ECS_TASK_DEFINITION_ARN --query taskDefinition | jq '.containerDefinitions[0].image="<IMAGE1_NAME>"' > taskdef.json)
- printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
- cp terraform/template/appspec.yml appspec.yml
artifacts:
files:
- appspec.yml
- taskdef.json
- imageDetail.json
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "<TASK_DEFINITION>"
LoadBalancerInfo:
ContainerName: "newworld"
ContainerPort: 80
PlatformVersion: "1.4.0"
結果
ブルー/グリーンデプロイを実行
無事にデプロイが出来ました。