Amazon ECRにあるイメージを使う、AWS FargateをTerraformで構築する

# What's？

Amazon ECRにあるイメージを使用する、AWS FargateクラスタをTerraformで構築してみよう、ということで。

簡単なDockerイメージを作ってAmazon ECRにPushして、そのイメージを使ったAWS Fargateクラスタを作ってみます。

DockerイメージはPythonアプリで、コンテナ定義で指定された環境変数の内容を表示するものにしてみましょう。

# 環境

TerraformとAWS Providerのバージョンは、こちらです。

```shell
$ terraform version
Terraform v0.12.28
+ provider.aws v2.69.0
```


# Amazon ECRを作成する

最初に、Amazon ECRを使ってリポジトリを作成します。

`main.tf`

```go
terraform {
  required_version = "=  0.12.28"
}

provider "aws" {
  version = "2.69.0"
}

resource "aws_ecr_repository" "print_env_app" {
  name = "print-env-app"
}
```

こちらは簡単ですね。あとは`terraform apply`。

```shell
$ terraform apply
```


# DockerイメージをAmazon ECRにPushする

次に、Amazon ECRにDockerイメージをPushしましょう。

アプリケーションは、Pythonで作成します。

```shell
$ python3 -V
Python 3.6.9
```

アプリケーションコード。

`print-env-app.py`

```python
import os

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])

    env_value = os.environ['MY_VARIABLE']

    return [f"MY_VARIABLE = {env_value}".encode()]
```

`MY_VARIABLE`という名前の環境変数の値を表示するだけの、簡単なWSGIアプリケーションです。

こちらを使った、Dockerイメージを作成します。

`Dockerfile`

```shell
FROM python:3.6.11-slim-buster

ADD print-env-app.py print-env-app.py

RUN pip install gunicorn==20.0.4

ENTRYPOINT ["gunicorn", "print-env-app:application"]
CMD ["-b", "0.0.0.0:8000"]
```

WSGIサーバーはGunicornを使用し、デフォルト設定としてポートはデフォルトの8000を使うようにしつつ、ローカルアドレス外からも接続可能にしています。

Dockerイメージのビルド。

```shell
$ docker image build -t print-env-app:latest .
```

続いて、Amazon ECRにPushしましょう。最初にAmazon ECRにログイン。

```shell
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [AWS Account ID].dkr.ecr.ap-northeast-1.amazonaws.com
WARNING! Your password will be stored unencrypted in $HOME/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
```

作成したDockerイメージに、Amazon ECRを向くようにタグを追加してPush。

```shell
$ docker image tag print-env-app:latest [AWS Account ID].dkr.ecr.ap-northeast-1.amazonaws.com/print-env-app:latest
$ docker image push [AWS Account ID].dkr.ecr.ap-northeast-1.amazonaws.com/print-env-app:latest
```

これで、Amazon ECRに自分で作ったイメージを置けました。


# AWS Fargateクラスタを構築する

では、AWS FargateクラスタをTerraformで定義しましょう。

作成したTerraformの定義ファイルのうち、AWS Fargateに関する部分を抜粋しました。

`main.tf`

```go
terraform {
  required_version = "=  0.12.28"
}

provider "aws" {
  version = "2.69.0"
}

#############################
# いろいろ定義（あとで載せる）
#############################

locals {
  vpc_id = module.vpc.vpc_id

  private_subnets                = module.vpc.private_subnets
  ecs_service_security_groups    = [module.print_env_app_container_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

resource "aws_ecs_cluster" "print_env_app" {
  name = "print-env-app-cluster"
}

resource "aws_ecs_task_definition" "print_env_app" {
  family                   = "print-env-app-task-definition"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "print-env-app",
      "image": "[AWS Account ID].dkr.ecr.ap-northeast-1.amazonaws.com/print-env-app:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 8000
        }
      ],
      "environment": [
        { "name": "MY_VARIABLE", "value": "Hello World!!" }
      ]
    }
  ]
    JSON
}

resource "aws_ecs_service" "print_env_app" {
  name             = "print-env-app-service"
  cluster          = aws_ecs_cluster.print_env_app.arn
  task_definition  = aws_ecs_task_definition.print_env_app.arn
  desired_count    = 5
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  deployment_controller {
    type = "ECS" # default
  }

  network_configuration {
    assign_public_ip = false
    security_groups  = local.ecs_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "print-env-app"
    container_port   = 8000
  }
}
```

Amazon ECRにあるDockerイメージを使うためには、AWS Fargateのタスク定義に実行ロールの設定が必要です。

そのIAMロール定義。

```go
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "MyEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
```

ここで定義したIAMロールのARNを、`aws_ecs_task_definition`の`execution_role_arn`に指定する必要があります。

でないと、Terraformの実行時にこんなエラーを見ることになるでしょう。

```shell
Error: ClientException: Fargate requires task definition to have execution role ARN to support ECR images.

  on main.tf line xxx, in resource "aws_ecs_task_definition" "print_env_app":
 xxx: resource "aws_ecs_task_definition" "print_env_app" {
```

[Amazon ECS タスク実行 IAM ロール](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html)

ちなみに、`AmazonECSTaskExecutionRolePolicy`の内容はこんな感じです。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

AWS Fargateのクラスタ定義からタスク定義まで。コンテナ定義には、先ほどAmazon ECRにPushしたイメージを参照するように指定しています。

```go
resource "aws_ecs_cluster" "print_env_app" {
  name = "print-env-app-cluster"
}

resource "aws_ecs_task_definition" "print_env_app" {
  family                   = "print-env-app-task-definition"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn

  container_definitions = <<JSON
  [
    {
      "name": "print-env-app",
      "image": "[AWS Account ID].dkr.ecr.ap-northeast-1.amazonaws.com/print-env-app:latest",
      "essential": true,
      "portMappings": [
        {
          "protocol": "tcp",
          "containerPort": 8000
        }
      ],
      "environment": [
        { "name": "MY_VARIABLE", "value": "Hello World!!" }
      ]
    }
  ]
    JSON
}

resource "aws_ecs_service" "print_env_app" {
  name             = "print-env-app-service"
  cluster          = aws_ecs_cluster.print_env_app.arn
  task_definition  = aws_ecs_task_definition.print_env_app.arn
  desired_count    = 5
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  deployment_minimum_healthy_percent = 50

  deployment_controller {
    type = "ECS" # default
  }

  network_configuration {
    assign_public_ip = false
    security_groups  = local.ecs_service_security_groups
    subnets          = local.private_subnets
  }

  load_balancer {
    target_group_arn = local.load_balancer_target_group_arn
    container_name   = "print-env-app"
    container_port   = 8000
  }
}
```

あとは`terraform apply`して

```shell
$ terraform apply
```

構築後、動作確認。

```shell
$ curl [ALBのDNS名]
MY_VARIABLE = Hello World!!
```

OKです。

# VPC〜ALBまで

先ほどの`main.tf`で、以下のように書いていた部分を載せておきます。

```go
#############################
# いろいろ定義（あとで載せる）
#############################
```

VPC、ALB、セキュリティグループなどを、コミュニティモジュールを利用して定義しています。こちらは本筋ではないので、簡単に。

* VPC … [AWS VPC Terraform module](https://github.com/terraform-aws-modules/terraform-aws-vpc)
* ALB … [AWS Application and Network Load Balancer (ALB & NLB) Terraform module](https://github.com/terraform-aws-modules/terraform-aws-alb)
* セキュリティグループ … [AWS EC2-VPC Security Group Terraform module](https://github.com/terraform-aws-modules/terraform-aws-security-group)

先ほどのAWS Fargateの定義で、ローカル変数を参照していたのは、これらのモジュールの結果を利用するものです。

```go
locals {
  vpc_id = module.vpc.vpc_id

  private_subnets                = module.vpc.private_subnets
  ecs_service_security_groups    = [module.print_env_app_container_sg.this_security_group_id]
  load_balancer_target_group_arn = module.load_balancer.target_group_arns[0]
}
```

定義はこちら。

```go
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.44.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]

  map_public_ip_on_launch = true

  enable_nat_gateway     = true
  single_nat_gateway     = false
  one_nat_gateway_per_az = true
}

module "load_balancer_sg" {
  source  = "terraform-aws-modules/security-group/aws//modules/http-80"
  version = "3.11.0"

  name   = "load-balancer-sg"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
}

module "print_env_app_container_sg" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "3.11.0"

  name   = "print-env-app-container-sg"
  vpc_id = module.vpc.vpc_id

  ingress_with_cidr_blocks = [
    {
      from_port   = 8000
      to_port     = 8000
      protocol    = "tcp"
      description = "print-env-app inbound ports"
      cidr_blocks = "10.0.0.0/16"
    }
  ]

  egress_with_cidr_blocks = [
    {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      description = "print-env-app outbound ports"
      cidr_blocks = "0.0.0.0/0"
    }
  ]
}

module "load_balancer" {
  source  = "terraform-aws-modules/alb/aws"
  version = "5.6.0"

  name = "print-env-app"

  vpc_id             = module.vpc.vpc_id
  load_balancer_type = "application"
  internal           = false

  subnets         = module.vpc.public_subnets
  security_groups = [module.load_balancer_sg.this_security_group_id]

  target_groups = [
    {
      backend_protocol = "HTTP"
      backend_port     = 8000
      target_type      = "ip"

      health_check = {
        interval = 20
      }
    }
  ]

  http_tcp_listeners = [
    {
      port     = 80
      protocol = "HTTP"
    }
  ]
}
```