この記事については、
https://zenn.dev/kawahara/articles/309d05be8bc6c3
にもっと新しい情報を載せています。
ECSにはデフォルトで2つのキャパシティープロパイダーが用意されており、1つは通常の FARGATE
で、もうひとつは FARGATE_SPOT
です。
FARGATE_SPOT
により起動されたタスクは、中断されてしまうリスクはありますが、お値段が7割引となっており、 autoscaling などと合わせてうまく使わない手はありません。
この記事では、terraform により、Fargateキャパシティープロパイダーを意識してECSクラスターを構築する方法について考えたいと思います。
サンプルコードについて
この記事のためにサンプルコードを用意しました。
https://github.com/kawahara/ecs-fargate-autoscale
このサンプルコードは、VPC, ALB, NATゲートウェイなどを含めて、動く一歩手前のところまでを構築するものです。
他にも多くのサンプルはありますが、Single AZ, Public Subnet のみの例が多く、
今回は、複数のAZに展開し、Fargateにより展開するアプリケーションを Private Subnet 上に展開します。
(ちょっとサボってて、NATゲートウェイが Single AZ なのはご了承ください。。)
また、複数環境に対応するために、 workspaceを利用する前提をしております。
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
として、
タイプを 「安全な文字列」を選び、値に適当なものを設定します。
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 になっていることを確認します。
その上で、AWSコンソールからEC2に行き、ロードバランサーの設定を見ます。
ブラウザーから記載されているアドレス(DNS名) にアクセスして、 /sample.php
が正しく表示されることを確認しましょう。
2タスク動いているので、リロードするとホストが変化するのがわかるかと思います。
また、SSMによって設定されている、環境変数の値もバッチリ設定されていますね。
削除
実験が終わった時にリソースを削除するには、単純に 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
のみが参照されます。
キャパシティープロバイダー戦略を指定する場合、base 値はタスクの実行時にのみサポートされます。サービスを作成する場合、キャパシティープロバイダー戦略 base パラメータはサポートされていません。
今現在、 capacity_provider_strategy
については、terraform 上で変更してしまうと、 service が更新ではなく作り直しになってしまう問題があるので、ignore_changes
を設定しています。
この値を構築後に変更する場合は、コンソール上のサービスから更新するか、CLIを使う必要がありそうです。
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 による挙動
どっちのキャパシティープロパイダーで起動したのかの確認
クラスター -> タスク から確認できるようです。
コンソール画面上には現状、どの起動タイプで起動したかを一覧でみる画面がないのがやや不便ですね。
weight の設定で、タスクがどのように起動するのかを観察してみます。
FARGATE:FARGATE_SPOT = 1:1 で3台起動する
Fargate 2台、Fargate Spot 1台で起動しました。
サンプルコードでは、デフォルトでこの設定を利用しています。
FARGATE_SPOT:FARGATE = 1:1 で3台起動する
Fargete Spot2台、Fargate1台で起動しました。
したがって、記載順序は大いに関係するようです。
タスクの数が奇数の場合にFargate Spot を多めに起動しつつ、Fargateも起動しておきたい場合は、
この順序で設定すると良さそうです。
FARGATE_SPOT:FARGATE = 1:0 で3台起動する
この場合、すべてのタスクはFargate Spot で起動します。
本番環境でなければこれで問題ないかもしれません。
【参考にした記事】
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]
}
【参考にした記事】
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ロールを、タスク定義で利用しています。
【参考にした記事】