freeeの開発情報ポータルサイト

TerraformのAWS ProviderでRoute Table Resourceを利用するときに気を付けたほうがよいこと

こんにちは!PSIRTのimarikuです!この記事はfreee 基盤チーム Advent Calendar 2023の8日目としてお送りします。

私は普段、PSIRTのメンバーとして主にクラウド方面を中心にプロダクトセキュリティの向上に取り組んでいます。


PSIRT buleteamでは現在AWS Network Firewallを利用したegress filterの整備を進めており、その過程の中でAWS VPCのRoute Tableの変更をする機会がありました。 egress filter導入に関しては、過去の記事でも触れていますので是非ご覧ください。

freeeでは原則としてクラウドインフラストラクチャの構成管理にTerraformを利用していますが、Route TableでTerraformを扱う際に少々苦戦しました。

とは言っても今回はRoute TableをVPCやSubnetに関連づけるという一般的な利用ではなく、Internet Gatewayにアタッチして利用するという、どちらかと言えばイレギュラーな利用でしたので、そのことを念頭に読んでいただければ幸いです。


通常、TerraformでAWS VPCのRoute Tableを作成する場合、以下のような定義になると思います。

resource "aws_route_table" "example-route-table" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "example-route-table"
  }
}

ひとまずはシンプルにrouteが何もないRoute Tableを作成してみます。

これを terraform apply すると以下のようなrouteを持つRoute Tableが作成されます。

作成したRoute TableがAWSコンソール上でどのように見えているかわかる様子の画像

送信先が VPCのCIDR(ここでは10.0.0.0/16)に対しターゲットがlocalとなっているrouteが暗黙的に追加されます。これはRoute Tableに馴染みがある方にとっては自然なものだと思います。同一VPC CIDR内の通信はlocal(つまりネットワーク内)にルーティングするので内容的には自明でしょう。

今回はInternet GatewayにアタッチするRoute Tableを作成するということもあり、送信先が10.0.0.0/16の通信のターゲットをlocalではなく特定のVPC Endpointにするというrouteを作成する場面がありました。 ここでは便宜上NAT Gatewayを代わりに利用します。


先ほどのTerraformのコードを以下のように変更してみます。

resource "aws_route_table" "example-route-table" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = aws_nat_gateway.main.id
  }

  tags = {
    Name = "example-route-table"
  }
}

差分は route追加になります。terraform planの結果は以下の通りで問題なさそうです。

# aws_route_table.example-route-table will be updated in-place
  ~ resource "aws_route_table" "example-route-table" {
        id               = "rtb-******"
      ~ route            = [
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "10.0.0.0/16"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = "nat-******"
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = ""
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name" = "example-route-table"
        }
        # (5 unchanged attributes hidden)
    }
Plan: 0 to add, 1 to change, 0 to destroy.


では terraform apply してみます。

│ Error: creating Route in Route Table (rtb-******) with destination (10.0.0.0/16): RouteAlreadyExists: The route identified by 10.0.0.0/16 already exists.
│     status code: 400, request id: ********-****-****-****-************
│ 
│   with aws_route_table.example-route-table,
│   on main.tf line 221, in resource "aws_route_table" "example-route-table":
│  221: resource "aws_route_table" "example-route-table" {

送信先が10.0.0.0/16のrouteはすでに存在するため変更できないようです。これでは、ターゲットを変更できません。

ところが、公式ドキュメントにはしっかりとこういった場合の手順が明記されていました。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table#adopting-an-existing-local-route

Adopting an existing local route AWS creates certain routes that the AWS provider mostly ignores. You can manage them by importing or adopting them. See Import below for information on importing. This example shows adopting a route and then updating its target.

そのままではAWSの側で暗黙的に作られるlocal routeをTerraform AWS Providerが管理できないので、変更前に一度、localのrouteブロックを追加する必要があるようです。


ちなみに、このときのaws_route_tableのtfstateは以下のように、route が空配列になっています。

    {
      "mode": "managed",
      "type": "aws_route_table",
      "name": "example-route-table",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "(Route TableのARN)",
            "id": "rtb-******",
            "owner_id": "(AWSのアカウントID)",
            "propagating_vgws": [],
            "route": [],
            "tags": {
              "Name": "example-route-table"
            },
            "tags_all": {
              "Name": "example-route-table"
            },
            "timeouts": null,
            "vpc_id": "vpc-******"
          },
          "sensitive_attributes": [],
          "private": (略),
          "dependencies": [
            "aws_vpc.main"
          ]
        }
      ]
    },



routeを追加した状態のリソース定義は以下の通りとなりました。

resource "aws_route_table" "example-route-table" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }


  tags = {
    Name = "example-route-table"
  }
}

この状態でterraform applyを実行した結果の出力は以下の通りです。Terraform上ではchange扱いとなっていますが、実際にはtfstateのみ変更されるようです。

Terraform will perform the following actions:

  # aws_route_table.example-route-table will be updated in-place
  ~ resource "aws_route_table" "example-route-table" {
        id               =  "(AWSのアカウントID)"
      ~ route            = [
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "10.0.0.0/16"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = "local"
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = ""
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name" = "example-route-table"
        }
        # (5 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_route_table.example-route-table: Modifying... [id=rtb-*****]
aws_route_table.example-route-table: Modifications complete after 0s [id=rtb-********]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

実行後のコンソールの様子 terraform apply後もAWSコンソール上からはRoute Tableに変更がない様子がわかる画像

コンソール上では特に先ほどと変わった様子はなさそうです。 一方で、tfstateは以下のようにrouteの情報が加わりました。

tfstateの様子

{
    "mode": "managed",
    "type": "aws_route_table",
    "name": "example-route-table",
    "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
    "instances": [
      {
        "schema_version": 0,
        "attributes": {
          "arn": "(Route TableのARN)",
          "id": "rtb-******",
          "owner_id":  "(AWSのアカウントID)"
          "propagating_vgws": [],
          "route": [
            {
              "carrier_gateway_id": "",
              "cidr_block": "10.0.0.0/16",
              "core_network_arn": "",
              "destination_prefix_list_id": "",
              "egress_only_gateway_id": "",
              "gateway_id": "local",
              "ipv6_cidr_block": "",
              "local_gateway_id": "",
              "nat_gateway_id": "",
              "network_interface_id": "",
              "transit_gateway_id": "",
              "vpc_endpoint_id": "",
              "vpc_peering_connection_id": ""
            }
          ],
          "tags": {
            "Name": "example-route-table"
          },
          "tags_all": {
            "Name": "example-route-table"
          },
          "timeouts": null,
          "vpc_id": "vpc-******"
        },
        "sensitive_attributes": [],
        "private": (略),
        "dependencies": [
          "aws_vpc.main"
        ]
      }
    ]
  },



ここで当初の目的通り、10.0.0.0/16のターゲットをlocalからNAT Gatewayに向けてみます。

resource "aws_route_table" "example-route-table" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = aws_nat_gateway.main.id
  }


  tags = {
    Name = "example-route-table"
  }
}

この状態でterraform applyを実行します。

Terraform will perform the following actions:
  # aws_route_table.example-route-table will be updated in-place
  ~ resource "aws_route_table" "example-route-table" {
        id               = rtb-******
      ~ route            = [
          - {
              - carrier_gateway_id         = ""
              - cidr_block                 = "10.0.0.0/16"
              - core_network_arn           = ""
              - destination_prefix_list_id = ""
              - egress_only_gateway_id     = ""
              - gateway_id                 = "local"
              - ipv6_cidr_block            = ""
              - local_gateway_id           = ""
              - nat_gateway_id             = ""
              - network_interface_id       = ""
              - transit_gateway_id         = ""
              - vpc_endpoint_id            = ""
              - vpc_peering_connection_id  = ""
            },
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "10.0.0.0/16"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 =  "nat-******"
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = ""
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
        tags             = {
            "Name" = "example-route-table"
        }
        # (5 unchanged attributes hidden)
    }
Plan: 0 to add, 1 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
  Enter a value: yes
aws_route_table.example-route-table: Modifying... [id=rtb-*****]
aws_route_table.example-route-table: Modifications complete after 0s [id=rtb-*****]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

成功しました。差分も意図したものになっていそうです。

terraform apply後にAWSコンソール上からもRoute Tableに意図した変更が確認できる画像

tfstateも意図したものになっているようです。

  {
    "mode": "managed",
    "type": "aws_route_table",
    "name": "example-route-table",
    "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
    "instances": [
      {
        "schema_version": 0,
        "attributes": {
          "arn": "(Route TableのARN)",
          "id": "rtb-******",
          "owner_id": "(AWSのアカウントID)",
          "propagating_vgws": [],
          "route": [
            {
              "carrier_gateway_id": "",
              "cidr_block": "10.0.0.0/16",
              "core_network_arn": "",
              "destination_prefix_list_id": "",
              "egress_only_gateway_id": "",
              "gateway_id": "",
              "ipv6_cidr_block": "",
              "local_gateway_id": "",
              "nat_gateway_id": "nat-******",
              "network_interface_id": "",
              "transit_gateway_id": "",
              "vpc_endpoint_id": "",
              "vpc_peering_connection_id": ""
            }
          ],
          "tags": {
            "Name": "example-route-table"
          },
          "tags_all": {
            "Name": "example-route-table"
          },
          "timeouts": null,
          "vpc_id": "vpc-******"
        },
        "sensitive_attributes": [],
        "private": (略),
        "dependencies": [
          "aws_nat_gateway.main",
          "aws_vpc.main"
        ]
      }
    ]
  },

元に戻す時はgateway_idを"local"に戻せばよいとのことです。



このように、Terraformの公式AWS Providerはドキュメントがしっかりと整備されているため、使用するresourceについては一度目を通しておくと躓くことが少なくなるかもしれません。 また、Terraformを利用する際は常にtfstateがどのような状態に変化するかを想定・確認しつつ利用することが大事だと改めて思いました。



9日目の明日は、nil0kaさんが基盤組織に異動した話について書いてくれるようです。お楽しみに!