ECS Fargateデプロイ用CodePipelineのtaskdef.jsonに変数を持たせる方法

# はじめに
CodePipelineによるCI/CDパイプラインは便利なんだけど、せっかくTerraformやCloudFormationであれこれ変数化して自動化しているにもかかわらず、ECS Fargateにデプロイするための taskdef.json と Appspec.yml で変数化できずにハマるケースがあって困る。今回は、これを解決する手段を考えた。

# 前提条件
初学者向けのパイプライン作成ハンズオンに出てくるアウトプットをごちゃごちゃいじるので、少なくとも、CodePipelineの基礎は抑えていないと厳しい。過去の記事では以下のあたりを読んでおくと分かりやすくなっているはずだ。
なお、この記事ではIaCはTerraformで書いている。

- [CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(前編)](https://qiita.com/neruneruo/items/06b073f3846803f415e5)
- [CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(中編)](https://qiita.com/neruneruo/items/d62140f29f067b28925b)
- [CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(後編)](https://qiita.com/neruneruo/items/0f27dfc1117b404be8df)
- [Terraformの初心者がAmazon EC2に実行環境を作ってECS Fargateなアプリの自動構築をしてみる](https://qiita.com/neruneruo/items/d5e58b4013debdf9ebbe)

# 方針
CodePipelineの taskdef.json には、プレースホルダ(チュートリアルやハンズオンでは<IMAGE1_NAME>という名前で出てくる、コンテナのイメージIDで置換する機能)という強力な機能があるが、これは1つのビルドアーティファクトに対して1つのプレースホルダにしか対応していないし、最大4つまでしか指定できない。
これを駆使して頑張ろうとすると、変なビルドステージを作ったりしなければならずBuildSpecが煩雑化するので没にする。

となると、ビルドステージで良い感じに taskdef.json を生成してあげるのが簡単だろう。

# IaC見直し箇所
さて、通常のハンズオンやチュートリアルでは、taskdef.json はリポジトリに入っているものをそのままソースステージから引っ張ってくることになるが、今回はビルドステージで編集したものを利用するため、CodePipelineのリソースを以下のように修正する。

```HCL
  stage {
    name = "Deploy"

    action {
      run_order = 3
      name      = "Deploy"
      category  = "Deploy"
      owner     = "AWS"
      provider  = "CodeDeployToECS"
      version   = "1"
      input_artifacts = [
        "SourceArtifact",
        "BuildArtifact",
      ]

      configuration = {
        ApplicationName                = aws_codedeploy_app.application.name
        DeploymentGroupName            = aws_codedeploy_deployment_group.application.deployment_group_name
        AppSpecTemplateArtifact        = "SourceArtifact"
        AppSpecTemplatePath            = var.appspec_file_name
        TaskDefinitionTemplateArtifact = "BuildArtifact"  # ★ここをSourceArtifactから変更
        Image1ArtifactName             = "BuildArtifact"
        Image1ContainerName            = "IMAGE1_NAME"
      }
    }
```

また、taskdef.json は以下のように修正しておこう。
以下を、外部から指定可能にして置換できるようにする。
- \<EXECUTION_ROLE_ARN\>
- \<TASK_ROLE_ARN\>
- \<CONTAINER_NAME\>
- \<LOGGROUP_NAME\>
- \<TASK_FAMILY\>

```JSON
{
    "executionRoleArn": "<EXECUTION_ROLE_ARN>",
    "taskRoleArn": "<TASK_ROLE_ARN>",
    "containerDefinitions": [
      {
        "name": "<CONTAINER_NAME>",
        "image": "<IMAGE1_NAME>",
        "logConfiguration": {
          "logDriver": "awslogs",
          "options": {
            "awslogs-group": "<LOGGROUP_NAME>",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "ecs"
          }
        },
        "portMappings": [
          {
            "containerPort": 80,
            "hostPort": 80,
            "protocol": "tcp"
          }
        ],
        "essential": true,
        "cpu": 0,
        "memoryReservation": 256,
      }
    ],
    "requiresCompatibilities": ["FARGATE"],
    "networkMode": "awsvpc",
    "cpu": "256",
    "memory": "512",
    "family": "<TASK_FAMILY>"
  }
```

さて、CodeBuildでは、Buildspec の post_build で以下のようにして、上記の置換対象のタグを環境変数で置換する。
sed の置換の区切り文字をスラッシュから変更しているのは、IAMロールのARNやロググループ名にスラッシュが入ってくる可能性があるからだ。Terraformのtfファイル側で上手くエスケープしようと思ったが、面倒なのでBuildspec側で対応した。

```YAML
  post_build:
    commands:
      - docker push ${REPOSITORY_URI}:${IMAGE_TAG}
      - docker push ${REPOSITORY_URI}:latest
      - printf '{"name":"%s","ImageURI":"%s"}' $ECR_REPOSITORY_NAME $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
      # ★以下を追加
      - sed -i -e "s#<EXECUTION_ROLE_ARN>#${EXECUTION_ROLE_ARN}#" taskdef.json
      - sed -i -e "s#<TASK_ROLE_ARN>#${TASK_ROLE_ARN}#" taskdef.json
      - sed -i -e "s#<CONTAINER_NAME>#${CONTAINER_NAME}#" taskdef.json
      - sed -i -e "s#<LOGGROUP_NAME>#${LOGGROUP_NAME}#" taskdef.json
      - sed -i -e "s#<TASK_FAMILY>#${TASK_FAMILY}#" taskdef.json
```

また、taskdef.json をアーティファクトに指定することを忘れないように。

```YAML
artifacts:
  files:
    - imageDetail.json
    - taskdef.json
```

で、肝心のCodeBuild側から、良い感じに環境変数を渡してあげれば、taskdef.json も自動化の流れに乗せらえれる。CPUやメモリを検証環境とプロダクション環境で変更する場合も、tfファイル側でmapしてあげれば環境差分も吸収可能だ！

```HCL
resource "aws_codebuild_project" "application" {
(中略)
  environment {
    type            = "LINUX_CONTAINER"
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
    privileged_mode = "true"

    # 以下を追加
    environment_variable {
      name  = "EXECUTION_ROLE_ARN"
      value = aws_iam_role.ecs_taskexecution.arn
    }

    environment_variable {
      name  = "TASK_ROLE_ARN"
      value = aws_iam_role.ecs.arn
    }

    environment_variable {
      name  = "CONTAINER_NAME"
      value = var.container_name
    }

    environment_variable {
      name  = "LOGGROUP_NAME"
      value = aws_cloudwatch_log_group.ecstask_log_group.name
    }

    environment_variable {
      name  = "TASK_FAMILY"
      value = aws_ecs_task_definition.ecsfargate.family
    }
  }
(中略)
}
```

これでビルドを走らせると、ビルドアーティファクトの taskdef.json が以下のように出力される。

```JSON
{
    "executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/ContainerPipeline-ECSTaskExecutionRole",
    "taskRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/ContainerPipeline-ECSTaskRole",
    "containerDefinitions": [
      {
        "name": "ContainerPipeline-ECSContainer",
        "image": "<IMAGE1_NAME>",
        "logConfiguration": {
          "logDriver": "awslogs",
          "options": {
            "awslogs-group": "/ecstask/ContainerPipeline-LogGroup",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "ecs"
          }
        },
        "portMappings": [
          {
            "containerPort": 80,
            "hostPort": 80,
            "protocol": "tcp"
          }
        ],
        "essential": true,
        "cpu": 0,
        "memoryReservation": 256,
      }
    ],
    "requiresCompatibilities": ["FARGATE"],
    "networkMode": "awsvpc",
    "cpu": "256",
    "memory": "512",
    "family": "ContainerPipeline-ECSTaskFamily"
  }
```

Appspecの ```ContainerName``` も同じ要領で置換可能なはず。
これで、夢の全自動ビルドに一歩近づいた！