AWS事業本部 コンサルティング部の鈴木です。
なぜTerraformを始めるか
CloudFormationはAWS公式の機能と言う理由で選定に特に深い理由はなく使用していました。
実際に触っていて良かったと感じたのは以下の通りです。
- json/yamlのためベースのフォーマットの新規学習が不要で始めやすかった
- 自分は
{}
のネストに悩まされずコメントも書けるのでyamlを使用
- 自分は
- デプロイはマネジメントコンソールから可能でCLI慣れしてない人でも使いやすい
- デプロイした時の情報がCloudFormationサービス上に残る
- リソースタグでEC2等のサービス側から見てもCloudFormationで作ったことが判別できる
- 公式ドキュメントが部分的にだが日本語で英語苦手な人も始めやすい
- 以前は殆ど日本語対応していたのですが気づいたら各種リソースの翻訳はなくなっていました
一方である程度組み込もうとなるとお手軽というメリットは損なわれがちになります。
- 組込み関数はあまり機能が多くない
- 例えば複数EC2インスタンスを立てる場合ループが無いので列挙が必要でコード量が嵩み見通しが悪くなる
- マクロで独自に処理は拡張可能ではあるが別途コードを書く必要がある
- yamlの機能であるアンカーやエイリアスは使えない
- スタックをネストしようと思うとGUIでは少し手間になる
- 別途S3に子テンプレートのアップロードをする必要がある
特にマクロは本来作成したいリソース以外にIaCを実現するためのサービス・コード管理が発生するため避けたい要素です。
複雑なシステムになるほどその管理も増えていくことが考えられるため
今回標準の機能が充実しているTerraformを構築手段の一つとして習得することにしました。
CDKではなくTerraformを採用した理由は、
CloudFormationでは未対応、Terraformでは対応というサービス利用時の選択肢のためです。
(CDKは裏ではCloudFormationが動いています)
上記の例としてSESのドメイン認証が挙げられます。
一応CloudFormationでもカスタムリソースで拡張すれば対応可能ですがマクロ同様そのコード管理が必要になります。
https://github.com/medmunds/aws-cfn-ses-domain
準備
環境
- MacOS Monterey(Apple M2)
- Terraform v1.3.2
作成イメージ
以下のCloudFormationテンプレート相当のものをTerraformで作成します。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
VpcCidrBlock:
Type: String
SubnetCidrBlock:
Type: String
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidrBlock
Tags:
- Key: Name
Value: "terraform-sample"
Subnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: !Ref SubnetCidrBlock
Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-078296f82eb463377
SubnetId: !Ref Subnet
Tags:
- Key: Name
Value: "sample-instance"
インストール
terraform
コマンドのインストールはbrew
経由で行いました。
チュートリアルでは外部リポジトリを追加していますが
自分の環境では特に追加せずにインストールできました。
AWSのアクセスキーの設定も必要ですが既に設定されているものとして省略します。
$ brew install terraform
...(略)
$ terraform --version
Terraform v1.3.2
on darwin_arm64
フォルダ構成
以下のページを参考にしました。
Terraform ベストプラクティスを整理してみました。
「それ、どこに出しても恥ずかしくないTerraformコードになってるか?」
ネストスタックを利用するようにモジュールという単位で分割することが本来のベストプラクティスの様ですが、今回はお試しで小さく作りたかったためにフラットな構成にしました。
モジュールを利用せず同一ディレクトリの範囲であれば
全ての*.tf
ファイルを読み込むため好きなように分割ができます。
.
├── ec2.tf //EC2定義
├── provider.tf //プロバイダー情報
├── subnet.tf //subnet定義
├── terraform.tfvars //変数入力値
├── variable.tf //変数定義
└── vpc.tf //VPC定義
利用したブロック
今回作成するにあたり利用したブロックは以下の通りです。
- resource
- CloudFormationにおける
Resources
セクションに記載する各種リソース相当のもの
- CloudFormationにおける
- variable
- CloudFormationにおける
Parameters
セクションに記載する変数相当のもの - 入力値はコマンド上で直接渡すか
*.tfvars
ファイルに記載して渡す- ファイル名を
terraform.tfvars(.json)
とすると実行時に未指定でも自動で読み込んでくれる
- ファイル名を
- 今回は使用しなかったが外部から入力が不要な場合はlocalsが良い
- CloudFormationにおける
Mappings
セクションに記載する定数相当のもの
- CloudFormationにおける
- CloudFormationにおける
- provider
- 読み込むプロバイダー毎の設定を記載
- プロバイダーはプラグインの様なもの
- 何のプロバイダーを読み込むか自体は
terraform
ブロック側に記載する
- 読み込むプロバイダー毎の設定を記載
- terraform
- 利用するTerraformやproviderのバージョン等のプロジェクトの設定
各種ファイル内容
provider.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "ap-northeast-1"
}
vpc.tf
resource "aws_vpc" "sample" {
cidr_block = var.vpc_cidr
tags = {
Name = "terraform-sample"
}
}
subnet.tf
resource "aws_subnet" "sample" {
vpc_id = aws_vpc.sample.id
cidr_block = var.subnet_cidr
}
ec2.tf
resource "aws_instance" "sample" {
ami = "ami-078296f82eb463377"
instance_type = "t3.micro"
subnet_id = aws_subnet.sample.id
tags = {
Name = "sample-instance"
}
}
variable.tf
variable "vpc_cidr" {
type = string
}
# cidrsubnetで算出すれば入力を省略できるが今回はCloudFormationに合わせて変数からの入力方式をとる
# https://www.terraform.io/language/functions/cidrsubnet
variable "subnet_cidr" {
type = string
}
variable.tfvars
vpc_cidr = "192.168.0.0/16"
subnet_cidr = "192.168.1.0/24"
実行
以下の順序でterraform
コマンドを実行していきます。
init
(プロバイダーのインストール等)validate
(文法チェック)fmt
(コード整形)plan
(実行計画の確認)apply
(デプロイ)destroy
(削除)
apply
時の注意点としてCloudFormationの様に失敗時のロールバック機能がなく、
途中で失敗すると一部のみリソースが作成された状態で終了する可能性があります。
各種コミュニティで情報を見てみるとAWS上のリソースを直接操作して修正する、
もしくはコードを修正して再度apply
を実行し修正するといった別途対応が必要になります。
$ terraform init
Initializing the backend...
...
Terraform has been successfully initialized!
...
$ terraform validate
Success! The configuration is valid.
# 既に整形されている場合、fmtのCLI上の出力は空になります
$ terraform fmt
ec2.tf
$ terraform plan
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:
...
# aws_instance.sample will be created
...(作成されるEC2の情報が表示される)
# aws_subnet.sample will be created
...(作成されるsubnetの情報が表示される)
# aws_vpc.sample will be created
....(作成されるVPCの情報が表示される)
Plan: 3 to add, 0 to change, 0 to destroy.
$ terraform 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
aws_vpc.sample: Creating...
aws_vpc.sample: Creation complete after 1s [id=vpc-xxxx]
aws_subnet.sample: Creating...
aws_subnet.sample: Creation complete after 1s [id=subnet-xxxx]
aws_instance.sample: Creating...
aws_instance.sample: Still creating... [10s elapsed]
aws_instance.sample: Creation complete after 13s [id=i-xxxxx]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Webコンソール上で指定したEC2インスタンスが作成されていることが確認できます。
実行後terraform.tfstate
という状態を保存したStateファイルが生成されます。
これを削除してしまうと再実行時に新規作成扱いになってしまうので取り扱いは注意が必要です。
terraform.state.backup
はapply
前のStateファイルです。
$ terraform plan
aws_vpc.sample: Refreshing state... [id=vpc-xxxx]
aws_subnet.sample: Refreshing state... [id=subnet-xxxx]
aws_instance.sample: Refreshing state... [id=i-xxxx]
No changes. Your infrastructure matches the configuration.
# terraform.stateを無くして再実行すると新規リソース追加扱い
# 実際にapplyするとplan通り新規作成になる(前のリソースは残ったまま)
$ mv terraform.tfstate terraform.tfstate.bk
$ terraform plan
...
Plan: 3 to add, 0 to change, 0 to destroy.
作成したリソースが確認できたのでdestroyを実行して削除します。
# Stateファイルを元に戻してから実行する
$ mv terraform.tfstate.bk terraform.tfstate
$ terraform destroy
Plan: 0 to add, 0 to change, 3 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_instance.sample: Destroying... [id=i-xxxx]
aws_instance.sample: Still destroying... [id=i-xxxx, 10s elapsed]
aws_instance.sample: Still destroying... [id=i-xxxx, 20s elapsed]
aws_instance.sample: Still destroying... [id=i-xxxx, 30s elapsed]
aws_instance.sample: Still destroying... [id=i-xxxx, 40s elapsed]
aws_instance.sample: Still destroying... [id=i-xxxx, 50s elapsed]
aws_instance.sample: Destruction complete after 50s
aws_subnet.sample: Destroying... [id=subnet-xxxx]
aws_subnet.sample: Destruction complete after 1s
aws_vpc.sample: Destroying... [id=vpc-xxxx]
aws_vpc.sample: Destruction complete after 0s
Destroy complete! Resources: 3 destroyed.
実際に使用してみて
実際に実行まで試した後のTerraformの印象は以下の通りです。
- 独自フォーマット(HCL)ではあるが記法自体はそこまで難しくはない
- プログラミング言語チックな印象でアプリのコード書いてる人の方が好きそう
- 複雑な処理でもコンパクトにまとめられる
- 関数機能が豊富で標準機能の範囲でも取れる選択肢が多い
- その分ベストな書き方をしようと思うと学習コストが高くなりそうではある
- 変数や属性の指定がオブジェクト指向ライクな階層で直感的にわかりやすい
apply
の実行中に細かい経過が見られるのでデプロイ中の安心感がある- CloudFormationの
deploy
は実行中経過が表示されない(別画面が必要) - 定期的に処理経過時間が表示されるのでより経過がわかりやすい
- 実行結果を残した時に他の人が見ても経過が伝わりやすい
- CloudFormationの
- 複雑な処理をせず列挙するだけであればCloudFormationの方が直感的にわかりやすい
- yamlフォーマットが構造的にマークアップ言語ライクな構造表現フォーマットの為
- デプロイ周りは若干機能不足な面を感じる
- ロールバック機能が存在しない
- IAM Userのアクセスキー管理が必要になる(IAM Roleを使う場合別途仕組みが必要)
CloudFormationはできること少ないけど見やすい・わかりやすい、
Terraformはできること多いけど重厚気味と感じました。
少し話は外れるのですが過去に関わったことあるコーディングルールで
複雑な処理は禁止、物量は多くなってもプログラミング初心者でも理解できる処理にするという方針がありました。
例) COMPUTE文は複数の意味があるので禁止(四則演算ができる)、代わりにADD文(足し算専用処理)等を使う
もしどちらか一方のみを取得する場合
コード量が増えてもこういった誰にでもできる(できる人に依存しない)を目指すのであればCloudFormationの方がメリットが大きく、
中身が多少複雑でもより汎用性の高いモジュールを作り資産化していく、コンパクトなコードを目指すということであればTerraformの方がメリットが大きそうです。
できることが多いというのはメリットな一方、
適切に取り扱わないとデメリットも増えていきますので自分の周りの環境も含め選択を検討していくのが理想です。
自分としては趣味も含めると今後別のクラウドを使うこともあると思うので
Terraformのも選択肢として持ちつつ、サクッと書きたいような時はCloudFormationで書いてくという形をとることができればと考えています。
メリット・デメリットは実際触ってみると個人で感じ方は違うと思いますので、
CloudFormationしかこれまで触ったことない人も是非一度Terraformを触ってみてください。