はじめに

Terraformを使っていると、どのプロジェクトでも流用することができる汎用的なテンプレートを作成したいと思ってしまうことが間間あります。
今回は、三項演算子を使用し、複数条件に応じたリソースを作成できるようなTerraformの記述を展開していきたいと思います。

想定シナリオ

三項演算子を活用する上でAWSリソースの中の、VPCエンドポイントを例にしてTerraformテンプレートを作成することを想定します。
VPCエンドポイントにはGateway EndpointInterface Endpointなどを作成することで、インターネット経由で通信することなく、プライベートネットワークからAWSリソースにアクセスすることができます。
この2種類のエンドポイントには、定義するパラメータも異なってくるため、1つ1つ解説していきます。

先ずはじめに、Gateway Endpoint では、ルートテーブルにS3やDynamoDBのプレフィックスリストを指定して、
エンドポイント→ AWSサービスにアクセスすることができます。(S3やDynamoDBのゲートウェイ経由でのアクセスとなります。)
Terraformのテンプレートは、route_table_idsでルートテーブルを定義し、以下のようになります。

resource "aws_vpc_endpoint" "s3" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.us-west-2.s3"
  route_table_ids = [
    aws_route_table.pri.id
  ]
}

次に、Interface Endpoint では、サブネットにネットワークインターフェースを作成し、ネットワークインターフェース経由でエンドポイント→ AWSサービスにアクセスすることができます。
ネットワークインターフェースのため、作成先のサブネットIDとアタッチするセキュリティグループが必要になります。
Terraformのテンプレートは、subnet_idsaws_security_groupを定義し、以下のようになります。
また、パラメータprivate_dns_enabledは、Interface型のエンドポイントでのみ設定が可能になります。(Gateway型の場合は定義しない。)

resource "aws_vpc_endpoint" "ec2" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-west-2.ec2"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.sg1.id,
  ]

  subnet_ids = [
    aws_subnet.edp1.id,
    aws_subnet.edp2.id,
  ]

  private_dns_enabled = true
}

このようにエンドポイントの種類ごとに設定するパラメータが異なるため、
Gateway型とInterface型で設定するパラメータが変わるようにテンプレートを作成する方法を次で解説します。

三項演算子によるTerraform での条件分岐方法について

Terraform上ではif elseによる条件分岐を使用できないため、三項演算子を使用することで条件分岐を定義することができます。
三項演算子は、分岐条件 ? 正の値 : 負の値で表現することができます。
例えば、以下の値は、変数「service_name」が、DynamoDB or S3の場合かつ、変数「rtb_ids(ルートテーブルIDの定義)」がnullでない場合、エンドポイントタイプをGatewayとしそれ以外をInterfaceとすることができます。

locals {
  edp_type = ( (var.service_name == "dynamodb" || var.service_name == "s3" ) && var.rtb_ids != null ) ? "Gateway" : "Interface"
}

上記で定義したedp_typeを用いて、edp_typeがGatewayかInterfaceかで、サブネットやルートテーブルを定義します。不要な変数はnullなどを与えるようにします。

locals {
  # Define Interface Endpoint
  interface_edp_subnet_ids = local.edp_type == "Gateway" ? null : var.subnet_ids
  interface_edp_sg_ids = local.edp_type == "Gateway" ? null : var.sg_ids

  # Define Gateway Endpoint
  gateway_edp_rtb_ids = local.edp_type == "Gateway" ? var.rtb_ids : null
}

Terraform 条件分岐デモ

デモとしてフォルダ構成は以下の通り、module化して呼び出しできる構成にします。

.
│
├── modules/
│   └── vpc/
│       └── endpoint/
│           ├── main.tf
│           └── variables.tf
└── main.tf

./modules/vpc/endpoint/variables.tfの内容は以下の通りで、Moduleに必要な変数を定義します。

variable "vpc_id" {
  type = string
  description = "VPC ID"
}

variable "subnet_ids" {
  type = list(string)
  default = null
  description = "List of Subnet IDs"
}

variable "sg_ids" {
  type = list(string)
  default = null
  description = "List of Security Group IDs"
}

variable "rtb_ids" {
  type = list(string)
  default = null
  description = "List of Route Table IDs"
}

variable "service_name" {
  type = string
  description = "AWS Service Name"
}

./modules/vpc/endpoint/main.tfの内容は以下の通りです。
特にS3の場合はGateway型とInterface型の2種類存在するので、ルートテーブルを明示的に指定されたらGateway型、それ以外の場合はInterface型とします。
localsに条件分岐を記載することで、resource部分の記述をシンプルに記載が可能になります。

data "aws_region" "current" {}

locals {
  edp_type = ( (var.service_name == "dynamodb" || var.service_name == "s3" ) && var.rtb_ids != null ) ? "Gateway" : "Interface"
  interface_edp_subnet_ids = local.edp_type == "Gateway" ? null : var.subnet_ids
  interface_edp_sg_ids = local.edp_type == "Gateway" ? null : var.sg_ids
  gateway_edp_rtb_ids = local.edp_type == "Gateway" ? var.rtb_ids : null
}

resource "aws_vpc_endpoint" "main" {
  vpc_id       = var.vpc_id
  service_name = format("com.amazonaws.%s.%s", data.aws_region.current.name, var.service_name)

  vpc_endpoint_type = local.edp_type

  subnet_ids = local.interface_edp_subnet_ids
  security_group_ids = local.interface_edp_sg_ids
  route_table_ids   = local.gateway_edp_rtb_ids
  private_dns_enabled = local.edp_type == "Gateway" ? null : true

  tags = {
    Name = format("%s-endpoint", var.service_name)
  }
}

./main.tfでmoduleで定義したリソースを呼び出します。

# Gateway Endpoint
module "edp_gateway_s3" {
    source = "./modules/vpc/endpoint"
    vpc_id = "vpc-xxx"
    rtb_ids = ["rtb-xxx"]
    service_name = "s3"
}


# Interface Endpoint
module "edp_interface_ec2" {
    source = "./modules/vpc/endpoint"
    vpc_id = "vpc-xxx"
    subnet_ids = [
        "subnet-xxx",
        "subnet-yyy",
    ]
    sg_ids = [
        "sg-xxx",
        "sg-yyy",
    ]
    service_name = "ec2"
}

module "edp_interface_s3" {
    source = "./modules/vpc/endpoint"
    vpc_id = "vpc-xxx"
    subnet_ids = [
        "subnet-xxx",
        "subnet-yyy",
    ]
    sg_ids = [
        "sg-xxx",
        "sg-yyy",
    ]
    service_name = "s3"
}

上記の内容を全てテンプレートにて定義したら、terraform applyを実行します。

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.edp_gateway_s3.aws_vpc_endpoint.main will be created
  + resource "aws_vpc_endpoint" "main" {
      + arn                   = (known after apply)
      + cidr_blocks           = (known after apply)
      + dns_entry             = (known after apply)
      + id                    = (known after apply)
      + ip_address_type       = (known after apply)
      + network_interface_ids = (known after apply)
      + owner_id              = (known after apply)
      + policy                = (known after apply)
      + prefix_list_id        = (known after apply)
      + private_dns_enabled   = (known after apply)
      + requester_managed     = (known after apply)
      + route_table_ids       = [
          + "rtb-xxx",
        ]
      + security_group_ids    = (known after apply)
      + service_name          = "com.amazonaws.ap-northeast-1.s3"
      + service_region        = (known after apply)
      + state                 = (known after apply)
      + subnet_ids            = (known after apply)
      + tags                  = {
          + "Name" = "s3-endpoint"
        }
      + tags_all              = {
          + "Name" = "s3-endpoint"
        }
      + vpc_endpoint_type     = "Gateway"
      + vpc_id                = "vpc-xxx"

      + dns_options {
          + dns_record_ip_type                             = (known after apply)
          + private_dns_only_for_inbound_resolver_endpoint = (known after apply)
        }

      + subnet_configuration {
          + ipv4      = (known after apply)
          + ipv6      = (known after apply)
          + subnet_id = (known after apply)
        }
    }

  # module.edp_interface_ec2.aws_vpc_endpoint.main will be created
  + resource "aws_vpc_endpoint" "main" {
      + arn                   = (known after apply)
      + cidr_blocks           = (known after apply)
      + dns_entry             = (known after apply)
      + id                    = (known after apply)
      + ip_address_type       = (known after apply)
      + network_interface_ids = (known after apply)
      + owner_id              = (known after apply)
      + policy                = (known after apply)
      + prefix_list_id        = (known after apply)
      + private_dns_enabled   = true
      + requester_managed     = (known after apply)
      + route_table_ids       = (known after apply)
      + security_group_ids    = [
          + "sg-xxx",
          + "sg-yyy",
        ]
      + service_name          = "com.amazonaws.ap-northeast-1.ec2"
      + service_region        = (known after apply)
      + state                 = (known after apply)
      + subnet_ids            = [
          + "subnet-xxx",
          + "subnet-yyy",
        ]
      + tags                  = {
          + "Name" = "ec2-endpoint"
        }
      + tags_all              = {
          + "Name" = "ec2-endpoint"
        }
      + vpc_endpoint_type     = "Interface"
      + vpc_id                = "vpc-xxx"

      + dns_options {
          + dns_record_ip_type                             = (known after apply)
          + private_dns_only_for_inbound_resolver_endpoint = (known after apply)
        }

      + subnet_configuration {
          + ipv4      = (known after apply)
          + ipv6      = (known after apply)
          + subnet_id = (known after apply)
        }
    }

  # module.edp_interface_s3.aws_vpc_endpoint.main will be created
  + resource "aws_vpc_endpoint" "main" {
      + arn                   = (known after apply)
      + cidr_blocks           = (known after apply)
      + dns_entry             = (known after apply)
      + id                    = (known after apply)
      + ip_address_type       = (known after apply)
      + network_interface_ids = (known after apply)
      + owner_id              = (known after apply)
      + policy                = (known after apply)
      + prefix_list_id        = (known after apply)
      + private_dns_enabled   = true
      + requester_managed     = (known after apply)
      + route_table_ids       = (known after apply)
      + security_group_ids    = [
          + "sg-xxx",
          + "sg-yyy",
        ]
      + service_name          = "com.amazonaws.ap-northeast-1.s3"
      + service_region        = (known after apply)
      + state                 = (known after apply)
      + subnet_ids            = [
          + "subnet-xxx",
          + "subnet-yyy",
        ]
      + tags                  = {
          + "Name" = "s3-endpoint"
        }
      + tags_all              = {
          + "Name" = "s3-endpoint"
        }
      + vpc_endpoint_type     = "Interface"
      + vpc_id                = "vpc-xxx"

      + dns_options {
          + dns_record_ip_type                             = (known after apply)
          + private_dns_only_for_inbound_resolver_endpoint = (known after apply)
        }

      + subnet_configuration {
          + ipv4      = (known after apply)
          + ipv6      = (known after apply)
          + subnet_id = (known after apply)
        }
    }

Plan: 3 to add, 0 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

module.edp_gateway_s3.aws_vpc_endpoint.main: Creating...
module.edp_interface_s3.aws_vpc_endpoint.main: Creating...
module.edp_interface_ec2.aws_vpc_endpoint.main: Creating...
module.edp_gateway_s3.aws_vpc_endpoint.main: Creation complete after 6s [id=vpce-xxx]
module.edp_interface_ec2.aws_vpc_endpoint.main: Still creating... [10s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [10s elapsed]
module.edp_interface_ec2.aws_vpc_endpoint.main: Still creating... [20s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [20s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [30s elapsed]
module.edp_interface_ec2.aws_vpc_endpoint.main: Still creating... [30s elapsed]
module.edp_interface_ec2.aws_vpc_endpoint.main: Still creating... [40s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [40s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [50s elapsed]
module.edp_interface_ec2.aws_vpc_endpoint.main: Still creating... [50s elapsed]
module.edp_interface_ec2.aws_vpc_endpoint.main: Creation complete after 53s [id=vpce-yyy]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [1m0s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [1m10s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [1m20s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [1m30s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [1m40s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [1m50s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [2m0s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [2m10s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [2m20s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [2m30s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [2m40s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [2m50s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [3m0s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [3m10s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [3m20s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [3m30s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [3m40s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [3m50s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [4m0s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [4m10s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [4m20s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Still creating... [4m30s elapsed]
module.edp_interface_s3.aws_vpc_endpoint.main: Creation complete after 4m35s [id=vpce-zzz]

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

S3のGateway型もInterface型のエンドポイントが作成されていることを確認できればOKです。

三項演算子による条件分岐によって、異なるタイプのVPCエンドポイントの作成を1つのresourceで表現することができました。