この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
どうも、ちゃだいん(@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,2以外はあまりコスト削減効果はない可能性が高いです。追加調査した結果、CloudTrailコストの原因は1つのリージョンに2つ以上の証跡が存在することによる課金でした...(凡ミス)。ですので、1,2以外の3,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-lambda-account-request-processor
によって、5分ごとに実行される設定になっています。これによって Lambdaのコストがかかる可能性があるので、この頻度を下げます。
対象のEventBridgeはLambdaのコンソールにて見つけることができます。
EventBridgeルールの設定にて、5分
ごとを30日
ごとに変更します。
(3. DynamoDBストリームにトリガーされたLambdaを外す)
DynamoDBテーブルaft-request
はストリームが有効化されており、トリガーとしてLambdaaft-account-request-action-trigger
とaft-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)でした。