クラウドコントロールAPIをTerraformのaws providerから使ってみた
2021年9月30日にクラウドコントロールAPIがGA(一般提供開始)しました。
従来、各サービスごとにバラバラだったAPIの使い方を、一貫した方法で使えるようにしたAPIです。またAWSサービスのみならず CloudFormation Registry で利用可能なサードパーティー製ソリューションもこのAPIを通して利用可能です。
さて、Terraformのaws providerも早速このクラウドコントロールAPIに対応しています。Version 3.61.0にてResourceとData Sourceがそれぞれ追加されました。
- aws_cloudcontrolapi_resource | Resources | hashicorp/aws | Terraform Registry
- aws_cloudcontrolapi_resource | Data Sources | hashicorp/aws | Terraform Registry
今回はこのResourceを使って、2AZにそれぞれPublicとPrivateサブネットを持つような一般的なVPC構成を作ってみたいと思います。
基本的な書き方
使うResourceは前述のaws_cloudcontrolapi_resource
のみです。このResourceのattribute type_name
に異なる値を指定することで様々なリソースをプロビジョニングすることができます。リソースの各プロパティ値はdesired_state
attributeにJSON形式で渡します。
そして、type_name
に指定する値はCloudFormation(CFn)のリソースタイプネームで、desired_state
以下で指定する値もCFnで各リソースを作成するときのProperties
以下の値です。
例としてまずは(サブネット等のVPC内部リソースは置いておいて、)一旦VPCだけを作成してみましょう。
desired_state
以下をHCL形式で書きたい場合は以下のようにjsonencode
関数で囲いましょう。
resource "aws_cloudcontrolapi_resource" "vpc" { type_name = "AWS::EC2::VPC" desired_state = jsonencode({ CidrBlock = "192.168.0.0/16" EnableDnsSupport = true EnableDnsHostnames = true InstanceTenancy = "default" Tags = [{ Key = "Name" Value = "plain-vpc" }] }) }
もしくはCFnライクにYAMLで書きたい場合は以下のような書き方もできます。
resource "aws_cloudcontrolapi_resource" "vpc" { type_name = "AWS::EC2::VPC" desired_state = jsonencode(yamldecode( <<-EOT CidrBlock: "192.168.0.0/16" EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default Tags: - Key: Name Value: "plain-vpc" EOT )) }
残りのリソース…対応してなかった
続いてInternet Gatewayを作成しようと以下コードを書いてterraform apply
しました。
resource "aws_cloudcontrolapi_resource" "igw" { type_name = "AWS::EC2::InternetGateway" desired_state = jsonencode(yamldecode( <<-EOT Tags: - Key: Name Value: "basic-vpc-igw" EOT )) } resource "aws_cloudcontrolapi_resource" "igw_attachment" { type_name = "AWS::EC2::VPCGatewayAttachment" desired_state = jsonencode(yamldecode( <<-EOT InternetGatewayId: ${aws_cloudcontrolapi_resource.igw.id} VpcId: ${aws_cloudcontrolapi_resource.vpc.id} EOT )) }
すると…エラーになりました。
╷ │ Error: error creating Cloud Control API Resource (AWS::EC2::InternetGateway): UnsupportedActionException: Resource type AWS::EC2::InternetGateway does not support CREATE action │ │ with aws_cloudcontrolapi_resource.igw, │ on vpc.tf line 17, in resource "aws_cloudcontrolapi_resource" "igw": │ 17: resource "aws_cloudcontrolapi_resource" "igw" { │ ╵
対応リソースを確認する方法
Internet GatewayはクラウドコントロールAPI未対応でした。どのリソースタイプが対応済なのかは以下の方法で確認可能です。
AWS CLIで
aws cloudformation describe-type
で各リソースの対応状況を確認できます。type
引数にRESOURCE
を、 type-name
に前述aws_cloudcontrolapi_resource
Resourceのtype_name
attribute値(=CFnのリソースタイプネーム値)を指定します。
このときの返り値ProvisioningType
の値がFULLY_MUTABLE
もしくはIMMUTABLE
の場合、クラウドコントロールAPIでプロビジョニングできる、つまりaws_cloudcontrolapi_resource
Resourceでプロビジョニングできるようです。
VPCはFULLY_MUTABLE
になっているのでプロビジョニングできます。
{ "Arn": "arn:aws:cloudformation:ap-northeast-1::type/resource/AWS-EC2-VPC", "Type": "RESOURCE", "TypeName": "AWS::EC2::VPC", "IsDefaultVersion": true, "Description": "Resource Type definition for AWS::EC2::VPC", "Schema": "(長い&パースされていないので割愛)", "ProvisioningType": "FULLY_MUTABLE", "DeprecatedStatus": "LIVE", "Visibility": "PUBLIC", "TimeCreated": "2021-08-20T20:25:24.577000+00:00" }
さきほどエラーになったInternet GatewayはNON_PROVISIONABLE
ですね。
{ "Arn": "arn:aws:cloudformation:ap-northeast-1::type/resource/AWS-EC2-InternetGateway", "Type": "RESOURCE", "TypeName": "AWS::EC2::InternetGateway", "IsDefaultVersion": true, "Schema": "(長い&パースされていないので割愛)", "ProvisioningType": "NON_PROVISIONABLE", "DeprecatedStatus": "LIVE", "Visibility": "PUBLIC", "TimeCreated": "2019-11-15T15:43:30.265000+00:00" }
ドキュメントで
以下ページにクラウドコントロールAPI対応リソースタイプ一覧がまとまっています。が、先程プロビジョニングできたVPC(AWS::EC2::VPC
)が載っていないので、実態と差があるかもしれません。
残りのリソースもできる限りクラウドコントロールAPI使ってみた
さて、クラウドコントロールAPIが対応していないリソースについては、(aws_cloudcontrolapi_resource
ではなく、)既存のResourceを使う方針に転換し、再度基本的なVPC構成を作成してみました。結果、クラウドコントロールAPIが使えたのは以下3種のリソースのみでした。
- VPC
- Route Table
- サブネットとRoute Tableの紐付け
resource "aws_cloudcontrolapi_resource" "vpc" { type_name = "AWS::EC2::VPC" desired_state = jsonencode(yamldecode( <<-EOT CidrBlock: "192.168.0.0/16" EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default Tags: - Key: Name Value: "basic-vpc" EOT )) } # public subnet resource "aws_internet_gateway" "igw" { vpc_id = aws_cloudcontrolapi_resource.vpc.id tags = { Name = "basic-vpc-igw" } } data "aws_availability_zones" "az" {} resource "aws_subnet" "public" { count = 2 vpc_id = aws_cloudcontrolapi_resource.vpc.id cidr_block = cidrsubnet(jsondecode(aws_cloudcontrolapi_resource.vpc.desired_state).CidrBlock, 8, count.index) availability_zone = data.aws_availability_zones.az.names[count.index] map_public_ip_on_launch = true tags = { Name = "basic-vpc-public-subnet-${count.index + 1}" } } resource "aws_cloudcontrolapi_resource" "public_route_table" { type_name = "AWS::EC2::RouteTable" desired_state = jsonencode(yamldecode( <<-EOT VpcId: ${aws_cloudcontrolapi_resource.vpc.id} Tags: - Key: Name Value: "basic-vpc-public-subnet-rtb" EOT )) } resource "aws_route" "public" { route_table_id = aws_cloudcontrolapi_resource.public_route_table.id gateway_id = aws_internet_gateway.igw.id destination_cidr_block = "0.0.0.0/0" } resource "aws_cloudcontrolapi_resource" "public_subnet_route_table_association" { count = 2 type_name = "AWS::EC2::SubnetRouteTableAssociation" desired_state = jsonencode(yamldecode( <<-EOT SubnetId: ${aws_subnet.public.*.id[count.index]} RouteTableId: ${aws_cloudcontrolapi_resource.public_route_table.id} EOT )) } # private subnet resource "aws_subnet" "private" { count = 2 vpc_id = aws_cloudcontrolapi_resource.vpc.id cidr_block = cidrsubnet(jsondecode(aws_cloudcontrolapi_resource.vpc.desired_state).CidrBlock, 8, count.index + 2) availability_zone = data.aws_availability_zones.az.names[count.index] map_public_ip_on_launch = false tags = { Name = "basic-vpc-private-subnet-${count.index + 1}" } } resource "aws_cloudcontrolapi_resource" "private_route_table" { type_name = "AWS::EC2::RouteTable" desired_state = jsonencode(yamldecode( <<-EOT VpcId: ${aws_cloudcontrolapi_resource.vpc.id} Tags: - Key: Name Value: "basic-vpc-private-subnet-rtb" EOT )) } resource "aws_cloudcontrolapi_resource" "private_subnet_route_table_association" { count = 2 type_name = "AWS::EC2::SubnetRouteTableAssociation" desired_state = jsonencode(yamldecode( <<-EOT SubnetId: ${aws_subnet.private.*.id[count.index]} RouteTableId: ${aws_cloudcontrolapi_resource.private_route_table.id} EOT )) }
感想
今回は敢えてやってみましたが、VPCを作成するという要件の場合はaws_cloudcontrolapi_resource
を使う必要性は薄いと思います。まだまだ非対応リソースがたくさんあるので、既存のResourceの使用に統一したほうが書きやすい・読みやすいと思います。特にaws_cloudcontrolapi_resource
のdesired_state
Attribute以下の値を参照したい場合、desired_state
はJSON型になっているので一度jsondecode関数をかます必要があり、可読性を下げると思います。
(hoge = jsondecode(aws_cloudcontrolapi_resource.vpc.desired_state).CidrBlock
←こういう感じです)
ユースケース
まずは、作りたいリソースがまだTerraform aws provider未対応の場合です。今後クラウドコントロールAPIは基本的に新リソースのローンチ時にそのリソースをサポートするとAWS News Blogで述べられているので、ローンチ後すぐさまTerraformでIaC化したい、という場合には活躍するでしょう。
ですが、Terraform aws providerの開発も活発なので、数日、数週待てば新リソース専用のResourceが提供されている印象があります。場合によっては事前にHashiCorp社にリリース情報が共有されていて、リリース当日にTerraformが対応している場合もあります。ですので、この「aws provider未対応のとき」というシーンは非常に限定的なものになるかもしれません。。
もうひとつ考えられるユースケースは、CFnに慣れ親しんだ開発者がTerraformを始めるとき、です。リソース単位はCFnのリソースタイプですし、desired_state
以下のプロパティもまんまCFnのものなので、CFnを良くご存じの方は書きやすいかと思います。
が、!Ref
とか!Sub
とかのCFnの構文は使えないので、Terraform流に書き換える必要があります。さらに前述の通り現状aws_cloudcontrolapi_resource
で作れるリソースは非常に限定的なので、今すぐ始めるのはしんどそうですね。またそもそもですが、せっかくTerraform始めるんだったら、普通にaws_cloudcontrolapi_resource
使わずに書けるように勉強したほうが良いとも思います。すみません、このユースケースもちょっと苦しいですかね。。今後のアップデートでさらに便利になっていくのを期待したいと思います。