terraform で ECS Fargateキャパシティープロパイダーを意識しつつECSクラスターを構築する

この記事については、
https://zenn.dev/kawahara/articles/309d05be8bc6c3
にもっと新しい情報を載せています。

---

ECSにはデフォルトで2つのキャパシティープロパイダーが用意されており、1つは通常の `FARGATE` で、もうひとつは `FARGATE_SPOT` です。
`FARGATE_SPOT` により起動されたタスクは、中断されてしまうリスクはありますが、お値段が7割引となっており、 autoscaling などと合わせてうまく使わない手はありません。

- [AWS Fargate の料金](https://aws.amazon.com/jp/fargate/pricing/)

この記事では、terraform により、Fargateキャパシティープロパイダーを意識してECSクラスターを構築する方法について考えたいと思います。

## サンプルコードについて

この記事のためにサンプルコードを用意しました。
https://github.com/kawahara/ecs-fargate-autoscale

このサンプルコードは、VPC, ALB, NATゲートウェイなどを含めて、動く一歩手前のところまでを構築するものです。
他にも多くのサンプルはありますが、Single AZ, Public Subnet のみの例が多く、
今回は、複数のAZに展開し、Fargateにより展開するアプリケーションを Private Subnet 上に展開します。
(ちょっとサボってて、NATゲートウェイが Single AZ なのはご了承ください。。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/41d4cd71-9bf6-fe15-bb94-90c40bd5fb00.png)


また、複数環境に対応するために、 [workspace](https://www.terraform.io/docs/state/workspaces.html)を利用する前提をしております。

terraform のバージョンは 0.12.29 を利用しております。

## サンプルコードでの環境構築

特に、workspace を指定しない場合 (`default` という名前がそれぞれのリソースに付きます) は、以下のような手順で環境を構築できます。
事前に、以下の環境が揃っているものとします。

- dockerのインストール
- terraform 0.12.29 のインストール
- aws cli によるプロファイル設定

### AWS Systems Manager (SSM) によるパラメータの設定

後述していますが、環境変数の割り当てを確認するため、このサンプルコードではSSMを利用して情報を取得しています。
このため、事前に手動で環境変数を設定する必要があります。

AWSのコンソールから、AWS Systems Manager に行き、「パラメータストア」を選びます。
そこから、「パラメータの作成」を選び、名前を `/my-sample-app/default/sample` として、
タイプを 「安全な文字列」を選び、値に適当なものを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/c9e99473-8a99-030d-c395-7e9b885bd9c4.png)

### KMSのARNを探す

次にAWSコンソールから、Key Management Service に行き、「AWSマネージド型キー」を選びます。
エイリアスが、`aws/ssm` となっているものを探し、そのキーのARNをコピーしておきます。

この情報は先程SSMに保存した暗号化された値を、復号するために利用されます。
このため、このARNは環境構築上必要です。

### 適用


```
terraform init
AWS_DEFAULT_REGION='ap-northeast-1' TF_VAR_ssm_kms_arn='SSMに利用しているKMSキーのARN' terraform apply
```

TF_VAR_ssm_kms_arn については、`terraform.tfvars` を作成し、以下の内容を記載することで省略ができます。

```
ssm_kms_arn=SSMに利用しているKMSキーのARN
```

### Docker image の push 

構築した直後では、Docker image がまだpushされていないため、ECSでタスクを開始することができません。
サンプルコードでは、PHPスクリプトで `phpinfo();` を表示するための、シンプルな `Dockerfile` を用意しています。

push の手順は、AWSコンソール上のECRリポジトリの画面に記載されています。

```
cd docker
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxx.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t my-sample-app/default/server .
docker tag my-sample-app/default/server:latest xxx.dkr.ecr.ap-northeast-1.amazonaws.com/my-sample-app/default/server:latest
docker push xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/my-sample-app/default/server:latest
```


### ALBからの動作確認

今回の環境では、Private Subnet 側にタスクを展開しているため、外からは個別の環境にアクセスすることができません。
ALBからリクエストして確認します。

AWSコンソール上のECSクラスターのタスクから、タスクの状態が RUNNING になっていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/f8a91a05-1bd6-6cdb-0332-4f9f7c6bc257.png)



その上で、AWSコンソールからEC2に行き、ロードバランサーの設定を見ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/e878db13-96e1-6c7d-3b95-139e0cb036fb.png)


ブラウザーから記載されているアドレス(DNS名) にアクセスして、 `/sample.php` が正しく表示されることを確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/a845638a-37a1-1913-9b1d-1475ae6afa2b.png)

2タスク動いているので、リロードするとホストが変化するのがわかるかと思います。

また、SSMによって設定されている、環境変数の値もバッチリ設定されていますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/d1c81313-d823-43e3-e2a4-f99b19d12796.png)


### 削除

実験が終わった時にリソースを削除するには、単純に `destroy` を実行して削除できます。

```
AWS_DEFAULT_REGION='ap-northeast-1' TF_VAR_ssm_kms_arn='SSMに利用しているKMSキーのARN' terraform destroy
```

以降、今回のサンプルコードで注目するポイントについて解説します。

## サービスのキャパシティープロパイダーの設定

`aws_ecs_service` リソースの作成時に、`capacity_provider_strategy` の `weight` を指定することで、通常のFargateとFargate spot での起動するタスクの割合を調整することができます。
例えば、1:1 のときに、4タスク起動する場合は、2タスクは `FARGATE` で、残り2つのタスクは `FARGATE_SPOT` により起動されます。
`capacity_provider_strategy` では、 `base` も指定することができますが、2020年8月現在では、
`run-task` の場合にのみ利用されるもので、サービスからタスクを起動する場合は `weight` のみが参照されます。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cluster-capacity-providers.html

>  キャパシティープロバイダー戦略を指定する場合、base 値はタスクの実行時にのみサポートされます。サービスを作成する場合、キャパシティープロバイダー戦略 base パラメータはサポートされていません。


今現在、 `capacity_provider_strategy` については、terraform 上で変更してしまうと、 service が更新ではなく作り直しになってしまう問題があるので、`ignore_changes` を設定しています。
この値を構築後に変更する場合は、コンソール上のサービスから更新するか、CLIを使う必要がありそうです。

https://github.com/terraform-providers/terraform-provider-aws/issues/11351


```
resource "aws_ecs_service" "service" {
  name            = "my-sample-app-${terraform.workspace}"
  cluster         = aws_ecs_cluster.cluster.id
  task_definition = aws_ecs_task_definition.task.arn
  desired_count   = 0

  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    base              = 0
    weight            = var.ecs_cluster.fargate_weight
  }

  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    base              = 0
    weight            = var.ecs_cluster.fargate_spot_weight
  }

  ...

  lifecycle {
    ignore_changes = [
      desired_count,
      task_definition,
      # NOTE: https://github.com/terraform-providers/terraform-provider-aws/issues/11351
      capacity_provider_strategy
    ]
  }
}
```

## ちょっと実験: キャパシティープロパイダー の weight による挙動

### どっちのキャパシティープロパイダーで起動したのかの確認

クラスター -> タスク から確認できるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/a46d05ff-fb25-16cf-5e88-bd45abf76edd.png)

コンソール画面上には現状、どの起動タイプで起動したかを一覧でみる画面がないのがやや不便ですね。
weight の設定で、タスクがどのように起動するのかを観察してみます。

### FARGATE:FARGATE_SPOT = 1:1 で3台起動する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/dfb35203-c123-c8e3-7e12-c3e35eafc505.png)

Fargate 2台、Fargate Spot 1台で起動しました。
サンプルコードでは、デフォルトでこの設定を利用しています。

### FARGATE_SPOT:FARGATE = 1:1 で3台起動する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/5144b376-e148-087a-1001-c61cb41246df.png)

Fargete Spot2台、Fargate1台で起動しました。
したがって、記載順序は大いに関係するようです。
タスクの数が奇数の場合にFargate Spot を多めに起動しつつ、Fargateも起動しておきたい場合は、
この順序で設定すると良さそうです。

### FARGATE_SPOT:FARGATE = 1:0 で3台起動する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61997/6f00853f-b547-ea81-58f8-c345e51b2e8e.png)


この場合、すべてのタスクはFargate Spot で起動します。
本番環境でなければこれで問題ないかもしれません。

【参考にした記事】

- [ECS Capacity Auto ScalingをTerraformで実装する - Sansan Builders Blog](https://buildersbox.corp-sansan.com/entry/2020/01/28/110000)

## Fargate + autoscaling

CPU利用率が高くなってしまった場合は、サービスの必要タスク数を増やすして、スケールアウトする設定をしましょう。
サンプルコードでは、CPU使用率が75%を超えた場合に、desired count を増やす設定をしています。

スケールアウトだけでなく、CPU使用率が低いときに desired count を減らす設定もしておきましょう。

```
# オートスケールターゲット
resource "aws_appautoscaling_target" "ecs" {
  service_namespace  = "ecs"
  resource_id        = "service/${aws_ecs_cluster.cluster.name}/${aws_ecs_service.service.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  min_capacity       = var.ecs_autoscale.min
  max_capacity       = var.ecs_autoscale.max
}

# スケールアウトを行う設定
resource "aws_appautoscaling_policy" "out" {
  name               = "my-sample-app-out-${terraform.workspace}"
  resource_id        = "service/${aws_ecs_cluster.cluster.name}/${aws_ecs_service.service.name}"
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 300
    metric_aggregation_type = "Average"

    step_adjustment {
      metric_interval_upper_bound = 0
      scaling_adjustment          = 1
    }
  }

  depends_on = [aws_appautoscaling_target.ecs]
}

# CPU使用率によるアラーム設定
resource "aws_cloudwatch_metric_alarm" "out" {
  alarm_name          = "my-sample-app-${terraform.workspace}-ecs-cpu-gt-75"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "1"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "300"
  statistic           = "Average"
  threshold           = "75"

  dimensions = {
    ClusterName = aws_ecs_cluster.cluster.name
    ServiceName = aws_ecs_service.service.name
  }

  alarm_actions = [aws_appautoscaling_policy.out.arn]
}

```

【参考にした記事】

- [AWS FargateをTerraformで構築する](https://qiita.com/takeshi_hirosue/items/90051144aae8bb5a646f)


## SSMから環境変数に設定する値を取得する

本題とは関係が無いですが、パスワード情報などの機密性の高い情報については、
AWS Systems Manager (SSM) のパラメータストアを利用して環境変数に設定しています。

SSMからデータを取得してタスクを起動する場合は、タスク定義のタスク起動ロールで、
SSM利用と、暗号キー利用の権限が必要です。

今回は、SSMにおけるパラメーター名を、`/my-sample-app/${環境名}/${パラメータ名}` としたため、
`/my-sample-app/${環境名}/*` をについて、`ssm:GetParameters` を許可するようにします。

以下のようにIAMロールを作成する設定を行いました。

```
resource "aws_iam_role" "execution" {
  name = "execution"
  path = "/my-sample-app/${terraform.workspace}/ecs/"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ExecutionRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

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

data "aws_iam_policy_document" "ecs_get_ssm" {
  statement {
    sid = "KSMDecrypt"
    actions = [
      "kms:Decrypt"
    ]
    resources = [
      var.ssm_kms_arn
    ]
  }

  statement {
    sid = "SSMGetParameters"
    actions = [
      "ssm:GetParameters"
    ]
    resources = [
      "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/my-sample-app/${terraform.workspace}/*"
    ]
  }
}

resource "aws_iam_policy" "ecs_execution_ssm" {
  name        = "my-sample-app-execution-ssm-${terraform.workspace}"
  description = "read permission to fetch ssm secret params"
  policy      = data.aws_iam_policy_document.ecs_get_ssm.json
}
```

ここで作成したIAMロールを、タスク定義で利用しています。

【参考にした記事】

- [ECSのコンテナ定義からパラメータストアが参照できるようになった！](https://qiita.com/hayao_k/items/e34c706464d562d8b8f0)