[非公式] Account Factory for Terraform (AFT) 環境の維持コストを最小化する

AFT節約術
2022.09.15

どうも、ちゃだいん(@chazuke4649)です。

AFT環境のコストを最小化したい

Account Factory for Terraform (AFT) はControl TowerをTerraformで管理する上でとても便利なソリューションです。 しかし、利用頻度が多い訳ではないワークロードにおいては、その維持コストが気になったりすることがあります。特にPoC/検証環境などなら尚更でしょう。

今回は、非公式な方法ではありますが、AFTの維持コストを最小化してみます。

非公式で個人的なワークアラウンドであり、重要度の高い環境では非推奨です。これらによってAFTのシステムやデータが修復不可能になる可能性はゼロではありません。自己責任でお願いします。

前提

  • AFT バージョン: 1.6.2

そもそも、VPCエンドポイントの利用は避けよう

コストを抑えたい場合、モジュールのデプロイ時、aft_vpc_endpointsは必ずfalseにしましょう。

これがデフォルトだとtrueになり、VPCにVPCエンドポイントが作成され、かなりの課金が発生します。詳細はコチラをどうぞ。

公式的な方法では上記のみであり、それ以上のコスト低減方法は現時点でありません。

よってここからは、非公式的にコスト削減のアクションを実施していきます。

コスト削減効果

※ 金額はあくまで超概算であり、変更される可能性があります。

実施前

  • だいたい $4/日 くらい(主に東京リージョン)

よってだいたい $120/月 になります。

実施後

  • だいたい $0.4/日 くらい(主に東京リージョン)

よってだいたい $12/月 になります。

やること

  • 1-1.NATGWとEIP x2 を削除する
  • 1-2.Lambda x1 を削除する
  • (2.Lambdaの実行頻度を減らす)
  • (3.DynamoDBストリームにトリガーされたLambdaを外す)
  • (4.DynamoDBのグローバルレプリケーションを削除する)

2022.9.30追記)1-1,2以外はあまりコスト削減効果はない可能性が高いです。追加調査した結果、CloudTrailコストの原因は1つのリージョンに2つ以上の証跡が存在することによる課金でした...(凡ミス)。ですので、1以外の2〜4は基本的に実施不要でよさそうです。念の為、参考までにオプションとして項目名に()をつけて残しておきます。 "どのリージョンでも管理イベントの最初の証跡は無料ですが、追加の証跡には料金がかかります。組織の証跡の潜在的なコストを削減するには、管理アカウントとメンバーアカウントの不要な証跡を削除することを検討してください"

1-1. NATGWとEIP x2 を削除する

利用料金の中で最も大きな比率を占める、NATGW x2を削除します。

上記が完了後、残ったEIP x2も削除します。

1-2. Lambda x1を削除する

NATGWを削除した環境では、Lambda関数aft-lambda-layer-codebuild-invokerを削除しておかないと、復旧のためのplanがエラーになってしまいます。(以下エラー内容)

╷
│ Error: Lambda function (aft-lambda-layer-codebuild-invoker) returned error: ({"errorMessage": "Connect timeout on endpoint URL: \"https://codebuild.ap-northeast-1.amazonaws.com/\"", "errorType": "ConnectTimeoutError", "stackTrace": ["  File \"/var/task/codebuild_invoker.py\", line 28, in lambda_handler\n    job_id = client.start_build(projectName=codebuild_project_name)[\"build\"][\"id\"]\n", "  File \"/var/runtime/botocore/client.py\", line 391, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n", "  File \"/var/runtime/botocore/client.py\", line 705, in _make_api_call\n    http, parsed_response = self._make_request(\n", "  File \"/var/runtime/botocore/client.py\", line 725, in _make_request\n    return self._endpoint.make_request(operation_model, request_dict)\n", "  File \"/var/runtime/botocore/endpoint.py\", line 104, in make_request\n    return self._send_request(request_dict, operation_model)\n", "  File \"/var/runtime/botocore/endpoint.py\", line 138, in _send_request\n    while self._needs_retry(attempts, operation_model, request_dict,\n", "  File \"/var/runtime/botocore/endpoint.py\", line 254, in _needs_retry\n    responses = self._event_emitter.emit(\n", "  File \"/var/runtime/botocore/hooks.py\", line 357, in emit\n    return self._emitter.emit(aliased_event_name, **kwargs)\n", "  File \"/var/runtime/botocore/hooks.py\", line 228, in emit\n    return self._emit(event_name, kwargs)\n", "  File \"/var/runtime/botocore/hooks.py\", line 211, in _emit\n    response = handler(**kwargs)\n", "  File \"/var/runtime/botocore/retryhandler.py\", line 183, in __call__\n    if self._checker(attempts, response, caught_exception):\n", "  File \"/var/runtime/botocore/retryhandler.py\", line 250, in __call__\n    should_retry = self._should_retry(attempt_number, response,\n", "  File \"/var/runtime/botocore/retryhandler.py\", line 277, in _should_retry\n    return self._checker(attempt_number, response, caught_exception)\n", "  File \"/var/runtime/botocore/retryhandler.py\", line 316, in __call__\n    checker_response = checker(attempt_number, response,\n", "  File \"/var/runtime/botocore/retryhandler.py\", line 222, in __call__\n    return self._check_caught_exception(\n", "  File \"/var/runtime/botocore/retryhandler.py\", line 359, in _check_caught_exception\n    raise caught_exception\n", "  File \"/var/runtime/botocore/endpoint.py\", line 201, in _do_get_response\n    http_response = self._send(request)\n", "  File \"/var/runtime/botocore/endpoint.py\", line 270, in _send\n    return self.http_session.send(request)\n", "  File \"/var/runtime/botocore/httpsession.py\", line 442, in send\n    raise ConnectTimeoutError(endpoint_url=request.url, error=e)\n"]})
│
│   with module.aft.module.aft_lambda_layer.data.aws_lambda_invocation.invoke_codebuild_job,
│   on .terraform/modules/aft/modules/aft-lambda-layer/lambda.tf line 21, in data "aws_lambda_invocation" "invoke_codebuild_job":
│   21: data "aws_lambda_invocation" "invoke_codebuild_job" {
│
╵

planが通るようにするため、Lambda関数aft-lambda-layer-codebuild-invokerを削除します。

(2. Lambdaの実行頻度を減らす)

Lambda関数aft-account-request-processorは、EventBridgeaft-account-request-processorによって、5分ごとに実行される設定になっています。これが地味に、CloudTrailのコストをかさませるので、この頻度を下げます。

対象のEventBridgeはLambdaのコンソールにて見つけることができます。

EventBridgeルールの設定にて、5分ごとを30日ごとに変更します。

(3. DynamoDBストリームにトリガーされたLambdaを外す)

DynamoDBテーブルaft-requestはストリームが有効化されており、トリガーとしてLambdaaft-account-request-action-triggeraft-account-request-audit-triggerが設定されています。

これによりLambdaが高頻度でDybamoDBテーブルのストリームをポーリングするため、CloudTrailのコストがかさみます。

よって2つ分のLambdaのトリガー設定を外します。

下にスクロールすると、トリガー設定があるのでこれ2つを削除します。

(4. DynamoDBのグローバルレプリケーションを削除する)

DynamoDBテーブルaft-backend-{AwsAccountId}は高可用性のためグローバルレプリケーションによってシンガポールにテーブルがレプリケートされています。 微々たるではありますが、今回はコスト削減を徹底するため、レプリケーション先のテーブルを削除します。

コスト削減活動は以上です。

復旧方法

もちろんこのままではAFTは正常に機能しません。実際にAFTを使用する機会が訪れたとして、AFTが機能するように復旧させる必要があります。

やり方は簡単で、AFTモジュールを再度適用し、手動削除・変更したリソースを修復します。

ここはIaCの強みを活かし、コードで定義した状態に戻そうとする動きを活用します。

管理アカウント権限で terraform plan / apply を実行します。

% terraform plan

## 中略 ##

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
 <= read (data resources)

Terraform will perform the following actions:

  # module.aft.module.aft_account_request_framework.aws_cloudwatch_event_rule.aft_account_request_processor will be updated in-place
  ~ resource "aws_cloudwatch_event_rule" "aft_account_request_processor" {
        id                  = "aft-lambda-account-request-processor"
        name                = "aft-lambda-account-request-processor"
      ~ schedule_expression = "rate(30 days)" -> "rate(5 minutes)"
        tags                = {}
        # (5 unchanged attributes hidden)
    }

  # module.aft.module.aft_account_request_framework.aws_eip.aft-vpc-natgw-01 will be created
  + resource "aws_eip" "aft-vpc-natgw-01" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = (known after apply)
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags_all             = {
          + "managed_by" = "AFT"
        }
      + vpc                  = (known after apply)
    }

  # module.aft.module.aft_account_request_framework.aws_eip.aft-vpc-natgw-02 will be created
  + resource "aws_eip" "aft-vpc-natgw-02" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = (known after apply)
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags_all             = {
          + "managed_by" = "AFT"
        }
      + vpc                  = (known after apply)
    }

  # module.aft.module.aft_account_request_framework.aws_lambda_event_source_mapping.aft_account_request_action_trigger will be created
  + resource "aws_lambda_event_source_mapping" "aft_account_request_action_trigger" {
      + batch_size                    = 1
      + enabled                       = true
      + event_source_arn              = "arn:aws:dynamodb:ap-northeast-1:111111111111:table/aft-request/stream/2021-11-30T08:05:19.518"
      + function_arn                  = (known after apply)
      + function_name                 = "arn:aws:lambda:ap-northeast-1:111111111111:function:aft-account-request-action-trigger"
      + id                            = (known after apply)
      + last_modified                 = (known after apply)
      + last_processing_result        = (known after apply)
      + maximum_record_age_in_seconds = (known after apply)
      + maximum_retry_attempts        = 1
      + parallelization_factor        = (known after apply)
      + starting_position             = "LATEST"
      + state                         = (known after apply)
      + state_transition_reason       = (known after apply)
      + uuid                          = (known after apply)
    }

  # module.aft.module.aft_account_request_framework.aws_lambda_event_source_mapping.aft_account_request_audit_trigger will be created
  + resource "aws_lambda_event_source_mapping" "aft_account_request_audit_trigger" {
      + batch_size                    = 1
      + enabled                       = true
      + event_source_arn              = "arn:aws:dynamodb:ap-northeast-1:111111111111:table/aft-request/stream/2021-11-30T08:05:19.518"
      + function_arn                  = (known after apply)
      + function_name                 = "arn:aws:lambda:ap-northeast-1:111111111111:function:aft-account-request-audit-trigger"
      + id                            = (known after apply)
      + last_modified                 = (known after apply)
      + last_processing_result        = (known after apply)
      + maximum_record_age_in_seconds = (known after apply)
      + maximum_retry_attempts        = 1
      + parallelization_factor        = (known after apply)
      + starting_position             = "LATEST"
      + state                         = (known after apply)
      + state_transition_reason       = (known after apply)
      + uuid                          = (known after apply)
    }

  # module.aft.module.aft_account_request_framework.aws_nat_gateway.aft-vpc-natgw-01 will be created
  + resource "aws_nat_gateway" "aft-vpc-natgw-01" {
      + allocation_id        = (known after apply)
      + connectivity_type    = "public"
      + id                   = (known after apply)
      + network_interface_id = (known after apply)
      + private_ip           = (known after apply)
      + public_ip            = (known after apply)
      + subnet_id            = "subnet-0fd5be2f3d40b1e8b"
      + tags                 = {
          + "Name" = "aft-vpc-natgw-01"
        }
      + tags_all             = {
          + "Name"       = "aft-vpc-natgw-01"
          + "managed_by" = "AFT"
        }
    }

  # module.aft.module.aft_account_request_framework.aws_nat_gateway.aft-vpc-natgw-02 will be created
  + resource "aws_nat_gateway" "aft-vpc-natgw-02" {
      + allocation_id        = (known after apply)
      + connectivity_type    = "public"
      + id                   = (known after apply)
      + network_interface_id = (known after apply)
      + private_ip           = (known after apply)
      + public_ip            = (known after apply)
      + subnet_id            = "subnet-050a3231338de4f45"
      + tags                 = {
          + "Name" = "aft-vpc-natgw-02"
        }
      + tags_all             = {
          + "Name"       = "aft-vpc-natgw-02"
          + "managed_by" = "AFT"
        }
    }

  # module.aft.module.aft_account_request_framework.aws_route_table.aft_vpc_private_subnet_01 will be updated in-place
  ~ resource "aws_route_table" "aft_vpc_private_subnet_01" {
        id               = "rtb-0ee906de3470c5157"
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "0.0.0.0/0"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = ""
              - instance_id                = ""
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = "nat-0efbd5f8afc83ba59"
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
            # (1 unchanged element hidden)
        ]
        tags             = {
            "Name" = "aft-vpc-private-subnet-01"
        }
        # (5 unchanged attributes hidden)
    }

  # module.aft.module.aft_account_request_framework.aws_route_table.aft_vpc_private_subnet_02 will be updated in-place
  ~ resource "aws_route_table" "aft_vpc_private_subnet_02" {
        id               = "rtb-0dc79631f7a2d3880"
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "0.0.0.0/0"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = ""
              - instance_id                = ""
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = "nat-0d3af787ce7388ff2"
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
            # (1 unchanged element hidden)
        ]
        tags             = {
            "Name" = "aft-vpc-private-subnet-02"
        }
        # (5 unchanged attributes hidden)
    }

  # module.aft.module.aft_backend.aws_dynamodb_table.lock-table will be updated in-place
  ~ resource "aws_dynamodb_table" "lock-table" {
        id               = "aft-backend-111111111111"
        name             = "aft-backend-111111111111"
        tags             = {
            "Name" = "aft-backend-111111111111"
        }
        # (10 unchanged attributes hidden)



      + replica {
          + kms_key_arn            = (known after apply)
          + point_in_time_recovery = false
          + propagate_tags         = false
          + region_name            = "ap-southeast-1"
        }

        # (3 unchanged blocks hidden)
    }

  # module.aft.module.aft_lambda_layer.data.aws_lambda_invocation.invoke_codebuild_job will be read during apply
  # (config refers to values not yet known)
 <= data "aws_lambda_invocation" "invoke_codebuild_job"  {
      ~ id            = "aft-lambda-layer-codebuild-invoker_$LATEST_3d56455871df474e76c910b73e2d2648" -> (known after apply)
      - qualifier     = "$LATEST" -> null
      ~ result        = jsonencode(
            {
              - Status = 200
            }
        ) -> (known after apply)
        # (2 unchanged attributes hidden)
    }

  # module.aft.module.aft_lambda_layer.aws_lambda_function.codebuild_invoker will be created
  + resource "aws_lambda_function" "codebuild_invoker" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + description                    = "AFT Lambda Layer - CodeBuild Invoker"
      + filename                       = ".terraform/modules/aft/modules/aft-archives/../../src/aft_lambda/aft_builder.zip"
      + function_name                  = "aft-lambda-layer-codebuild-invoker"
      + handler                        = "codebuild_invoker.lambda_handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 1024
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + reserved_concurrent_executions = -1
      + role                           = "arn:aws:iam::111111111111:role/codebuild_invoker_role"
      + runtime                        = "python3.8"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + source_code_hash               = "MQZ8MM29zszuIJTotrdcCzRxdrLwMk6gh/6VjrdOcLI="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "managed_by" = "AFT"
        }
      + timeout                        = 900
      + version                        = (known after apply)

      + ephemeral_storage {
          + size = (known after apply)
        }

      + tracing_config {
          + mode = (known after apply)
        }

      + vpc_config {
          + security_group_ids = [
              + "sg-026ad8cf6c06c5063",
            ]
          + subnet_ids         = [
              + "subnet-0b367c1cc182804fc",
              + "subnet-0dca45d7cc47d0f86",
            ]
          + vpc_id             = (known after apply)
        }
    }

Plan: 7 to add, 4 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

※AWSアカウントIDはダミー値(111111111111)に変更しています

問題なければ、適用します。

% terraform apply -auto-approve

## 中略 ##

module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Still modifying... [id=aft-backend-597692810508, 11m10s elapsed]
module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Still modifying... [id=aft-backend-597692810508, 11m20s elapsed]
module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Still modifying... [id=aft-backend-597692810508, 11m30s elapsed]
module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Modifications complete after 11m31s [id=aft-backend-111111111111]

Apply complete! Resources: 7 added, 4 changed, 0 destroyed.

削除・変更したリソースが修復されました。

終わりに

AFT環境の維持コストを最小化してみました。

それでは今日はこの辺で。ちゃだいん(@chazuke4649)でした。