TerraformでECS on Fargate構築

# 目的
TerraformによるECS on Fargateの構築手順を載せていくが、各段階で必要な知識だったり、なぜこのパラメータを設定するかなどの背景や解説もしていく。

執筆時点でterraform歴３日程度なので、間違っている点がありましたらご容赦ください。

# 前提
- AWSアカウントを保持していること
- Terraform、aws-cliを既にインストールしている

# 0. Terraformの準備
## terraform block
```terraform
# terraformに関する情報の設定
terraform {
  # terraformのバージョン指定
  #　　バージョン指定方法についてはこちら　https://learn.hashicorp.com/tutorials/terraform/versions#terraform-version-constraints
  # ~> を指定するのがベストプラクティスだそう
  required_version = "1.2.2"

  # terraformで使用するプロバイダーの指定
  # keyでそのプロバイダーのローカルネームを決定できる。以下のprovider blockでそのローカルネームが使える
  required_providers {
    # local name: https://www.terraform.io/language/providers/requirements#local-names
    aws = {
      source  = "hashicorp/aws"
      version = "4.18.0"
    }
  }
}
```

terraform　blockでは、変数やリソースなどのオブジェクトは参照しない。
> Each terraform block can contain a number of settings related to Terraform's behavior. Within a terraform block, only constant values can be used; arguments may not refer to named objects such as resources, input variables, etc, and may not use any of the Terraform language built-in functions.

### source address
- hostname
- namespace
- type


の３つから成り立っている。
公式プロバイダーは、registry.terraform.io（hostname）上のhashicorp(namespace)に属している。
公式プロバイダーであるhttpだと、source addressは、registry.terraform.io/hashicorp/httpとなる。

hostnameのデフォルトは、 registry.terraform.io であるため、 公式プロバイダーであれば、hashicortp/http　というショートハンドで指定できる。

https://www.terraform.io/language/providers/requirements#source-addresses

### provider configuration
```terraform
# "aws" は上記の required_providers で設定したlocal name
provider "aws" {
  region = "ap-northeast-1"
}
```

provider に関する設定。
なんのパラメータを指定するかはドキュメントを見るのが良い。
今回使用している hashicorp/awsであれば[こちら](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference)

シークレットキーなどはこのファイルに直接書かず、環境変数に設定したり、 shared_config_files などを指定してあげるのが良い。

https://www.terraform.io/language/providers/configuration

# 1. NetWorkの構築

## 1.1 VPC
何はともあれVPCを作成


```terraform
resource "aws_vpc" "tf_vpc" {
  cidr_block           = "10.1.0.0/16"
  # ハードウェアを共有するかどうか
  # https://dev.classmethod.jp/articles/ec2-tenancy/
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "tf_vpc"
  }
}
```

[aws_vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc)

### enable_dns_support
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-dns.html#vpc-dns-support
VPC が Amazon提供のDNSサーバーを介した DNS解決策をサポートする

### enable_dns_hostnames
VPC がパブリック IP アドレスを持つインスタンスへのパブリック DNS ホスト名の割り当てをサポートする

**両方がtrueの場合**

- Public IPアドレスを持つインスタンスは、対応するPublic DNS ホスト名を受け取る。
- Amazon Route 53 Resolver サーバーは、Amazon が提供するPrivate DNS ホスト名を解決できます。

**片方がfalseの場合**

- Public IPアドレスを持つインスタンスは、対応するPublic DNS ホスト名を受け取らない
- Amazon Route 53 Resolver は、Amazon が提供するPrivate DNS ホスト名を解決できない

:::note warn
警告
Private DNS ホスト名が付与されていても実際の挙動としては、名前解決がされない
:::
https://blog.serverworks.co.jp/tech/2017/04/03/vpc-dns-matome/


# 1.2 Subnet

必要なサブネットは以下

- Public Subnet
    -  Load Balancer
- Private Subnet
    - Application(Nginx)

ロードバランサーを設定する関係上サブネットを２つのAZに作成しておく
(指定するAZは１つでもいいらしいけど、terraformで１つしか指定しなかったらエラー吐かれた気がする・・)

> Select at least one Availability Zone and one subnet for each zone. We recommend selecting at least two Availability Zones. The load balancer will route traffic only to targets in the selected Availability Zones. Zones that are not supported by the load balancer or VPC cannot be selected. Subnets can be added, but not removed, once a load balancer is created.

【追記】
以下のドキュメントには、２つ以上のAZを指定しなければいけないって書いてた。
　
> You must select at least two Availability Zone subnets. 
Each subnet must be from a different Availability Zone.
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#subnets-load-balancer

```terraform
resource "aws_subnet" "alb_public_subnet_1a" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.1.1.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = "tf-alb-public-subnet-1a"
  }
}

resource "aws_subnet" "app_private_subnet_1a" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.1.2.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = "tf-app-private-subnet-1a"
  }
}

## ap-northeast-1c
resource "aws_subnet" "alb_public_subnet_1c" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.1.11.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = "tf-alb-public-subnet-1c"
  }
}

resource "aws_subnet" "app_private_subnet_1c" {
  vpc_id            = aws_vpc.tf_vpc.id
  cidr_block        = "10.1.12.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = "tf-app-private-subnet-1c"
  }
}
```

# 1.3 Internet Gateway

パブリックサブネットからインターネットに接続するために作成

```terraform
resource "aws_internet_gateway" "tf_internet_gateway" {
  vpc_id = aws_vpc.tf_vpc.id
}
```

# 1.4 NatGateway

Application(Nginx)は、Private Subnetに配置することで、外界からのリクエストは、Public Sunbetに配置されているロードバランサーを通して行われる構成を想定している。

ECSを使用する場合、Containerを起動させるためにDocker Imageが必要となる。
コンテナはPrivate Subnetに配置しているので、Docker ImageをPullしてくるときにインターネットにアクセスする必要がある。このためにNatGatewayが必要になってくる。
(他にも、VPC Endpointを使用するなどあります)

Public NatGatewayを作成するとき、EIPを紐づける必要がある。

> NAT ゲートウェイは、インスタンスの送信元 IP アドレスを NAT ゲートウェイの IP アドレスに置き換えます。パブリック NAT ゲートウェイの場合、これは NAT ゲートウェイの Elastic IP アドレス です。... インスタンスに応答トラフィックを送信するとき、NAT デバイスはアドレスを元の送信元 IP アドレスに変換します。
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-nat-gateway.html


```terraform
### EIP ###
resource "aws_eip" "tf_eip_1a" {
  vpc = true
  tags = {
    "Name" = "tf-eip-1a"
  }
}

resource "aws_eip" "tf_eip_1c" {
  vpc = true
  tags = {
    "Name" = "tf-eip-1c"
  }
}

## NatGateway ###
resource "aws_nat_gateway" "tf_nat_gateway_1a" {
  allocation_id = aws_eip.tf_eip_1a.id
  subnet_id = aws_subnet.alb_public_subnet_1a.id

  tags = {
    "Name" = "tf-nat-gateway-1a"
  }

  depends_on = [aws_internet_gateway.tf_internet_gateway]
}

resource "aws_nat_gateway" "tf_nat_gateway_1c" {
  allocation_id = aws_eip.tf_eip_1c.id
  subnet_id = aws_subnet.alb_public_subnet_1c.id

  tags = {
    "Name" = "tf-nat-gateway-1c"
  }

  depends_on = [aws_internet_gateway.tf_internet_gateway]
}
```


# 1.5 Route Table
ルートテーブルはサブネット単位で指定し、通信先を決定する。

Public Subnetには、インターネットにアクセスできるよう、インターネットゲートウェイへのルーティングを、Private Subnetには、Nat Gatewayへのルーティングを追加する。

```terraform
### Route Table ###
## ALB ##
resource "aws_route_table" "alb_route_table" {
  vpc_id = aws_vpc.tf_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.tf_internet_gateway.id
  }
  tags = {
    "Name" = "tf-alb-route-table"
  }
}

resource "aws_route_table_association" "alb_table_association_1a" {
  subnet_id      = aws_subnet.alb_public_subnet_1a.id
  route_table_id = aws_route_table.alb_route_table.id
}

resource "aws_route_table_association" "alb_table_association_1c" {
  subnet_id      = aws_subnet.alb_public_subnet_1c.id
  route_table_id = aws_route_table.alb_route_table.id
}

## Application ##
# ap-northeast-1a
resource "aws_route_table" "app_route_table_1a" {
  vpc_id = aws_vpc.tf_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.tf_nat_gateway_1a.id
  }

  tags = {
    "Name" = "tf-app-route-table-1a"
  }
}

resource "aws_route_table_association" "app_route_table_association_1a" {
  subnet_id = aws_subnet.app_private_subnet_1a.id
  route_table_id = aws_route_table.app_route_table_1a.id
}

# ap-north-east-1c
resource "aws_route_table" "app_route_table_1c" {
  vpc_id = aws_vpc.tf_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.tf_nat_gateway_1c.id
  }

  tags = {
    "Name" = "tf-app-route-table-1a"
  }
}

resource "aws_route_table_association" "app_route_table_association_1c" {
  subnet_id = aws_subnet.app_private_subnet_1c.id
  route_table_id = aws_route_table.app_route_table_1c.id
}
```

# 1.6 Security Group
セキュリティグループは仮想ファイアウォールとして機能し、関連付けられたリソースから送信、およびリソースに受信されるトラフィックを制御する。
Security Groupは、作成されたVPC内でのみ適応できる。

> セキュリティグループはステートフルです。例えば、インスタンスからリクエストを送信した場合、そのリクエストのレスポンストラフィックは、インバウンドセキュリティグループのルールに関係なく、インスタンスに到達が許可されます。許可されたインバウンドトラフィックへのレスポンスは、アウトバウンドルールに関係なく、インスタンスを離れることができます
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_SecurityGroups.html#VPCSecurityGroups
許可ルールを指定できます。拒否ルールは指定できません。

```terraform
### Security Group ###
## application ##
resource "aws_security_group" "app_sg" {
  name        = "app-sg"
  description = "Application Secuirty Group"
  vpc_id      = aws_vpc.tf_vpc.id
  tags = {
    Name = "tf-app-sg"
  }
}

resource "aws_security_group_rule" "allow_alb_sg_inbound" {
  type                     = "ingress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  security_group_id        = aws_security_group.app_sg.id
  source_security_group_id = aws_security_group.alb_sg.id
}

resource "aws_security_group_rule" "allow_every_outbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  security_group_id = aws_security_group.app_sg.id
  cidr_blocks       = ["0.0.0.0/0"]
}

## ALB ##
resource "aws_security_group" "alb_sg" {
  name        = "alb-sg"
  description = "ALB Secuirty Group"
  vpc_id      = aws_vpc.tf_vpc.id
  tags = {
    Name = "tf-alb-sg"
  }
}

resource "aws_security_group_rule" "allow_http_inbound" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  security_group_id = aws_security_group.alb_sg.id
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_security_group_rule" "allow_app_sg_egress" {
  type                     = "egress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  security_group_id        = aws_security_group.alb_sg.id
  source_security_group_id = aws_security_group.app_sg.id
}
```

# 2. ECS on Fargate
とりあえず、 ロードバランサーや継続的デリバリーなどを気にせず単純にコンテナを起動させる。

## 2.1 Cluster

```terraform
### ECS Cluster ###
resource "aws_ecs_cluster" "terafform_cluster" {
  name = "terraform_cluster"
}

resource "aws_ecs_cluster_capacity_providers" "provider" {
  cluster_name       = aws_ecs_cluster.terafform_cluster.name
  capacity_providers = ["FARGATE", "FARGATE_SPOT"]
}
```

[ECSで使用できるCapacity Provider](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html)

## 2.2 Cloud Watch Log Group
Container上で発生するログを記録する保存場所

```terraform
### Cloud Watch Log Group ###
resource "aws_cloudwatch_log_group" "nginx" {
  name              = "/ecs/logs/terraform/nginx"
  retention_in_days = 1
}
```

## 2.3 IAM Role
上記でCloud Watch Logをログ記録用に作成したり、Docker ImageをPullしたりするときに、Cloud Watch や　ECRにアクセスするための権限が必要になる。
この権限をECS Container Agentに付与しなければならない。

```terraform
### IAM Role ###
resource "aws_iam_role" "ecs_task_execution_role" {
  assume_role_policy = jsonencode(
    {
      Statement = [
        {
          Action = "sts:AssumeRole"
          Effect = "Allow"
          Principal = {
            Service = "ecs-tasks.amazonaws.com"
          }
          Sid = ""
        },
      ]
      Version = "2012-10-17"
    }
  )
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
  ]
  description = "Allows ECS tasks to call AWS services on your behalf."
  name        = "ecsTaskExecutionRole"
  path        = "/"
}
```
**Task Execution Role**
> The task execution role grants the Amazon ECS container and Fargate agents permission to make AWS API calls on your behalf. The task execution IAM role is required depending on the requirements of your task.


https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html

**Task Role**
> Your Amazon ECS tasks can have an IAM role associated with them. The permissions granted in the IAM role are assumed by the containers running in the task.

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html

ここで使用されるassumedとは引き受けるの意味。（assume roleとかの）
「タスクの中で起動しているコンテナに引き継がれる」

--- 
Task Execution RoleはFargate Agentが他のAWS Serviceへリクエストする際の権限
Task Roleは、コンテナ内で実行されるApplicationが他のAWS Serviceへリクエストする権限
を担う。

Task Execution RoleにCloud Watch Logへのリクエスト権限が必要なことから、ログの送信はコンテナ内部で行われているのではなく、コンテナ内部のログをFargate Agentなどが受け取ってそれをCloud Watch Logに送信しているのかな？？

https://dev.classmethod.jp/articles/ecs_fargate_iamrole/

# 2.4 Task Definition
タスク定義。
今回は、nginxを起動するコンテナ１つを作成。

```terraform
### ECS Task Definition
resource "aws_ecs_task_definition" "nginx" {
  family                   = "nginx"
  container_definitions    = file("./container_definitions.json")
  cpu                      = 256
  memory                   = 512
  network_mode             = "awsvpc"
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  requires_compatibilities = ["FARGATE"]
  tags = {
    "Name" = "terraform"
  }
}
```

```json:container_definition.json
[
  {
    "name": "nginx",
    "image": "nginx:latest",
    "cpu": 256,
    "essential": true,
    "memory": 128,
    "portMappings": [
      {
        "containerPort": 80,
        "hostPort": 80
      }
    ],
    "LogConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/logs/terraform/nginx",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "terraform"
      }
    }
  }
]
```


## 2.5 ECS Service
今回は節約のために FARGATE_SPOTを採用しています。
ECS Serviceで使用するパラメータは結構多いので、キャパシティプロバイダーなどの説明も後で記事にまとめたいと思っています

```terraform
### ECS Service ###
resource "aws_ecs_service" "tf_ecs_service" {
  name = "terraform_ecs"
  capacity_provider_strategy {
    base              = 1
    capacity_provider = "FARGATE_SPOT"
    weight            = 1
  }
  cluster       = aws_ecs_cluster.terafform_cluster.id
  desired_count = 1
  network_configuration {
    subnets = [
      aws_subnet.app_private_subnet_1a.id,
      aws_subnet.app_private_subnet_1c.id
    ]
    security_groups  = [aws_security_group.app_sg.id]
    assign_public_ip = false # private subnetに配置しているため
  }
  task_definition = aws_ecs_task_definition.nginx.arn
  tags = {
    "Name" = "terraform"
  }

  lifecycle {
    ignore_changes = [
      task_definition, # デプロイ毎にタスク定義は変更されるため
      desired_count,
    ]
  }
}
```

--- 
ここまででFargate上でnginxを起動できていると思います。
しかし、private subnetに配置しているため、インターネット上からのアクセスはできないので、ALBを設定していきます。

# 3. Load Balancer
ロードバランサーも結構説明したいことがあるのですが、結構長くなってしまうので別記事にて・・・

## 3.1 ALB
とりあえず、何も設定されていないロードバランサー自体をパブリックサブネットに配置。

```terraform
### ALB ###
resource "aws_alb" "alb" {
  name               = "terraform-test-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets = [
    aws_subnet.alb_public_subnet_1a.id,
    aws_subnet.alb_public_subnet_1c.id,
  ]
}
```

## 3.2 Target Group
Load　Balancerで受け取ったリクエストが送られる対象のグループ。


```terraform
resource "aws_lb_target_group" "alb_target_group" {
  name        = "tf-test-tg"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.tf_vpc.id
  health_check {
    enabled  = true
    path     = "/"
    port     = 80
    protocol = "HTTP"
  }
}
```

【TODO】
health_checkで指定しているprotocol/portと aws_lb_target_groupで指定しているprotocol/portの違いについてまとめる

## 3.3 ALB Listner
Load Balancerが受け取ったリクエストの内容を吟味し、あるルールを満たしたらこのターゲットグループに送る、というような指定をするもの。

今回は、protocol: HTTP, port: 80 のリクエストは、上記で作成したターゲットグループに送信する設定をしている。

```terraform
resource "aws_lb_listener" "alb_listner" {
  load_balancer_arn = aws_alb.alb.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_target_group.arn
  }
}
```

## 3.4 ECS Service

```terraform
resource "aws_ecs_service" "tf_ecs_service" {
  ...
  load_balancer {
    target_group_arn = aws_lb_target_group.alb_target_group.arn
    container_name   = "nginx"
    container_port   = 80
  }
  ...
}
```
2. で作成していたECS Serviceにロードバランサーに関する項目を追加する。
この記述によりECS Serviceによって実行されたコンテナが指定されているターゲットグループに登録される。

--- 
漏れがなければ以上の記述で、 `terraform apply`　を実行させることにより、nginxの初期画面にインターネットからアクセスできると思います。

【追記予定】
- CodePipline実装
- B/G Deployment実装