メインコンテンツまでスキップ

ECS 前編: Fargateでコンテナ実行基盤を整える

前章で、TasukuのALBはHTTPSの入口を持ちました。ただし443番のリスナーは固定文字列を返すだけで、その奥にはまだ何も実体がありません。この章では、リクエストを実際に処理するコンテナの実行基盤を作ります。ECSのクラスタとタスク定義、イメージの置き場となるECRリポジトリ、コンテナに権限を与える2種類のIAMロール、ログの送り先となるCloudWatch Logsです。ALBとの接続や常時稼働するサービス化は次の章に譲り、この章では単発のタスクを起動して基盤が機能することを確かめるところまでを扱います。

8.1. ECSの基本概念: クラスタ・タスク定義・タスク・サービスの整理

ECS (Elastic Container Service) は、コンテナの実行基盤です。4つの概念の関係を先に整理します。

  • タスク定義: コンテナの構成を記述するJSON形式の設計図です。使うイメージ、CPU・メモリ、ログの送り先などを指定します
  • タスク: タスク定義をクラスタ内で実際にインスタンス化したものです1
  • サービス: 指定した数のタスクを維持し続ける仕組みです。タスクが落ちれば、サービスが自動的に代わりのタスクを起動します1
  • クラスタ: サービスや単独のタスクをまとめる論理的なグループです1

この章で扱うのは、クラスタとタスク定義、そして動作確認のための単発のタスクまでです。サービスとしての常時稼働は第10章に譲ります。 前編でタスク定義とその実行に必要な権限・ログの基盤を固め、後編でALBと接続した常駐サービスに仕立てるという順序です。

8.2. なぜFargateか

ECSには、コンテナをどこで動かすかを決める起動タイプが複数あります。この連載ではFargateを使います。第1章で予告した通りです。2

もう1つの主要な選択肢がEC2起動タイプです。EC2起動タイプでは、コンテナを乗せるEC2インスタンス自体を自分で用意し、OSのパッチ適用やインスタンスサイズの調整を継続的に行う必要があります。Fargateはこの管理をAWS側に任せるサーバーレスの実行基盤で、タスクごとに専用のLinuxカーネル・CPU・メモリ・ネットワークインターフェースを持ち、他のタスクと共有しません。3 Tasukuの規模 (数十テナント、同時アクセス数百ユーザー程度) では、インスタンスの管理コストをかける理由がないため、Fargateを選びます。

8.3. ECR: イメージの置き場

ECR (Elastic Container Registry) は、コンテナイメージを保管するAWSのレジストリです。タスク定義が参照するイメージは、このリポジトリに置きます。

第1章1.3節で述べた通り、この連載ではTasukuのアプリケーションコードを書きません。そのため、この章のECRリポジトリには実際のweb/apiイメージではなく、動作確認用のプレースホルダーイメージを置きます。使うイメージは8.9節で説明します。

ECRへイメージをpushするには、まずDockerクライアントをレジストリに対して認証する必要があります。認証トークンはaws ecr get-login-passwordコマンドで取得し、docker loginにパイプで渡します。4

aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com

このトークンは12時間で失効します。4 長時間の作業では再認証が必要になる点に注意してください。

リポジトリのimage_tag_mutability(タグの上書き可否) は、省略した場合MUTABLE(上書き可能) が既定値です。5 本番運用ではタグの固定 (IMMUTABLE) を検討する余地がありますが、この章では基盤を学ぶことに主眼を置き、既定値のまま進めます。

8.4. 実行ロールとタスクロール

第3章3.4節で、ECS向けに2種類のIAMロールを見込んでいると予告しました。ここでその具体的な値を見ます。

タスク実行ロール (execution role) は、ECSのコンテナエージェントとFargateエージェントが、ユーザーに代わってAWS APIを呼び出すためのロールです。ECRからのイメージ取得や、awslogsドライバによるCloudWatch Logsへのログ送信に使われます。コンテナの中からこのロールの権限に直接アクセスはできません。6

タスクロール (task role) は、コンテナの中で動くアプリケーションコード自身が使う権限です。実行ロールとは別物で、タスク定義ごとに個別のロールを用意し、必要最小限の権限だけを与えることが推奨されています。7 この章のタスクロールには、まだ何のポリシーもアタッチしません。第3章3.4節の予告表で、タスクロールへの権限追加は第10章 (S3添付バケットへのアクセス) と明記した通りです。この章ではロールの入れ物と信頼ポリシーだけを作ります。

どちらのロールも、信頼ポリシーのPrincipalはecs-tasks.amazonaws.comです。8 このサービスプリンシパルが、ロールを引き受けられる相手を「ECSタスク」に限定します。

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

8.5. 実行ロールの権限設計: マネージドポリシーかカスタムポリシーか

タスク実行ロールに与える権限には、AWSが用意するAmazonECSTaskExecutionRolePolicyという管理ポリシーを使う方法があります。このポリシーの中身は、ECRの認証・イメージ取得と、CloudWatch Logsへのログ書き込みに絞られた6つのアクションです。9

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

ただし、この管理ポリシーをそのままアタッチする方法は、Tasukuのterraform実行者では選べません。第3章で、terraform実行者がAWS管理ポリシーを含む任意のポリシーを自由にアタッチできないよう、iam:AttachRolePolicy/tasuku/配下の自作ポリシーだけに制限したためです。この制限を緩めるには、terraform_iam_managementポリシー自体の内容を書き換える必要がありますが、それにはiam:CreatePolicyVersionという権限が要ります。10 この権限は、terraform実行者が自分自身の権限を書き換えられる経路を塞ぐという第3章3.6節の設計判断により、意図的に与えていません。つまり、確立済みのterraform実行者のままでは、管理ポリシーへの切り替え自体がapply不可能です。

そこで、この章ではAmazonECSTaskExecutionRolePolicy相当の権限を、/tasuku/配下の自前のaws_iam_policyとして新規に定義し、実行ロールにアタッチします。この方法なら第3章のポリシーには一切触れず、iam:CreatePolicyiam:AttachRolePolicyという既に許可済みの権限の範囲内で完結します。加えて、自前で定義する分、権限をECRリポジトリ・ロググループの単位まで絞り込めます。ecr:GetAuthorizationTokenはリソースレベル権限に対応しないアクションのため、Resource "*"が必須です。11 一方、ecr:BatchCheckLayerAvailability/ecr:GetDownloadUrlForLayer/ecr:BatchGetImageのpull系アクションはリポジトリのARNにスコープできます。12 logs:CreateLogStream/logs:PutLogEventsも、ロググループARNの末尾に:*を付けた形にスコープできます。13

自前ポリシーの内容がAWS管理ポリシーの権限に追従しない (AWSが将来ポリシーの中身を拡張しても自動的には反映されない) 点は、この方式の対称的なトレードオフです。第10章でSecrets Managerからの読み取り権限を実行ロールに追加する際も、このポリシー自体を書き換えるのではなく、別の新規ポリシーとして追加する方針にします。1つのロールに複数のポリシーをアタッチすることは、IAMの正規の使い方です。

8.6. ログ設計: CloudWatch Logsとawslogs

コンテナの標準出力をCloudWatch Logsへ送るには、タスク定義のログ設定でawslogsドライバを指定します。必要なオプションは、送信先リージョンを示すawslogs-region(必須)14、送信先ロググループを示すawslogs-group、ログストリームの識別しやすさのために付けるawslogs-stream-prefixです。14

ロググループは、この章のTerraformコードで明示的に作成します。CloudWatch Logsのロググループは、保持期間を指定しなければ「失効しない」が既定の挙動です。15 ログを無期限に貯め続けるとコストが積み上がるため、保持期間を明示的に設定します。

8.7. ECSタスク用セキュリティグループ

第4章4.7節のセキュリティグループチェーンの図では、ECSタスク用のSGが「ALB用SGのみ」からのingressを許可する形で予告されていました。ここでその通りに作ります。第6章のALBと同じく、VPCの既定セキュリティグループには依存せず、明示的なSGを新設します。

ingressはALB用SGからの3000番ポートのみ許可します。第1章1.3節の「web」コンテナが待ち受けるポート番号です。この章で動くタスクはあとで説明するプレースホルダーイメージで、実際には何のポートも待ち受けません。このingressルールは、第10章でweb相当のサービスがALBからの転送を受け取れるようにするための、先取りした準備です。

egressは443番のみを許可します。Fargateのタスクがprivate-appサブネットからECRのイメージを取得し、CloudWatch LogsへログをputするためのHTTPS通信です。16 第4章で用意したNAT Gateway経由の経路をそのまま使うため、この章でVPCエンドポイントの追加は行いません。ドメイン名の解決 (DNS) にはポート53番の通信が要りますが、VPCが提供するAmazon DNSサーバーとの通信はセキュリティグループやネットワークACLでフィルタできないため、この一覧にDNS向けのegressルールは含みません。17

8.8. Terraform実装

ここまでの内容をecs.tfという新しいファイルに書きます。

resource "aws_ecr_repository" "app" {
name = "${local.name_prefix}-app"

tags = {
Name = "${local.name_prefix}-app"
}
}

resource "aws_ecs_cluster" "main" {
name = "${local.name_prefix}-cluster"

tags = {
Name = "${local.name_prefix}-cluster"
}
}

resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${local.name_prefix}-app"
retention_in_days = 30

tags = {
Name = "${local.name_prefix}-app-logs"
}
}

信頼ポリシーは、実行ロール・タスクロール共通のためデータソースとして1つ定義し、両方のロールから参照します。

data "aws_iam_policy_document" "ecs_tasks_assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]

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

resource "aws_iam_role" "ecs_task_execution" {
name = "${local.name_prefix}-ecs-task-execution-role"
path = "/${var.project}/"
assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume_role.json

tags = {
Name = "${local.name_prefix}-ecs-task-execution-role"
}
}

resource "aws_iam_role" "ecs_task" {
name = "${local.name_prefix}-ecs-task-role"
path = "/${var.project}/"
assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume_role.json

tags = {
Name = "${local.name_prefix}-ecs-task-role"
}
}

8.5節で決めたカスタムポリシーは、次のように定義してアタッチします。

resource "aws_iam_policy" "ecs_task_execution" {
name = "${local.name_prefix}-ecs-task-execution-policy"
path = "/${var.project}/"
description = "AmazonECSTaskExecutionRolePolicy相当の権限を、ECRリポジトリ・ロググループ単位に絞って自前定義したもの"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EcrAuth"
Effect = "Allow"
Action = ["ecr:GetAuthorizationToken"]
Resource = "*"
},
{
Sid = "EcrPull"
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
]
Resource = aws_ecr_repository.app.arn
},
{
Sid = "LogsWrite"
Effect = "Allow"
Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "${aws_cloudwatch_log_group.app.arn}:*"
},
]
})
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = aws_iam_policy.ecs_task_execution.arn
}

セキュリティグループは、8.7節の方針通りに作ります。

resource "aws_security_group" "ecs_tasks" {
name = "${local.name_prefix}-ecs-tasks-sg"
description = "ECSタスク用SG。ALB用SGからのingressのみ許可"
vpc_id = aws_vpc.main.id

tags = {
Name = "${local.name_prefix}-ecs-tasks-sg"
}
}

resource "aws_vpc_security_group_ingress_rule" "ecs_tasks_from_alb" {
security_group_id = aws_security_group.ecs_tasks.id

referenced_security_group_id = aws_security_group.alb.id
from_port = 3000
to_port = 3000
ip_protocol = "tcp"

description = "ALBからのみ許可 (第10章のweb service化・forward切替で実際に使う)"
}

resource "aws_vpc_security_group_egress_rule" "ecs_tasks_https" {
security_group_id = aws_security_group.ecs_tasks.id

cidr_ipv4 = "0.0.0.0/0"
from_port = 443
to_port = 443
ip_protocol = "tcp"

description = "ECR API・CloudWatch Logs APIへのHTTPS"
}

最後にタスク定義です。Fargateではnetwork_modeawsvpcが必須で、Terraformのスキーマ上もrequires_compatibilitiesFARGATEの場合はcpumemoryruntime_platform.operating_system_familyが必須になります。18 cpu/memoryは、Fargateで有効な組み合わせのうち最小の256(0.25 vCPU)/512MiBを使います。19

resource "aws_ecs_task_definition" "app" {
family = "${local.name_prefix}-app"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn

runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}

container_definitions = jsonencode([
{
name = "app"
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.region
"awslogs-stream-prefix" = "app"
}
}
}
])

tags = {
Name = "${local.name_prefix}-app"
}
}

コンテナ定義にportMappingsがないのは、8.9節で使うイメージがポートを一切待ち受けないためです。

最後に、後続の章が参照する値をoutputs.tfに追加します。

output "ecs_cluster_id" {
description = "ECSクラスタのID (第9/10章でも同じクラスタを使う)"
value = aws_ecs_cluster.main.id
}

output "ecr_repository_url" {
description = "ECRリポジトリのURL (push手順、タスク定義のimage参照で使う)"
value = aws_ecr_repository.app.repository_url
}

output "ecs_task_execution_role_arn" {
description = "ECSタスク実行ロールのARN (第10章のweb/apiサービスのタスク定義でも再利用)"
value = aws_iam_role.ecs_task_execution.arn
}

output "ecs_task_role_arn" {
description = "ECSタスクロールのARN (第10章でS3添付バケットへのアクセス権限を追加)"
value = aws_iam_role.ecs_task.arn
}

output "ecs_tasks_security_group_id" {
description = "ECSタスク用SGのID (第9章のRDS用SGのingress許可元として参照)"
value = aws_security_group.ecs_tasks.id
}

output "ecs_task_definition_arn" {
description = "タスク定義のARN (run-task・第10章のサービス定義で参照)"
value = aws_ecs_task_definition.app.arn
}

8.9. 動作確認

terraform fmt -check -recursive -diffterraform validateを実行し、構文とプロバイダスキーマの整合を確認します。この章の検証も、これまでと同じくvalidateまでで、planapplyは対象に含めません。

実際にapplyして確認する場合の手順です。まず、Tasukuのアプリイメージはまだ存在しないため、Docker公式のhello-worldイメージで代用します。このイメージは単一の静的バイナリで、実行するとテキストを標準出力に出してすぐに終了します。20 ポートを待ち受けず常駐もしないという性質が、この章で確認したい一連の流れにちょうど合います。タスクが起動し、ログが届き、正常に終了するという流れです。hello-worldはマルチアーキテクチャ対応イメージのため、--platformを指定しないとホストマシンのCPUアーキテクチャに一致するものが自動選択されます。タスク定義はruntime_platform.cpu_architectureX86_64に固定しているため(後述)、ARM64ホスト(Apple Siliconなど)からpushする場合は--platformで明示的にx86_64を指定してください。

aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com
docker pull --platform linux/amd64 hello-world
docker tag hello-world:latest <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:latest
docker push <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:latest

イメージをpushしたら、run-taskで単発のタスクを起動します。awsvpcConfigurationにはsubnetsが必須で、securityGroupsを省略するとVPCの既定セキュリティグループが使われてしまうため、この章で作ったSGを明示します。21 サブネットIDとSG IDはterraform outputで取得した値を使います。

aws ecs run-task \
--cluster tasuku-dev-cluster \
--task-definition tasuku-dev-app \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<private-appサブネットID>],securityGroups=[<ecs_tasks SGのID>],assignPublicIp=DISABLED}"

タスクは、PROVISIONINGPENDINGACTIVATINGRUNNINGDEACTIVATINGSTOPPINGDEPROVISIONINGSTOPPEDという状態を遷移します。22 aws ecs describe-taskslastStatusを確認し、STOPPEDになった時点でcontainers[].exitCode0であることを確認します。あわせてaws logs tail /ecs/tasuku-dev-appを実行すると、hello-worldの標準出力がログストリームに記録されているのが確認できます。

この章のホストゾーンやACMと同様、実際にAWSへapplyしてこの手順を実行した結果は保証しません。第7章までと同じ検証水準です。

本章のまとめ

  • ECSのクラスタ・タスク定義・タスク・サービスの関係を整理し、サービスとしての常時稼働は第10章に送りました
  • Fargateはタスクごとに専用のカーネル・CPU・メモリ・ネットワークインターフェースを持つサーバーレスの実行基盤で、EC2起動タイプのようなインスタンス管理が要りません
  • タスク実行ロールの権限は、AWS管理ポリシーではなく/tasuku/配下の自前ポリシーとして定義しました。第3章の権限制約と矛盾せず、権限をリポジトリ・ロググループ単位まで絞れます
  • タスクロールは信頼ポリシーのみで作り、S3添付バケットへのアクセス権限は第10章で追加します
  • ECSタスク用SGは、ALB用SGからのingressのみを許可する形で新設し、default SGには依存しません
  • 動作確認はhello-worldイメージの単発run-taskで行い、タスクの起動からログ到達までの一連の流れを確かめました

次に読む

  • 第 9 章: RDS: サブネットグループとMulti-AZ、マスターパスワードのSecrets Manager統合、セキュリティグループチェーンの完成
  • 第 7 章: Route 53 / ACM: DNS委任とACMのDNS検証、443番リスナーの追加によるHTTPS化
  • 連載の目次: 全 12 章の構成と読み進め方

Footnotes

  1. 出典: Amazon ECS task definitionsCluster details。タスクはタスク定義をクラスタ内でインスタンス化したもの、サービスは指定した数のタスクを維持し続ける仕組み、クラスタはサービスや単独タスクの論理的なグループであるとしています。 2 3

  2. 出典: 第 1 章 1.5節。ECSの起動タイプにFargateを使い、EC2サーバーの管理なしでコンテナを動かすと予告しています。

  3. 出典: How Amazon ECS manages CPU and memory resources。各Fargateタスクは専用の分離境界を持ち、カーネル・CPU・メモリ・ネットワークインターフェースを他のタスクと共有しないとしています。

  4. 出典: Private registry authentication in Amazon ECR。認証トークンは12時間有効で、get-login-passwordコマンドで取得しdocker loginにパイプで渡すとしています。 2

  5. 出典: CreateRepositoryRequestimage_tag_mutabilityを省略した場合、既定値はMUTABLE(タグの上書き可能) だとしています。

  6. 出典: Amazon ECS task execution IAM role。実行ロールの権限はECS/Fargateエージェントに一時的な認証情報として渡され、タスク内のコンテナから直接アクセスできないとしています。

  7. 出典: Amazon ECS task IAM role。複数のタスク定義やサービスがIAM権限を要する場合、それぞれに専用のロールを作り、必要最小限の権限に絞ることを推奨しています。

  8. 出典: 同上、およびAmazon ECS task execution IAM role。信頼ポリシーのPrincipalにecs-tasks.amazonaws.comを指定するJSON例を示しています。

  9. 出典: AmazonECSTaskExecutionRolePolicy。ポリシーのJSON文書として、ECRの認証・イメージ取得とCloudWatch Logsへの書き込みに関わる6つのアクションをResource "*"で許可する内容を示しています。

  10. 出典: Versioning IAM policies。既存の管理ポリシーを更新するには新しいバージョンを作成する必要があり、そのためにiam:CreatePolicyVersion権限が要るとしています。

  11. 出典: How Amazon Elastic Container Registry works with IAM。リソースレベル権限に対応しないアクションは、Resourceをワイルドカード"*"にする必要があるとしています。

  12. 出典: Actions, resources, and condition keys for Amazon Elastic Container RegistryBatchCheckLayerAvailabilityGetDownloadUrlForLayerBatchGetImageはいずれもrepositoryリソースタイプに対応し、リソースレベル権限を指定できるとしています。GetAuthorizationTokenには対応するリソースタイプの記載がありません。

  13. 出典: Using identity-based policies (IAM policies) for CloudWatch LogsCreateLogStream/PutLogEventsのようなログストリーム単位のアクションは、ロググループARNの末尾に:*を付けたARNでスコープできるとしています。

  14. 出典: Example Amazon ECS task definition: Route logs to CloudWatch、およびLogConfiguration optionsawslogs-regionが必須であること、awslogs-stream-prefixを指定しないとECSコンソールのLogペインでログが確認できないことを示しています。 2

  15. 出典: Log group creation。新規作成したロググループの保持期間は「失効しない」が既定値だとしています。

  16. 出典: Task Networking in AWS Fargate。private subnet内のFargateタスクがNAT Gatewayを持たない場合、ECRからのイメージ取得やCloudWatch Logsとの通信ができないとしています。

  17. 出典: Understanding Amazon DNS。「Amazon DNSサーバーとの通信はネットワークACLやセキュリティグループでフィルタできない」としています。

  18. 出典: terraform MCP (hashicorp/aws provider v6.53.0、aws_ecs_task_definition)。requires_compatibilitiesFARGATEの場合、cpumemoryruntime_platform.operating_system_familyがいずれも必須になるとしています。

  19. 出典: Amazon ECS task definition parameters for Fargate。Fargateで有効なCPU/メモリの組み合わせを表で示しており、256(.25 vCPU)には512 MiB/1 GB/2 GBが対応するとしています。

  20. 出典: hello-world - Official Image。単一の静的バイナリで構成され、実行するとテキストを標準出力に出力するとしています。

  21. 出典: Amazon ECS API: NetworkConfigurationsubnetsは必須、securityGroupsを指定しない場合はVPCの既定セキュリティグループが使われるとしています。

  22. 出典: Amazon ECS task lifecycle。タスクの状態遷移をPROVISIONINGからSTOPPEDまで、各状態の意味とともに示しています。