🧐

AWS & Terraform の間違い探しを作りました

2023/08/31に公開

ソースコード

GitHub で公開しています.
https://github.com/naitoNanaco/exercise_aws_terraform

想定する対象者

  • AWS, Terraform 初学者 (なんとなく使い方はわかる人)
  • VPC, ECS (Fargate) , ALB あたりの知見を深めたい人

なんで作ったの?

会社でAWSを利用していてterraformで管理しているのですが, これからクラウドインフラも触りたいという人から
「現コードと照らし合わせて設定値を変更するくらいならできるけど, イチからつくる自信はないし, つくれるようになりたいけど何から始めたらいいかわからない」
みたいな話を聞きました.

正直, 自分もどうやってわかるようになったのか覚えていません.
EC2とは?VPCとは?みたいな基礎はドキュメントやネットの情報で知ったと思います.
ただ細かな勘所みたいなものは, 実現したいことがあるのに思ったように動かないものを, なんとか動かす苦行の中で身につけたように思います.

そんなわけで, 動きそうで動かないアプリケーション(のインフラコード)を用意してみました.
Terraformで使われるHCLの文法には則った一見正しそうなコードですが, まともに plan も実行できなければ, apply が通るようになってもデプロイに失敗する...... そんなコードです.

デバッグしていく中で少しでも知らないエラーに出会い, 知識が深まるといいなという思いです.

解答例

以下, 修正のポイントと解答例です.

ALB, Target Group の名前に _ は許可されていません.

まず terraform plan を実行すると以下のエラーが出ます.

╷
│ Error: only alphanumeric characters and hyphens allowed in "name"
│ 
│   with aws_lb_target_group.test_app,
│   on main.tf line 26, in resource "aws_lb_target_group" "test_app":
│   26:   name                 = "${local.name}-tg"
│ 
╵
╷
│ Error: only alphanumeric characters and hyphens allowed in "name": "test_app"
│ 
│   with aws_lb.test_app,
│   on main.tf line 54, in resource "aws_lb" "test_app":
│   54:   name                       = local.name
│ 
╵

エラーメッセージの通り ALB, Target Group の名前に _ は許可されていません.
_ を含まない形に修正しましょう. replace 関数を使うと一括で取り除くこともできます.

main.tf
-  name                 = "${local.name}-tg"
+  name                 = replace("${local.name}-tg", "_", "-")
main.tf
-  name                       = local.name
+  name                       = replace(local.name, "_", "-")

S3 Bucket の名前に _ は許可されていません.

Fargate ではタスク定義の constraints はサポートされません.

前述の修正で terraform plan が成功するようになりますが, terraform apply 時にエラーが出ます。

╷
│ Error: validating S3 Bucket (test_app_test_alb_log) name: only lowercase alphanumeric characters and hyphens allowed in "test_app_test_alb_log"
│ 
│   with aws_s3_bucket.alb_log_test_app,
│   on main.tf line 115, in resource "aws_s3_bucket" "alb_log_test_app":
│  115: resource "aws_s3_bucket" "alb_log_test_app" {
│ 
╵
╷
│ Error: creating ECS Task Definition (test_app): ClientException: Fargate compatible task definitions do not support constraints
│ 
│   with aws_ecs_task_definition.test_app,
│   on main.tf line 214, in resource "aws_ecs_task_definition" "test_app":
│  214: resource "aws_ecs_task_definition" "test_app" {
│ 
╵
╷
│ Error: validating S3 Bucket (test_app_test_flow_log) name: only lowercase alphanumeric characters and hyphens allowed in "test_app_test_flow_log"
│ 
│   with aws_s3_bucket.flow_log,
│   on network.tf line 23, in resource "aws_s3_bucket" "flow_log":
│   23: resource "aws_s3_bucket" "flow_log" {
│ 
╵

S3 のバケット名については ALB 同様に _ を取り除く必要があります.

main.tf
-  bucket = "${local.name}_${local.environment}_alb_log"
+  bucket = replace("${local.name}_${local.environment}_alb_log", "_", "-")
network.tf
-  bucket = "${local.name}_${local.environment}_flow_log"
+  bucket = replace("${local.name}_${local.environment}_flow_log", "_", "-")

また S3 のバケット名はグローバルにユニークである必要があります. 続けてエラーが出る場合にはユニークになるよう工夫してください.

Error: Error creating S3 bucket: BucketAlreadyExists: The requested bucket name is not available.
The bucket namespace is shared by all users of the system. Please select a different name and try again.
network.tf
-  bucket = replace("${local.name}_${local.environment}_flow_log", "_", "-")
+  bucket = replace("${local.name}_${local.environment}_flow_log_${data.aws_caller_identity.current.account_id}", "_", "-")

ECS ではタスクの配置に戦略 (strategy) や制約事項 (constraints) を設定できます. しかしこれらはプラットフォームに EC2 を選択した場合に限られ, Fargate ではサポートされません.

main.tf
-  placement_constraints {
-    type       = "memberOf"
-    expression = "attribute:ecs.availability-zone in [ap-northeast-1a, ap-northeast-1c]"
-  }
-

ALB に紐付けるサブネットは少なくとも2つのAZのものを指定する必要があります.

Fargate を使う時の network mode は awsvpc のみがサポートされています.

╷
│ Error: creating ELBv2 application Load Balancer (test-app): ValidationError: At least two subnets in two different Availability Zones must be specified
│       status code: 400, request id: 4506986c-3804-49a5-8dd7-0da46f55xxxx
│ 
│   with aws_lb.test_app,
│   on main.tf line 53, in resource "aws_lb" "test_app":
│   53: resource "aws_lb" "test_app" {
│ 
╵
╷
│ Error: creating ECS Task Definition (test_app): ClientException: Fargate only supports network mode ‘awsvpc’.
│ 
│   with aws_ecs_task_definition.test_app,
│   on main.tf line 214, in resource "aws_ecs_task_definition" "test_app":
│  214: resource "aws_ecs_task_definition" "test_app" {
│ 
╵

ALB は異なるAZに作られた2つ以上のサブネットを選択しなければ作ることができません.
サブネットを確認すると, パブリックサブネットを2つ作成しているためこの両方を使うことで解決します.

main.tf
-  subnets                    = [aws_subnet.public_a.id]
+  subnets                    = [aws_subnet.public_a.id, aws_subnet.public_c.id]

ECS ではタスクのネットワークモードを選択しますが, Fargate を利用する場合には awsvpc に限定されます.

main.tf
-  network_mode             = "bridge"
+  network_mode             = "awsvpc"

Fargateを利用するとき (network mode が awsvpc のとき) Target Group の target type は ip にします.

╷
│ Error: creating ECS Service (test_app): InvalidParameterException: The provided target group arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/test-app-tg/ac75038baxxxxxx0 has target type instance, which is incompatible with the awsvpc network mode specified in the task definition.
│ 
│   with aws_ecs_service.test_app,
│   on main.tf line 261, in resource "aws_ecs_service" "test_app":
│  261: resource "aws_ecs_service" "test_app" {
│ 
╵

awsvpc モードではタスクは EC2 インスタンスではなく ENI に関連付けられていて, Target Group を作成する際の target type は ip である必要があります.

main.tf
-  target_type          = "instance"
+  target_type          = "ip"

target type の変更時に Target Group の replace が発生しますが listner rule から参照されているため削除できません.
先に参照元のリソースを削除するなどして関連をなくすことで作り直せます.

╷
│ Error: deleting Target Group: ResourceInUse: Target group 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/test-app-tg/ac75038baxxxxxx0' is currently in use by a listener or a rule
│       status code: 400, request id: 15520ad9-0fbc-4002-92d9-xxx7xxx7xxx2
│ 
│ 
╵
$ terraform destroy -target=aws_lb_listener_rule.forward_service

Fargate ではサービスの placement constraints はサポートされません.

╷
│ Error: creating ECS Service (test_app): InvalidParameterException: Placement constraints are not supported with FARGATE launch type.
│ 
│   with aws_ecs_service.test_app,
│   on main.tf line 261, in resource "aws_ecs_service" "test_app":
│  261: resource "aws_ecs_service" "test_app" {
│ 

タスク配置の制約はタスク定義だけでなくサービスにも定義できます. こちらも Fargate ではサポートされていません.

main.tf
-
-  placement_constraints {
-    type       = "memberOf"
-    expression = "attribute:ecs.availability-zone in [ap-northeast-1a, ap-northeast-1c]"
-  }

ここまでの修正で terraform apply が成功するようになります. しかしまだ URL アクセス時にエラーが出ます。

Secrets Manager で管理された secret へのアクセスに timeout で失敗しています.

URL にアクセスすると503エラーの画面が表示されます. 503エラーはALBの先にターゲットが存在しない時に起こります.
停止したタスクの詳細からエラーを確認できます.

ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve secret from asm: service call has been retried 5 time(s): failed to fetch secret arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:test_app-XXXxxx from secrets manager: RequestCanceled: request context canceled caused by: context deadline exceeded. Please check your task network configuration.

ネットワークの設定を見直すとタスクを配置するプライベートサブネットからインターネットへのルートも VPC エンドポイント (PrivateLink) もありません.
これでは, タスクの起動するプライベートサブネットから secret へのアクセスができません.

network.tf
+
+resource "aws_route" "private_nat" {
+  route_table_id         = aws_route_table.private.id
+  destination_cidr_block = "0.0.0.0/0"
+  nat_gateway_id         = aws_nat_gateway.public_a.id
+  depends_on             = [aws_route_table.private]
+}
network.tf
+resource "aws_vpc_endpoint" "secretsmanager" {
+  vpc_id       = aws_vpc.main.id
+  service_name = "com.amazonaws.ap-northeast-1.secretsmanager"
+
+  tags = merge(
+    {
+      Name = "${local.name}_secretsmanager_vpc_endpoint"
+    },
+    local.tags,
+  )
+}
+
+resource "aws_vpc_endpoint_route_table_association" "secretsmanager" {
+  route_table_id  = aws_route_table.private.id
+  vpc_endpoint_id = aws_vpc_endpoint.secretsmanager.id
+}

Secrets Manager で管理された secret へのアクセスに権限不足で失敗しています.

secret へのアクセス経路を確保しても状況は変わりません.
同様にエラーの詳細を確認すると, 権限不足であることがわかります.

ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve secret from asm: service call has been retried 1 time(s): failed to fetch secret arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:test_app-JEUmdk from secrets manager: AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/test_app_EcsTaskExecutionRole/44b97888aaa6438a89926a3f6d42199d is not authorized to perform: secretsmanager:GetSecretValue on resource: arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:test_app-XXXxxx because no identity-based policy allows the secretsmanager:GetSecretValue action status code: 400, request id: 6f8ed41a-2dfc-47ef-b333-9c14afd07387

タスク実行ロールに secret の読み取り権限を付与します.

iam.tf
+data "aws_iam_policy_document" "get_secrets" {
+  statement {
+    effect  = "Allow"
+    actions = ["secretsmanager:GetSecretValue"]
+    resources = [
+      aws_secretsmanager_secret.test_app_keys.id,
+      "${aws_secretsmanager_secret.test_app_keys.id}*",
+    ]
+  }
+}
+
+resource "aws_iam_policy" "get_secrets" {
+  name   = "${local.name}_ECSTaskGetSecrets"
+  path   = "/application/"
+  policy = data.aws_iam_policy_document.get_secrets.json
+}
+
+resource "aws_iam_role_policy_attachment" "ecs_task_get_secrets" {
+  role       = aws_iam_role.ecs_task_execution_role.name
+  policy_arn = aws_iam_policy.get_secrets.arn
+}
+

ここまでの修正でタスクの起動に成功するようになりますが, 依然として URL アクセス時にエラーが出ます。

ALBからコンテナへの接続がタイムアウトしヘルスチェックに失敗しています.

URL にアクセスすると504エラーの画面が表示されます. 504エラーはALBとターゲットの接続に失敗している場合や, ALBのタイムアウトまでにターゲットが応答しなかった場合に起こります.

停止したタスクの詳細, および Target Group からエラーを確認できます.

Task failed ELB health checks in (target-group arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/test-app-tg/92d3e1bxxxxxxxx9)
unhealthy	| Request timed out

起動したタスクのヘルスチェックにタイムアウトして失敗しています.
負荷に問題がなく, コンテナのログにアクセスログが出てこないことからも, タスクが処理できていない事態よりも ALB からタスクへの通信経路が疑わしいと考えられます.
慎重に確認すると, タスク定義の Security Group の設定で ALB からのアクセスが許可されていないことがわかります.

main.tf
     security_groups = [
+      aws_security_group.from_alb.id,
       aws_security_group.to_all.id,
     ]

ここまで修正すると, 無事アプリケーションへのアクセスに成功します.

おまけ

本構成は tfsec によりいくつかの問題が検出されます.

$ tfsec
  ...
  35 passed, 35 potential problem(s) detected.

不要な設定を見直し, よりセキュアな構成にしてみましょう.

NE株式会社の開発ブログ

Discussion