この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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で書きたい場合は以下のような書き方もできます。
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
使わずに書けるように勉強したほうが良いとも思います。すみません、このユースケースもちょっと苦しいですかね。。今後のアップデートでさらに便利になっていくのを期待したいと思います。