Terraform＋AWS Fargateで、タスク定義を更新（ローリング更新）してみる

# What's？

* AWS Fargateに対するデプロイ方法は3種類あり、そのうちローリング更新を試してみたい
* 環境と更新操作は、Terraformで行う

というのをやってみたいという記事です。


# AWS Fargateでのデプロイ方法

Fargateというか、ECSでのデプロイ方法ですが。

ドキュメントによると、3つがあるようです。

[Amazon ECS デプロイタイプ](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/deployment-types.html)

* ローリング更新
* CodeDeployを使用したBlue/Greenデプロイ
* 外部デプロイ

AWS内でやろうとすると、ローリング更新かBlue/Greenデプロイ（CodeDeploy利用）の2つになるということですね。


# ローリング更新

ローリング更新に関するドキュメントはこちら、ですが、あんまり書いてないですね…。

[ローリング更新](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/deployment-type-ecs.html)

AWSのマネージメントコンソールでECSサービスを作成する時に、ローリング更新に関する記述があるのはドキュメントのうち次の2つのページです。

[基本的なサービスパラメータの設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/basic-service-params.html)

> [ローリング更新] デプロイタイプを使用している場合は、次のデプロイ設定パラメータを入力します。これらのパラメータの使用方法の詳細については、「デプロイ設定」を参照してください。
>
> * Minimum healthy percent: デプロイ時に RUNNING 状態に保つ必要のあるサービスのタスクの下限数をサービスのタスクの必要数のパーセント値 (最も近い整数に切り上げ) で指定します。
>
> * Maximum percent: デプロイ時に RUNNING または PENDING 状態にできるサービスのタスクの上限数をサービスの必要数のタスクのパーセント値 (最も近い整数に切り下げ) で指定します。

[ローリング更新のデプロイタイプのロードバランサーを設定する](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-create-loadbalancer-rolling.html)

こちらは、主にヘルスチェックに関する話ですね。

ローリング更新に関するパラメーターのリファレンスは、こちらを見ることになります。

[サービス定義パラメーター / デプロイ設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/service_definition_parameters.html#sd-deploymentconfiguration)

`deploymentController`をデフォルトの`ECS`とした場合、ローリング更新になるということですね。

[サービス定義パラメーター / デプロイメントコントローラー](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/service_definition_parameters.html#sd-deploymentcontroller)

ドキュメントを読んでいくと、ローリング更新というのはサービス内のタスクを順次更新していくもののようです。

どのくらいのタスクを残して更新していくかは、`minimumHealthyPercent`と`maximumPercent`で調整することになります。


# 環境

利用するTerraformと、AWS Providerのバージョンです。

```shell
$ terraform version
Terraform v0.12.26
+ provider.aws v2.65.0
```


# 構築内容

まっさらなところから、VPCを作ってAWS Fargateクラスタを構築、簡単な動作確認を行うところまでやります。

* マルチAZ
* 動作させるコンテナは、nginxとApache（ローリングアップデートで切り替える）
* ALBはHTTP


# TerraformとAWS Providerのバージョン指定

こんな感じで。

```shell
terraform {
  required_version = "=  0.12.26"
}

provider "aws" {
  version = "2.65.0"
}
```

AWSのクレデンシャルは、環境変数で指定します。

```shell
export AWS_ACCESS_KEY_ID=.....
export AWS_SECRET_ACCESS_KEY=.....
export AWS_DEFAULT_REGION=.....
```


# VPC〜ALBまで

このあたりは、コミュニティモジュールを使って作っていきます。


VPC。

[AWS VPC Terraform module](https://github.com/terraform-aws-modules/terraform-aws-vpc)

```golang
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.39.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
}
```

ALBとFargateに付与する、Security Group。

[AWS EC2-VPC Security Group Terraform module](https://github.com/terraform-aws-modules/terraform-aws-security-group)

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

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

  ingress_cidr_blocks = ["0.0.0.0/0"]
}

module "http_server_sg" {
  source  = "terraform-aws-modules/security-group/aws//modules/http-80"
  version = "3.10.0"

  name   = "http-server-sg"
  vpc_id = module.vpc.vpc_id

  ingress_cidr_blocks = ["10.0.0.0/16"]
}
```

ALB。

[AWS Application and Network Load Balancer (ALB & NLB) Terraform module](https://github.com/terraform-aws-modules/terraform-aws-alb)

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

  name = "http-server"

  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     = 80
      target_type      = "ip"

      health_check = {
        interval = 20
      }
    }
  ]

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


# ここまでのモジュール定義のOutputの利用と、Fargateでのコンテナ定義

ここまでで使用したモジュールのOutputのうち、この後のFargateで使うものをローカル変数としてまとめておきます。

対象は、VPC ID、プライベートサブネットのID、Fargate用のセキュリティグループ、そしてALBのターゲットグループです。

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

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

  nginx_container_definition = <<JSON
[
  {
    "name": "http-server",
    "image": "nginx:1.19.0",
    "essential": true,
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ],
    "healthCheck": {
      "command": [
        "CMD-SHELL",
        "curl -f localhost || exit 1"
      ],
      "interval": 5
    }
  }
]
  JSON

  apache_container_definition = <<JSON
[
  {
    "name": "http-server",
    "image": "httpd:2.4.43",
    "essential": true,
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ],
    "healthCheck": {
      "command": [
        "CMD-SHELL",
        "test -e /proc/1"
      ],
      "interval": 5
    }
  }
]
  JSON
}
```

また、コンテナ定義も2つ用意しておき、こちらを変更することでFargateのサービス内で稼働するコンテナイメージを切り替えていきます。

ローリング更新と言いつつも、今回はnginxとApacheを切り替えることにしました。

コンテナのヘルスチェックですが、Apacheのイメージには`curl`が入っていなかったので、だいぶ微妙な感じになっていますが…。

コンテナのヘルスチェックの設定については、こちらを参照。

[ヘルスチェック](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_healthcheck)


# Fargateクラスタの定義

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

* クラスタ定義
* タスク定義
* サービス定義

以上を行います。

```golang
resource "aws_ecs_cluster" "http_server" {
  name = "http-server-cluster"
}

resource "aws_ecs_task_definition" "http_server" {
  family                   = "http-server-task-definition"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]

  container_definitions = local.nginx_container_definition
  #container_definitions = local.apache_container_definition
}

resource "aws_ecs_service" "http_server" {
  name             = "http-server-service"
  cluster          = aws_ecs_cluster.http_server.arn
  task_definition  = aws_ecs_task_definition.http_server.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   = "http-server"
    container_port   = 80
  }
}
```

ローリング更新を行う場合、`deployment_controller`の`type`は`ECS`にする必要があります。といっても、これはデフォルト値のようですが。

```golang
  deployment_controller {
    type = "ECS" # default
  }
```

`minimumHealthyPercent`は、50％に設定。

ここ、注意ですね。

> レプリカサービスの minimumHealthyPercent のデフォルト値は 100% です。DAEMON サービススケジュールを使用しているデフォルトの minimumHealthyPercent 値は、AWS CLI、AWS SDK、API では 0%、AWS マネジメントコンソール では 50% です。

[デプロイ設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/service_definition_parameters.html#sd-deploymentconfiguration)

タスク定義は、まずはnginxを利用。

```golang
resource "aws_ecs_task_definition" "http_server" {
  family                   = "http-server-task-definition"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]

  container_definitions = local.nginx_container_definition
  #container_definitions = local.apache_container_definition
}
```


# 確認

まずは、`terraform apply`。

```shell
$ terraform apply -auto-approve
```

`curl`で1度確認してみます。

```shell
$ curl [ALBのDNS名]
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

nginxコンテナのレスポンスが返ってきました。

この時に、`ecs-cli ps`でコンテナの一覧を見てみます。

```shell
$ ecs-cli ps --cluster http-server-cluster --desired-status RUNNING
Name                                              State    Ports                  TaskDefinition                  Health
2a3da558-0dc6-4d26-9304-a5370520a4a4/http-server  RUNNING  10.0.1.226:80->80/tcp  http-server-task-definition:12  HEALTHY
6b6f5927-80b9-46ec-a6ff-8c4c85553804/http-server  RUNNING  10.0.2.220:80->80/tcp  http-server-task-definition:12  HEALTHY
8f0d5f0d-7321-4b75-9bea-b1423260f170/http-server  RUNNING  10.0.1.19:80->80/tcp   http-server-task-definition:12  HEALTHY
a6930cca-75ca-474e-9033-bfb1d889d14f/http-server  RUNNING  10.0.1.193:80->80/tcp  http-server-task-definition:12  HEALTHY
d4b0c60c-737c-42b3-9a74-4f1b7a5ae013/http-server  RUNNING  10.0.2.79:80->80/tcp   http-server-task-definition:12  HEALTHY
```

では、ここでコンテナ定義をnginxからApacheに変更してみます。

```golang
resource "aws_ecs_task_definition" "http_server" {
  family                   = "http-server-task-definition"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]

  #container_definitions = local.nginx_container_definition
  container_definitions = local.apache_container_definition
}
```

```shell
$ terraform apply -auto-approve
```

すると、次のコンテナが立ち上がり始めます。

```shell
$ ecs-cli ps --cluster http-server-cluster
Name                                              State                Ports                  TaskDefinition                  Health
2a3da558-0dc6-4d26-9304-a5370520a4a4/http-server  RUNNING              10.0.1.226:80->80/tcp  http-server-task-definition:12  HEALTHY
3efbc3b2-6618-4ebd-86d2-138ea0b75529/http-server  PENDING              10.0.1.203:80->80/tcp  http-server-task-definition:13  UNKNOWN
6b6f5927-80b9-46ec-a6ff-8c4c85553804/http-server  RUNNING              10.0.2.220:80->80/tcp  http-server-task-definition:12  HEALTHY
7c0694c2-21aa-49a0-b0dd-04da880bfd34/http-server  PENDING              10.0.1.85:80->80/tcp   http-server-task-definition:13  UNKNOWN
8f0d5f0d-7321-4b75-9bea-b1423260f170/http-server  RUNNING              10.0.1.19:80->80/tcp   http-server-task-definition:12  HEALTHY
a6930cca-75ca-474e-9033-bfb1d889d14f/http-server  RUNNING              10.0.1.193:80->80/tcp  http-server-task-definition:12  HEALTHY
bd842250-6b40-4b5c-91fa-9cdaf2c7d30c/http-server  PENDING              10.0.2.205:80->80/tcp  http-server-task-definition:13  UNKNOWN
d4b0c60c-737c-42b3-9a74-4f1b7a5ae013/http-server  RUNNING              10.0.2.79:80->80/tcp   http-server-task-definition:12  HEALTHY
de437ee4-afbd-4ef1-95eb-3d9eeb0f189d/http-server  PENDING              10.0.2.65:80->80/tcp   http-server-task-definition:13  UNKNOWN
e684fa00-e7cb-4765-966b-92b15f1e008f/http-server  PENDING              10.0.2.239:80->80/tcp  http-server-task-definition:13  UNKNOWN
```

`curl`の方ではnginxとApacheのレスポンスが交互に表示されたりもしますが、そのうちApacheになります。

```shell
$ curl [ALBのDNS名]
<html><body><h1>It works!</h1></body></html>
```

`ecs-cli ps`の結果。

```shell
$ ecs-cli ps --cluster http-server-cluster
Name                                              State                Ports                  TaskDefinition                  Health
2a3da558-0dc6-4d26-9304-a5370520a4a4/http-server  RUNNING              10.0.1.226:80->80/tcp  http-server-task-definition:12  HEALTHY
3efbc3b2-6618-4ebd-86d2-138ea0b75529/http-server  RUNNING              10.0.1.203:80->80/tcp  http-server-task-definition:13  HEALTHY
6b6f5927-80b9-46ec-a6ff-8c4c85553804/http-server  RUNNING              10.0.2.220:80->80/tcp  http-server-task-definition:12  HEALTHY
7c0694c2-21aa-49a0-b0dd-04da880bfd34/http-server  RUNNING              10.0.1.85:80->80/tcp   http-server-task-definition:13  HEALTHY
8f0d5f0d-7321-4b75-9bea-b1423260f170/http-server  RUNNING              10.0.1.19:80->80/tcp   http-server-task-definition:12  HEALTHY
a6930cca-75ca-474e-9033-bfb1d889d14f/http-server  RUNNING              10.0.1.193:80->80/tcp  http-server-task-definition:12  HEALTHY
bd842250-6b40-4b5c-91fa-9cdaf2c7d30c/http-server  RUNNING              10.0.2.205:80->80/tcp  http-server-task-definition:13  HEALTHY
d4b0c60c-737c-42b3-9a74-4f1b7a5ae013/http-server  RUNNING              10.0.2.79:80->80/tcp   http-server-task-definition:12  HEALTHY
de437ee4-afbd-4ef1-95eb-3d9eeb0f189d/http-server  RUNNING              10.0.2.65:80->80/tcp   http-server-task-definition:13  HEALTHY
e684fa00-e7cb-4765-966b-92b15f1e008f/http-server  RUNNING              10.0.2.239:80->80/tcp  http-server-task-definition:13  HEALTHY
```

さらにしばらく待っていると、nginx側のコンテナが全部終了します。

```shell
$ ecs-cli ps --cluster http-server-cluster
3efbc3b2-6618-4ebd-86d2-138ea0b75529/http-server  RUNNING              10.0.1.203:80->80/tcp  http-server-task-definition:13  HEALTHY
7c0694c2-21aa-49a0-b0dd-04da880bfd34/http-server  RUNNING              10.0.1.85:80->80/tcp   http-server-task-definition:13  HEALTHY
bd842250-6b40-4b5c-91fa-9cdaf2c7d30c/http-server  RUNNING              10.0.2.205:80->80/tcp  http-server-task-definition:13  HEALTHY
de437ee4-afbd-4ef1-95eb-3d9eeb0f189d/http-server  RUNNING              10.0.2.65:80->80/tcp   http-server-task-definition:13  HEALTHY
e684fa00-e7cb-4765-966b-92b15f1e008f/http-server  RUNNING              10.0.2.239:80->80/tcp  http-server-task-definition:13  HEALTHY
2a3da558-0dc6-4d26-9304-a5370520a4a4/http-server  STOPPED ExitCode: 0  80->80/tcp             http-server-task-definition:12  HEALTHY
6b6f5927-80b9-46ec-a6ff-8c4c85553804/http-server  STOPPED ExitCode: 0  80->80/tcp             http-server-task-definition:12  HEALTHY
8f0d5f0d-7321-4b75-9bea-b1423260f170/http-server  STOPPED ExitCode: 0  80->80/tcp             http-server-task-definition:12  HEALTHY
a6930cca-75ca-474e-9033-bfb1d889d14f/http-server  STOPPED ExitCode: 0  80->80/tcp             http-server-task-definition:12  HEALTHY
d4b0c60c-737c-42b3-9a74-4f1b7a5ae013/http-server  STOPPED ExitCode: 0  80->80/tcp             http-server-task-definition:12  HEALTHY
```

簡単にですが、確認できた感じですね。