GitHubActionsと連携してブルー/グリーンデプロイを実行[Terraform]


# はじめに

TerraformでIacをしながら、GitHubActionsと連携してブルー/グリーンデプロイを実行したいと思います。

codesourceでecrを監視しない理由は、最初のデブロイでブルー/グリーンデプロイを実行させないためです。
そのため、GitHubActionsでcodecommitにクローンしブルー/グリーンデプロイを実行するように設計しています。


以下が今回の構成図になります。

![aws_structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/293630c9-b5e1-0377-c255-debd97e88468.png)

以下がレポジトリーになります。

https://github.com/sugiyama404/practice_blue_green

## ブルー/グリーンデプロイとは

>ブルーグリーンデプロイメントとは現状の本番環境（ブルー）とは別に新しい本番環境（グリーン）を構築した上で、ロードバランサーの接続先を切り替えるなどして新しい本番環境をリリースする運用方法のこと。

中略

>ブルーグリーンデプロイメントの利点の1つは、システムのダウンタイムを短くできる点である。現状の本番環境を停止させて変更して戻す従来の運用方法とは違ってシステムをほとんど止める必要が無く、可用性を高められる。

以下、参照

https://xtech.nikkei.com/atcl/nxt/keyword/18/00002/070800077/

# アプリ編

terraformでデブロイした直後、画面を青くし、
ブルー/グリーンデプロイ後に画面を緑にします。

```Dockerfile:Dockerfile
FROM nginx:latest

COPY . /usr/share/nginx/html
```

```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>
```


```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を登録します。

https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services

https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-aws


## GitHubActions

GitHubActionsでcodecommitにクローンするのが前半
後半はecrにイメージをpushします。

```yaml:.github/workflows/main.yml
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に変えてくれます。

```terraform:terraform/modules/elb/aws_lb_listener.tf
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
  }
}
```

```terraform:terraform/modules/elb/aws_lb_target_group.tf
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取得して再利用します。


```terraform:terraform/modules/ecs/ecs_service.tf
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]
  }
}
```

```terraform:terraform/modules/ecs/ecs_task_definition.tf
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ディレクトリーに格納されます。

```terraform:terraform/modules/codepipeline/aws_codepipeline.tf
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"
      }
    }
  }
}
```

```terraform:terraform/modules/codepipeline/aws_codebuild_project.tf
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
    }
  }
}

```

```terraform:terraform/modules/codepipeline/aws_codecommit_repository.tf
resource "aws_codecommit_repository" "codecommit_repository" {
  repository_name = "${var.app_name}-repository"
  description     = "This is the ${var.app_name} App Repository"
}
```

cloudwatchでcodecommitを監視していないと、eventが発火しません。

```terraform:terraform/modules/codepipeline/aws_cloudwatch_event_rule.tf
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"]
    }
  })
}
```

```terraform:terraform/modules/codepipeline/aws_cloudwatch_event_target.tf
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
}
```

## 設定ファイル

```yml:terraform/template/buildspec.yml
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
```

```yml:terraform/template/appspec.yml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "<TASK_DEFINITION>"
        LoadBalancerInfo:
          ContainerName: "newworld"
          ContainerPort: 80
        PlatformVersion: "1.4.0"
```


# 結果


![Screenshot 2023-05-09 at 16.11.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/1a0e5510-87d1-0c1d-9ee0-a890fc294e61.png)

ブルー/グリーンデプロイを実行

![Screenshot 2023-05-07 at 13.04.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/df8bb031-26dc-c52c-3b30-fb7b8f045795.png)


![Screenshot 2023-05-09 at 16.31.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/be3452cb-1118-c4bf-3607-edb6c11dac6a.png)

無事にデプロイが出来ました。