RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その７、Blue/Greenデプロイ後編

# はじめに

RailsとNuxt3でtodoリストの作り方を
初めから丁寧に説明したいと思います。

使用pcはmacを想定しています。

完成した構成図は以下の通りです。

![aws_structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/362a1ef8-00e1-75d0-e923-ad2e59eca033.png)

また、githubレポジトリはこちらです。

https://github.com/sugiyama404/ruby_and_nuxt

各シリーズは以下の通りです。

[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その１、Rails基本設定編](https://qiita.com/sugiyama404/items/637900df3b533fd149f8)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その２、Rails API編](https://qiita.com/sugiyama404/items/acea4cc38fe7f155ab54)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その３、Nuxt.js編](https://qiita.com/sugiyama404/items/4e74c4446add5efc3669)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その４、TerraformECS前編](https://qiita.com/sugiyama404/items/4e19ac1f9be6b8213f55)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その５、TerraformECS後編](https://qiita.com/sugiyama404/items/d0e0e74628d38842c9bd)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その６、Blue/Greenデプロイ前編](https://qiita.com/sugiyama404/items/1ffb4334b3a1546b4e23)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その７、Blue/Greenデプロイ後編](https://qiita.com/sugiyama404/items/ea57e8c08156bf6eca46)

# codepipline

## codepipline

codepiplineはcodeシリーズをつなげるものです。


```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 {
    # S3 (アーティファクト格納用)
    # CodePileline では次のステージにアーティファクトを引き渡す時、S3 バケットを経由させます。
    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"

    # webserver
    action {
      name             = "WEB_Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["SourceArtifact"]
      output_artifacts = ["BuildArtifact1"]
      version          = "1"

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

    # apisever
    action {
      name             = "API_Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["SourceArtifact"]
      output_artifacts = ["BuildArtifact2"]
      version          = "1"

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

  stage {
    name = "Deploy"

    # webserver
    action {
      name            = "WEB_Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeployToECS"
      input_artifacts = ["BuildArtifact1"]
      version         = "1"

      configuration = {
        ApplicationName                = var.app_name
        DeploymentGroupName            = aws_codedeploy_deployment_group.web.deployment_group_name
        TaskDefinitionTemplateArtifact = "BuildArtifact1"
        TaskDefinitionTemplatePath     = "taskdef.json"
        AppSpecTemplateArtifact        = "BuildArtifact1"
        AppSpecTemplatePath            = "appspec.yml"
        Image1ArtifactName             = "BuildArtifact1"
        Image1ContainerName            = "IMAGE1_NAME"
      }
    }

    # apserver
    action {
      name            = "API_Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeployToECS"
      input_artifacts = ["BuildArtifact2"]
      version         = "1"

      configuration = {
        ApplicationName                = var.app_name
        DeploymentGroupName            = aws_codedeploy_deployment_group.api.deployment_group_name
        TaskDefinitionTemplateArtifact = "BuildArtifact2"
        TaskDefinitionTemplatePath     = "taskdef.json"
        AppSpecTemplateArtifact        = "BuildArtifact2"
        AppSpecTemplatePath            = "appspec.yml"
        Image1ArtifactName             = "BuildArtifact2"
        Image1ContainerName            = "IMAGE1_NAME"
      }
    }
  }
}
```

## codecommit

codecommitは、githubのミラーリングするために使います。

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

## codebuild

BuildAriifnに格納できるのは、3MBまでという制約はあるため、appspec.yml、taskdef.json、imageDetail.jsonの三つのみに絞ります。


```terraform:terraform/modules/codepipeline/aws_codebuild_project.tf
resource "aws_codebuild_project" "web" {
  name         = "${var.app_name}-web-codebuild"
  description  = "codebuild_project for web ${var.app_name}"
  service_role = aws_iam_role.codebuild-role.arn

  artifacts {
    type = "CODEPIPELINE"
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "terraform/template/web_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_WEB_REPOSITORY"
      value = var.web_app_name
    }
    environment_variable {
      name  = "ECS_WEB_TASK_DEFINITION_ARN"
      value = "${var.web_app_name}-definition"
    }
  }
}

resource "aws_codebuild_project" "api" {
  name         = "${var.app_name}-api-codebuild"
  description  = "codebuild_project for api ${var.app_name}"
  service_role = aws_iam_role.codebuild-role.arn

  artifacts {
    type = "CODEPIPELINE"
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "terraform/template/api_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_API_REPOSITORY"
      value = var.api_app_name
    }
    environment_variable {
      name  = "ECS_API_TASK_DEFINITION_ARN"
      value = "${var.api_app_name}-definition"
    }
  }
}
```

```yml:terraform/template/api_buildspec.yml
version: 0.2

env:
  variables:
    AWS_REGION: ap-northeast-1
    IMAGE_TAG: latest

phases:
  pre_build:
    commands:
      - API_REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_API_REPOSITORY
  build:
    commands:
      - printf '{"Version":"1.0","ImageURI":"%s"}' $API_REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
      - echo $ECS_API_TASK_DEFINITION_ARN
      - $(aws ecs describe-task-definition --task-definition $ECS_API_TASK_DEFINITION_ARN --query taskDefinition | jq '.containerDefinitions[0].image="<IMAGE1_NAME>"' > taskdef.json)
      - cat taskdef.json
      - $(aws ecs describe-task-definition --task-definition apiserver-definition --query taskDefinition | jq '.containerDefinitions[0].image="<IMAGE1_NAME>"' > taskdef.json)
      - cat taskdef.json
      - cp terraform/template/api_appspec.yml appspec.yml

artifacts:
  files:
    - appspec.yml
    - taskdef.json
    - imageDetail.json
```

```ymlterraform/template/web_buildspec.yml
version: 0.2

env:
  variables:
    AWS_REGION: ap-northeast-1
    IMAGE_TAG: latest

phases:
  pre_build:
    commands:
      - WEB_REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_WEB_REPOSITORY
  build:
    commands:
      - cp terraform/template/web_appspec.yml appspec.yml
      - $(aws ecs describe-task-definition --task-definition $ECS_WEB_TASK_DEFINITION_ARN --query taskDefinition | jq '.containerDefinitions[0].image="<IMAGE1_NAME>"' > taskdef.json)
      - printf '{"Version":"1.0","ImageURI":"%s"}' $WEB_REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
  files:
    - appspec.yml
    - taskdef.json
    - imageDetail.json
```


## codedeploy

```terraform:terraform/modules/codepipeline/aws_codedeploy_app.tf
resource "aws_codedeploy_app" "main" {
  compute_platform = "ECS"
  name             = var.app_name
}

resource "aws_codedeploy_deployment_group" "web" {
  app_name               = aws_codedeploy_app.main.name
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
  deployment_group_name  = var.web_app_name
  service_role_arn       = aws_iam_role.codedeploy-role.arn

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }

    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 = "todolist-cluster"
    service_name = "webserver-service"
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [var.http_arn]
      }

      target_group {
        name = var.web_blue_name
      }

      target_group {
        name = var.web_green_name
      }
    }
  }
}


resource "aws_codedeploy_deployment_group" "api" {
  app_name               = aws_codedeploy_app.main.name
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
  deployment_group_name  = var.api_app_name
  service_role_arn       = aws_iam_role.codedeploy-role.arn

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }

    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 = "todolist-cluster"
    service_name = "apiserver-service"
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [var.http_arn]
      }

      target_group {
        name = var.api_blue_name
      }

      target_group {
        name = var.api_green_name
      }
    }
  }
}
```

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

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


# 結果

最後に実行します。

![Screenshot 2023-06-04 at 14.08.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/0d04b4cb-2991-3711-9c62-ece334ed3b8c.png)

![Screenshot 2023-06-04 at 14.09.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/b96f9fdd-99a6-4bfc-7291-8cf3a8b28c52.png)