CloudFormation vs Terraform
はじめに
CloudFormationとTerraform、どっちが良いの?
「AWSでIaCしたい」 そんなときほぼ確実に発生する議題ではないでしょうか。
この議題に対しては、以下のようなことをチームで話し合うことになるでしょう(僕はなりました)。
「CloudFormationはベンダロックインするからTerraformにしよう、Terraformならマルチクラウドに対応してるってよ!」
「AWSしか使わないならサービス連携面で強い(らしい)CloudFormation使おうよ」
「でもCloudFormationってなんか書きにくいらしいよ?Terraformにしない?」
「CloudFormationはyamlで書けるから学習コスト低いけどTerraformの文法は独自記法だからCloudFormationの方が…」
「あーだ」
「こーだ」
この類の議論は昨今不毛とされています、好きな方でさっさと開発始めた方が良いからです。
ですが、先に色々と考えを巡らせておいたほうが良いのもまた事実です。規模が大きいシステムでは後戻りが難しくなるのでなおさらですね。
そこで本エントリーではCloudFormationとTerraformに、どういった特徴や差異があるのかを比較してみたいと思います。
開発スタートまでの議論を素早く進めるための情報源として活用頂ければ幸いです。
比較観点はもちろん(?)独断と偏見により決定します。
筆者はCloudFormationもTerraformもチョットカイタコトアル程度です。間違ってたらスマン。
※ やさしいマサカリお待ちしてます
情報の時期
2020/07/05 あたり
SW | ver |
---|---|
terraform | v0.12.26 |
awscli | 1.18.79 |
独断と偏見による評価の凡例
凡例 | 意味 |
---|---|
◎ | バッチリ!最高! |
○ | ヨシ! |
△ | ちょい微妙? |
× | 無理ゲー/未対応 |
↓比較してイクゾー!↓
UI
Terraform | CloudFormation |
---|---|
○ | ◎ |
CUI ( terraform コマンド) |
GUI (AWSコンソール) CUI (AWSCLI) |
Terraform
各OS用に terraform
コマンドのバイナリが用意されており、それをダウンロードして使います。
Terraform CLIのバージョンマネージャである tfenv を使うと便利だったりします。
CloudFormation
スタック管理を含む様々な機能をGUIから利用できます。
AWSCLIでの操作もできます。
状態の持ち方
CloudFormationもTerraformも「現在管理しているリソースの状態」を管理する仕組みとなっており、この状態管理の仕方に差異があります。
Terraform | CloudFormation |
---|---|
○ | ◎ |
ファイルで管理 | AWSのサービスで(勝手に)管理 |
Terraform
状態管理用のファイルである terraform.tfstate
ファイルが生成されます。
中身は以下のようなjsonです。
{
"version": 4,
"terraform_version": "0.12.26",
"serial": 4,
"lineage": "f5d048e9-224c-865d-60f3-5e64f9871e89",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "sample",
"provider": "provider.aws",
"instances": [
{
"schema_version": 1,
"attributes": {
"ami": "ami-0a1c2ec61571737db",
"arn": "<ec2-arn>",
"associate_public_ip_address": false,
...省略...
生成先は何も指定しなければローカルになりますが、backendを指定することでS3などに格納することもできます。
チーム開発時はこのようにクラウドストレージに格納するのが良いでしょう。
terraform {
backend "s3" {
bucket = "tfstate-bucket"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
通常、このファイルを直接いじることはありませんが、状況によってはtfstateを直接修正するようなオペレーションが必要になるかもしれません(トラブル時など)。Terraformを扱う上では、tfstateの中身はいつか理解しなければならないものと捉えておいた方が良いでしょう。
参考 → 「一回くらいローカルでterraform applyしてもいいだろう。その時はそう思ってました。」 tfstateをダウングレードしなくてはいけないときの指南書
状態をファイルとして管理しなければならないのは初学者にとっては不便に感じるかもしれませんし、管理対象が増えるのは望むところではないと思います。
しかしファイルを自分たちで管理できるということは、「中身さえ理解すれば柔軟に運用できる」ということでもあるため、一概に悪いことばかりではないです。
実際、↑で紹介したブログではtfstateファイルの内容を理解して変更することにより、当初の目的を達成しつつトラブルを解決することができています。
また、tfstateの存在により運用に柔軟性を持たせることができている例として以下のようなものもあります。
Terraform職人入門: 日々の運用で学んだ知見を淡々とまとめる - tfファイルをリファクタリングする
この例では、本来ならば(?)あるリソースを再作成しないと実施できなかった変更が、tfstateファイルをいじることによりサービス影響無く実施できています。
CloudFormation
状態はAWSサービス側で持ってくれます。AWSコンソールやAWSCLIから参照できます。
現在のリソースの状態以外にも、イベントや適用元であるテンプレートの内容などが確認でき、CloudFormationの方がより多くの情報を保持していることが分かります。
スタック生成時に勝手に状態管理し始めてくれるため、Terraformのように状態ファイルの格納場所などを気にする必要はありません。
CloudFormationの各リソースにはリンクが付いていて該当サービスの画面に飛べたりするので便利です(たまに飛べないのもあるけど)。
また、AWSコンソールでCloudFormationの画面を確認すれば、どういうリソースをどういうテンプレートで管理しているのかを一元的に確認できます。
リソースのタグ付け
AWSリソースにタグを付与するための仕組みについてです。
Terraform | CloudFormation |
---|---|
△ | ◎ |
コードとして表現する必要がある | スタック生成時のオプションで実現可能 |
Terraform
全リソース分、コードとして明示的にタグを付与する必要があります。
CloudFormation
スタック生成時のオプションを用いて、スタック内のすべてのリソースに一貫したタグを付与することができます。
CloudFormationの力で行ってくれるので非常に便利です。
もちろんコード上表現することでリソース個別に付与することも可能です。
変更内容のチェック
コードを実機状態に反映する前に「何が変更されるのか」を確認する方法についてです。
Terraform | CloudFormation |
---|---|
◎ | ○ |
非常に見やすい | 場合によっては見づらい |
Terraform
terraform plan
を実行することで確認できます。出力例を以下に記載しておきます。
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_instance.sample will be updated in-place
~ resource "aws_instance" "sample" {
ami = "ami-0a1c2ec61571737db"
arn = "arn:aws:ec2:ap-northeast-1:521635626865:instance/i-09ecc4b25e3e55b30"
associate_public_ip_address = false
availability_zone = "ap-northeast-1d"
cpu_core_count = 1
cpu_threads_per_core = 1
disable_api_termination = false
ebs_optimized = false
get_password_data = false
hibernation = false
id = "i-09ecc4b25e3e55b30"
instance_state = "running"
~ instance_type = "t2.micro" -> "t3.micro"
ipv6_address_count = 0
ipv6_addresses = []
key_name = "test"
monitoring = false
primary_network_interface_id = "eni-0cfb6130b577006f4"
private_dns = "ip-10-2-0-88.ap-northeast-1.compute.internal"
private_ip = "10.2.0.88"
security_groups = []
source_dest_check = true
subnet_id = "subnet-0b237836abbd38b06"
tags = {}
tenancy = "default"
volume_tags = {}
vpc_security_group_ids = [
"sg-09c58fba174e8af21",
]
credit_specification {
cpu_credits = "standard"
}
metadata_options {
http_endpoint = "enabled"
http_put_response_hop_limit = 1
http_tokens = "optional"
}
root_block_device {
delete_on_termination = true
device_name = "/dev/xvda"
encrypted = false
iops = 100
volume_id = "vol-04a65178ac2a8ada0"
volume_size = 8
volume_type = "gp2"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
CloudFormation
変更セット という機能を使って確認できます。
ざっくりと視覚的に確認可能です。
具体的な変更内容の確認も確認できますが、結局は修正前後のテンプレートの差分を確認する必要があったりします。
総合すると以下のような感じでしょうか。
- 「どのリソースが追加/変更/削除されるのか」くらいのざっくりした確認で良い場合は便利
- ↑に加えて「どのリソースにどのような変更が加わるのか」といった詳細まで確認したい場合はちょっと不便
また、後述する「ネストされたスタック」を用いて構成管理する場合は、これ以上に差分が見づらい状態になります。
一部リソースの再作成
まぁまぁな頻度で発生しがちな、「このインスタンスだけ再作成したいなー」という要望への対応可否についてです。
Terraform | CloudFormation |
---|---|
◎ | × |
taintで簡単に実現可能 | めんどくさい |
Terraform
以下のような方法があります。
-
terraform taint
taintedにマークされたリソースは、次回apply時に再作成される挙動となります。
参考 → 公式ドキュメント - Command: taint -
terraform destroy
terraform destroy
コマンドのオプションに-target
があります。
以下の様にリソースの種別と論理名を与えることで、特定のリソースだけ選んで削除することができます。$ terraform destroy -target=aws_instance.sample
どちらの場合でも、この後 apply
を行えば削除したインスタンスは再作成されるので、非常に簡単に対応できます。
ちなみに特に理由がなければ taint
の方がおすすめです。
CloudFormation
該当リソースに関する記述を削除したテンプレートを用いてスタックを更新し、その後元のテンプレートで改めてスタックを更新する必要がある気がします。
★便利なやり方知ってる方いたら教えて頂けると嬉しいです
外部の設定変更に対する柔軟性
トラブルシュートなどを起因として、「実機が正、構成管理ツールが持っている情報が誤」という状態が発生することもあるかもしれません。
このような状況に対処するときの柔軟性や難易度の観点の比較です。
Terraform | CloudFormation |
---|---|
◎ | × |
柔軟に対応できる | 基本「手動変更の内容をコードにあわせて戻す」 |
ちなみにできるからといって、手動変更とIaCでの構成管理を並行して実施するのは全くおすすめしません。というかアンチパターンです、絶対にやめましょう!!!
Terraform
以下のような手順で、手動で行った変更内容をコード側に反映させる形での対処が可能です。
- 手動変更による差分を
terraform plan
で確認します- planを実行すると、terraformはtfstateの状態を最新に更新します(
terraform refresh
で明示的に更新することもできます)
参考 → 公式ブログ - Detecting and Managing Drift with Terraform
- planを実行すると、terraformはtfstateの状態を最新に更新します(
- 手動変更の内容をコードに反映し、
terraform plan
を再度実行します - 手動変更したパラメータと実機に差分があれば更に修正し、望む形まで修正したら
terraform apply
を実行します- コード-実機間の差分が存在する場合は、ここでコード側を正として更新される動作となります
また、Terraform外での変更をそもそも許容したいようなケースは、コード側で lifecycle
ブロックの ignore_changes
を用いることで実現できます。
参考 → Terraform職人入門: 日々の運用で学んだ知見を淡々とまとめる - リソースの差分を無視する
CloudFormation
構成差分は以下のようなドリフトステータスとして確認できます。
ドリフトが発生していて、それがCloudFormation外の何か行われている場合の対処は基本的に、「元のテンプレートにあわせて実体のリソースの方を修正する」となります。
実際に、CloudFormationで作成したEC2インスタンスを削除後に同じテンプレートでスタックを更新しようとすると、「テンプレートに差分が無いぞ」と怒られ、スタックの更新を行うことはできません。
ドリフトが発生したリソースやどういった差分なのかにより、対処方法は様々です。
参考 → CloudFormaitonで作成したStackを手動で変更した時のメモ
Terraformの lifecycle.ignore_changes
ように、パラメータレベルで差分を無視する方法については見つけられませんでした。
★知ってる方いたら教えて下さい
リソースの削除対策
IaCでリソース管理を行う場合絶対に避けたい「消えちゃいけないリソース消えちゃいました、てへぺろ(・ω<)」についてです。
Terraform | CloudFormation |
---|---|
△ | ◎ |
terraformの仕組みも一応あるが、それ以外も併せて検討しないとキツい | 色々な方法で保護がかけられてナイス |
Terraform
lifecycle
で prevent_destroy
に true
を指定することで、plan時にそのオブジェクトを削除するようなケースをエラーで止めることができます。
Error: Instance cannot be destroyed
on sample.tf line 10:
10: resource "aws_subnet" "private" {
Resource aws_subnet.private has lifecycle.prevent_destroy set, but the plan
calls for this resource to be destroyed. To avoid this error and continue with
the plan, either disable lifecycle.prevent_destroy or reduce the scope of the
plan using the -target flag.
ただし、この方法では対象リソースが消えるようなplanに対しては全て処理が止まるため、望んだ構成変更により副次的に対象リソースが再作成されるようなケースも、 terraform destroy
で全リソースを削除するようなケースも、どちらもapplyできなくなります。
無闇に使うと満足な運用ができなくなってしまう恐れがあるため、使い所はしっかりと考えたほうが良いでしょう。
なお、このフラグが効くのはコード上リソースに対して設定されている場合のみであり、コード内の resource
ブロックが丸々消えていたりすると普通に削除されます。
オペミス対策というよりは絶対に消したくないリソースを守る手段の1つと捉えたほうが正しいと思います。
CloudFormation
AWS CloudFormation スタックのリソースが削除または更新されないようにするには、どうすれば良いですか? にあるような方法でスタックやリソースに対する保護をかけることができます。
方法 | 効果 |
---|---|
削除保護フラグ | スタックの削除操作をブロックできる |
スタックポリシー | スタック内のリソースレベルで更新/削除が走るようなケースをブロックできる。スタックに対して行う設定でありCloudFormationテンプレートとは別のソースになる。ポリシーには、「このリソースは更新して良い/ダメ」みたいな規則を書いていく感じ |
DeletionPolicy | スタック削除時にもリソースを保持する。CloudFormationテンプレート内、各リソースのアトリビュートとして設定する |
複数のレイヤで対策が行えるため安心感があって良いですね。
適用失敗時のロールバック
リソースの構成変更に失敗した場合は、自動的に元の状態に戻しておいてくれると嬉しい!
Terraform | CloudFormation |
---|---|
× | ◎ |
Terraform
ロールバック機能はありません。
発生した問題を解消するか過去のバージョンのコードに戻し、再度 apply
を実行する必要があります。
CloudFormation
変更失敗時は自動的に以前の状態にロールバックされます。
手動作成リソースの取り込み
管理外のリソースを生きた状態で管理下におきたいような場合に対応できるか?という話。
Terraform | CloudFormation |
---|---|
◎ | ◎ |
両ツールともに、同じようなやり方で既存リソースをインポートすることができます。
Terraform
terraform import
により実現できます。対応していないリソースも存在しますが、最悪tfstateをガチャガチャすればなんとかできるようです。
参考 → Terraform職人入門: 日々の運用で学んだ知見を淡々とまとめる - Terraform管理外の既存のリソースをTerraform管理下に入れる
CloudFormation
CloudFormationスタックに既存リソースをインポートする機能があります(2019/11 GA)。
参考 → Amazon Web Services ブログ - 新機能 – CloudFormation スタックへの既存リソースのインポート
環境の分離性
本番環境/ステージング環境みたいな感じの環境分離について。
Terraform | CloudFormation |
---|---|
◎ | ◎ |
ちなみに当たり前の話ですが、どちらのツールでも環境間で差分となるような項目(例:VPC CIDRなど)はパラメタライズしてコードを作成し、環境ごとに別の値を使用しながらリソースを生成する必要があります。
Terraform
状態管理ファイルであるtfstateを分離することで環境分離を実現できます。
通常はディレクトリ(コード群)に対して1つのtfstateしか生成できませんが、 workspace という機能を使用することで、複数のtfstateを管理することができます。
CloudFormation
CloudFormationスタックを分離することで環境分離を実現できます。
依存関係の実現
リソース間の依存関係や初期処理の完了待ちが実装できるか。
Terraform | CloudFormation |
---|---|
△ | ◎ |
リソース間の依存関係のみ | リソース間の依存関係も初期処理の完了を待つ実装もOK |
Terraform
depends_on によりリソースの依存関係を表現できます。
しかし、リソース作成の依存しか表現できないので、例えばインスタンス生成後にUserDataなどで実行する初期処理の完了を待つことはできません。
CloudFormation
Terraform同様、リソース間の依存関係を実装する方法として DependsOn属性 が存在します。
これに加え、 WaitCondition や CreationPolicy属性 を使うと、例えばUserDataの処理完了待ちを実現可能です。
認証、権限
リソースの作成/削除のためには非常に強い権限が必要です。基本的にはあらゆるリソースに対するフルアクセスレベルの権限が必要となるでしょう。
この動作を実現できるアカウントやアクセスキーなどの情報は流出させるわけにはいきません。その辺の話です。
Terraform | CloudFormation |
---|---|
△ | ◎ |
Terraformの世界だけでは無理!別途考慮が必要 | スタックにRoleを付与する機能を使っていい感じに権限分離できる |
Terraform
AWS Providerの認証 は複数の方法が用意されていますが、どれを使おうと、結局強い権限がついた認証情報をterraformコマンド実行環境に持たせる必要があることは変わりません。
よって、アクセスキーの流出対策については別途考慮する必要があります(例えば以下のような感じでしょうか)。
- 権限をなるべく絞り、アクセスキーの管理方式を考える
- そもそも開発者に強い権限を持たせない仕組みを作る(CI/CDパイプラインを作ってローカルではterraformコマンドを実行しない、とか)
CloudFormation
スタック作成時のユーザにCloudFormationのスタック作成権限+作成したいリソースに関する更新系の権限を持たせる以外に、スタック作成時のパラメータとしてIAM Roleを指定する方法があります。
この方法の場合ユーザに必要な権限はCloudFormation周りのみで良くなるため、かなり権限の範囲を狭く絞ることができ、Terraformと比較してセキュアになります。
参考 → いつの間にかCloudFormationがIAM Roleに対応していました!
「CloudFormationは他のAWSサービスといい感じに連携する」とか「セキュリティ周りで考えることが少ない」 というのは、この辺の話が起因しているのかもしれません。
機能追随の早さ
Terraform | CloudFormation |
---|---|
○ | ◎ |
開発が活発なOSSなので素早く実装される | AWSのサービスなので素早く実装される |
Terraform
現状、AWSに新機能が追加されたときの機能追随は十分素早く行われていますが、OSS故に状況は変わる可能性があります。もちろん機能追随の早さが期待するレベルにならない可能性もあるでしょう。OSSであるTerraformを扱う上で重要なことは、そのような変化を許容しなければならない認識を持つことです。
ちなみにあきらめる以外の方法として、自身やチームのコントリビュートによって期待するレベルを担保するという選択肢もあります。
CloudFormation
CloudFormationはAWSのサービス故、素早い機能追随と安定した開発が見込めます。
新機能実装時、たまにCloudFormationがサポートされていないことがある点には注意が必要です。
マルチクラウド性
Terraform | CloudFormation |
---|---|
◎ | × |
Terraform
同じファイルやフォルダ内で複数のリソースを管理できます。
例えばAWSとAzureを併用するマルチクラウドなシステムに対して、以下のようにまとめてコードを記述でき、Terraformという一元的な手法でリソースの管理を行うことができます。
# AWS
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.2.0.0/24"
enable_dns_hostnames = true
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.2.0.0/25"
map_public_ip_on_launch = false
}
resource "aws_instance" "sample" {
ami = "ami-0a1c2ec61571737db"
subnet_id = aws_subnet.private.id
key_name = "test"
instance_type = "t2.micro"
lifecycle {
ignore_changes = [
tags["Name"],
]
}
}
# Azure
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "test" {
name = "test-resources"
location = "japaneast"
}
resource "azurerm_virtual_network" "private" {
name = "test-network-private"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
address_space = ["10.2.0.0/25"]
}
ただし、もちろんAWSにリソースを作成するコードとAzureにリソースを作成するコードは別物になります。「Terraformを使えば(何もしなくても)マルチクラウドに対応できる」とかいううまい話はありません。AWSに対してはAWSのリソースを作成するためのコードを書く必要があるし、Azureに対してはAzureのリソースを作成するためのコードを書く必要があります。
なおTerraformはパブリッククラウド以外にVMwareやk8s、Openstackなどの環境も扱うことができますので、ハイブリッドクラウドなシステムに対しても有効性を発揮します。
CloudFormation
AWSにベンダロックインします。AWS以外のリソースの作成を行うことはできません。
※ 一応カスタムリソースなるものもあるようですが、そんな実装をするくらいだったら別のツールを使ったほうが良いでしょう
ドキュメントや情報の充実度
Terraform | CloudFormation |
---|---|
○ | ◎ |
公式な情報は英語 | 日本語情報でも充実っぷりがすごい |
基本的にどちらも有名なツールなので、非公式な情報(ブログなど)は非常に多いです。大体ググればなんとかなります。ただしAWSサービスはそもそもアップデートが早いので、そのままだと動かないコードも結構遭遇したりします。
よって安定した情報である公式ドキュメントと併せて情報収集することが多くなるでしょう。ここには差分があります。
Terraform
公式ドキュメントがありますが英語のみです。
CloudFormation
公式ドキュメントが日本語にローカライズされています。
サービス名 Cloudformation
でググればすぐ公式ドキュメントが出てきます。
ために日本語ローカライズ版は情報が古かったりするので、英語版を読まないといけない時もある点は注意です。
文法の理解のしやすさ
Terraform | CloudFormation |
---|---|
◎ | ◎ |
HCL | yaml or json |
基本的な文法については細かい書き方は違えど学習コストに差異があるようなものではありません。
リソース作成時に設定するパラメータ(EC2インスタンスなら例えばSubnet IDやAMI IDなどの指定が必要、とか)を調べて記述するのがコーディングのメイン作業となりますが、それはどちらにせよやらなければならないことです。
なので、文法の学習コストをキーに「こっちの方が良い!」を決めるのは正直おすすめしません。
Terraform
HCL(Hashicorp Configuration Language)というDSLを用いてコードを記述します。
Hashicorp社の独自文法で、Terraformだけでなく同社の他SWでも使用します。
# comment
<type> <labels>... {
<key> = <value>
# nested block
<type> <labels>... {
# ...
}
}
基本的に↑のようなブロックを列挙していく文法です。
また、3項演算子やループなどの、少々高度な文法もあったりします。
CloudFormation
jsonまたはyamlでコードを記述します(yamlの方が書きやすいと思うので本エントリではyamlを使用しています)。
# comment
- <key>:
<key>: <value>
<key>:
- <value1>
- <value2>
- <value3>
基本的に↑のようなブロックを列挙していく文法です。
参考 → Ansible Documentation - YAML Syntax
文法レベルで繰り返しなどの制御構文は無いはずです。
マクロを使うと色々とできるみたいです(使ったことはない)。
参考 → AWS CloudFormation のマクロ機能を使ってみた
コードの記述量
Terraform | CloudFormation |
---|---|
◎ | ○ |
若干CloudFormationの方が長くなると思います。
例として、VPCとEC2インスタンスを作成するコードを並べてみます。
Terraform
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.2.0.0/24"
enable_dns_hostnames = true
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.2.0.0/25"
map_public_ip_on_launch = false
}
resource "aws_instance" "sample" {
ami = "ami-0a1c2ec61571737db"
subnet_id = aws_subnet.private.id
key_name = "test"
instance_type = "t2.micro"
}
22行、419文字
- AWS Providerの利用宣言を行う (1〜3行目)
-
resource "aws_vpc" "main"
のように、リソース定義(種別を含む)と論理名を同じ行に書ける - リソースに紐づくパラメータは
{}
内に表現される - リソースの参照は
<モジュール名>.<論理名>.<属性>
で行うため、論理名にはリソース種別を識別できる情報を盛り込まなくて良い (main_vpc
のようにしなくて良い) - スネークケースな感じ
CloudFormation
---
AWSTemplateFormatVersion: 2010-09-09
Resources:
mainVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.2.1.0/24
EnableDnsHostnames: true
privateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref mainVpc
CidrBlock: 10.2.1.0/25
MapPublicIpOnLaunch: false
sampleInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0a1c2ec61571737db
SubnetId: !Ref privateSubnet
KeyName: test
InstanceType: t2.micro
25行、498文字
- キー
Type:
に記載する値は、AWSリソースの場合AWS::
が付くことになっていて若干冗長 - リソースに紐づくパラメータは
Properties:
の要素として記述される - リソースの参照は
!Ref <論理名>
で行うため、論理名にはリソース種別を識別できる情報を盛り込まないとメンテナンス性が下がる - キャメルケースな感じ
ファイル分割の柔軟性
見通しが良いコードを書くには、リソースの分割(どのファイルにどのリソースを書くか)を柔軟に決定できる必要があります。
Terraform | CloudFormation |
---|---|
◎ | △ |
そもそも分割が許容されているSW設計 | やれないことはないが制約が多い |
Terraform
Terraformはカレントディレクトリをスキャンし、 .tf
という拡張子が付いているファイルを全て実行対象とします。
よって、リソースをまとめて書こうが、ファイルを分割して書こうが、実行内容は同じになります。ファイル間の紐付きをコード上表現する必要はありません。実行時に一緒にロードされれば(つまり同じディレクトリに格納されていれば)、どのファイルに書いてあっても良いのです。
これはTerraformの素晴らしい点であり、例えば後からリソースの記述場所を変更したい場合でも、対象リソースに関する記述を目的のファイルに移動するだけで達成することができます。
上述の、VPCとEC2インスタンスを作成するコードは、以下のように分割できます。
※ 単にファイルが分かれただけで、コードの総量は変化していないことに着目して下さい
-
provider.tf
provider "aws" { region = "ap-northeast-1" }
-
network.tf
resource "aws_vpc" "main" { cidr_block = "10.2.0.0/24" enable_dns_hostnames = true } resource "aws_subnet" "private" { vpc_id = aws_vpc.main.id cidr_block = "10.2.0.0/25" map_public_ip_on_launch = false } resource "aws_instance" "sample" { ami = "ami-0a1c2ec61571737db" subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t2.micro" }
-
compute.tf
resource "aws_instance" "sample" { ami = "ami-0a1c2ec61571737db" subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t2.micro" }
CloudFormation
CloudFormationがスタックを作成するときに受け付けるテンプレートは1つです。Terraformの様に直接複数ファイルを扱うことができません。
ではどのようにテンプレートを分割するの?と思いますが、あるスタックのリソースを他のスタックから参照させる仕組みを使うことで実現できます。
実装パターンとしては以下の2つの方法があります。
クロススタック参照
クロススタック参照とは、別のCloudFormationスタックのリソースにおける「出力」を参照するCloudFormationの機能です。この機能により、1つのスタックに全てのリソースを入れることなくシステムを構成できます。
以下のように Export
フィールドを使って出力の値を設定し、CloudFormationの組み込み関数である ImportValue
を使ってその値をインポートします。
-
コード
-
network.cfn.yml
--- AWSTemplateFormatVersion: 2010-09-09 Resources: mainVpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.2.1.0/24 EnableDnsHostnames: true privateSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref mainVpc CidrBlock: 10.2.1.0/25 MapPublicIpOnLaunch: false ### ここでExport ### Outputs: privateSubnet: Value: !Ref privateSubnet Export: Name: test-PrivateSubnet
-
compute.cfn.yml
--- AWSTemplateFormatVersion: 2010-09-09 Resources: sampleInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0a1c2ec61571737db ### ここでImport ### SubnetId: !ImportValue test-PrivateSubnet KeyName: test InstanceType: t2.micro
-
-
デプロイ
以下のように、テンプレートごとにデプロイを実施します。$ aws cloudformation deploy --template-file network.cfn.yml --stack-name test-stack-network $ aws cloudformation deploy --template-file compute.cfn.yml --stack-name test-stack-compute
-
注意点
- テンプレートごとにスタックのデプロイを行う必要があります
- スタックの作成/削除の順序に制約が発生します
- 参照したいExportが存在しないとスタック作成に失敗するし、参照されているExportがあるスタックは削除に失敗します
- 機能追加等の際はこのあたりも気にして実装、反映させる必要があります
- そのため、単純に「このスタックだけ再作成すりゃええやん」とならず、思ったより色々なスタックを再作成しないといけなくなったりします
- この制約はクロススタック参照を使う限り一生発生し、参照の数が増えるにつれて管理がどんどん難しくなります
-
Export.Name
は重複不可です、重複してるとデプロイ時にこけます
スタックのネスト
Type: AWS::CloudFormation::Stack
を用いることで、あるスタックから別のスタックを生成し、スタックをネストさせることができます。
参考 → CloudFormationでスタックをネストする
スタックをネストさせる場合、リソース参照にはクロススタック参照(Export)を使わなくても良く、以下のコードのようにスタックのOutputを親テンプレートで参照することで実現します。
-
コード
-
main.cfn.yml
(親テンプレート)--- AWSTemplateFormatVersion: 2010-09-09 Resources: networkStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: ./network.cfn.yml computeStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: ./compute.cfn.yml Parameters: ### GetAttを使って、network.cfn.ymlで定義したOutputの情報を引っ張ってパラメータ渡し ### SubnetId: !GetAtt networkStack.Outputs.privateSubnet
ここに記載するスタックの順序は重要ではありません。
→computeStack
はnetworkStack
に依存しますが、逆順に書いても問題なく動作します。 -
network.cfn.yml
--- AWSTemplateFormatVersion: 2010-09-09 Resources: mainVpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.2.1.0/24 EnableDnsHostnames: true privateSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref mainVpc CidrBlock: 10.2.1.0/25 MapPublicIpOnLaunch: false Outputs: privateSubnet: Value: !Ref privateSubnet
-
compute.cfn.yml
--- AWSTemplateFormatVersion: 2010-09-09 ### 親テンプレートから渡されるパラメータを定義しておく Parameters: SubnetId: Type: String Resources: sampleInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0a1c2ec61571737db SubnetId: !Ref SubnetId KeyName: test InstanceType: t2.micro
-
-
デプロイ
aws cloudformation package
コマンドにより、ネストされたスタックを用いるCloudFormationテンプレートのパッケージングを行うことができます。コマンドを実行すると、以下の処理が自動的に実行されます。
- ネストされるスタックのテンプレートをS3にアップロードする
- 親テンプレートでローカル参照していた部分がS3のURLに置き換わったファイルを標準出力する
$ aws cloudformation package --template ./main.cfn.yml --s3-bucket test-cloudformation-package --output json > packaged-template.yml
-
packaged-template.yml (上記コマンドの標準出力)
AWSTemplateFormatVersion: 2010-09-09 Resources: networkStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://s3.ap-northeast-1.amazonaws.com/test-cloudformation-package/c8c81c7010587919f6d3a354a7f3f8c2.template computeStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://s3.ap-northeast-1.amazonaws.com/test-cloudformation-package/c8c81c7010587919f6d3a354a7f3f8c2.template Parameters: SubnetId: Fn::GetAtt: - networkStack - Outputs.privateSubnet
このテンプレートからスタックの生成を行います。
クロススタック参照の時とは異なり、デプロイ対象のテンプレートは1つとなります。$ aws cloudformation deploy --template-file packaged-template.yml --stack-name test-stack-all
-
スタックがネストされていることがコンソール上で分かります。
親スタックの削除を行うと、子スタックも一緒に削除されます。 -
注意点
- デプロイのために追加のS3が必要になります
- ある値(VPC CIDRなど)をパラメータ化したいとき、以下の箇所にパラメータに関する記述を行う必要があり、実装時に結構面倒な思いをします。
- 上位テンプレートの
Parameters
- 下位テンプレートの
Parameters
-
AWS::CloudFormation::Stack
リソースのProperties.Paramters
- 上位テンプレートの
- ChangeSetは作成したところで子スタックの変更内容が全く分からず使い物になりません
- デプロイエラー発生時、どこで何が起きているのか分かりにくいためトラブルシュートしづらいです
...
といった感じで、ファイル分割に対してCloudFormationは意外と弱いです。
「CloudFormationは最初に設計が必要」とよく言われるのは、この辺が主な理由かと思います。
モジュール化
実装した一部のコード(あるいは誰かが作って公開したコード)の再利用ができるか。
Terraform | CloudFormation |
---|---|
◎ | ○ |
ネイティブサポート、公式モジュールも存在 | スタックのネストで実現できる |
Terraform
複数リソースをひとまとめにする機能として、 module があります。
再利用性の高いリソースのセットをまとめておき、複数の箇所から呼び出すことができます。
Terraform Providerが公開しているモジュールのレジストリ も存在します。
CloudFormation
上述した、スタックのネスト機能を使えば実現できますが、発生する課題も同様です。
むしろ再利用したいリソースのまとまりを1つのテンプレートに切り出すことになるため細分化の単位が小さくなり、スタック間の依存関係の複雑化やパラメータ伝搬数の増加が発生し、デメリットが色濃く現れることになります。
コードバリデーション
syntax checkやlintingなど。
Terraform | CloudFormation |
---|---|
◎ | ○ |
標準のvalidateとOSSのlinterを併用するといい感じ | 標準機能はゴミ。cfn-lintを使おう |
Terraform
標準機能として validate が用意されていたり、 tflint というツールがあったりします。
これらのツールを使って、実際にいくつかのパターンでNG検出を試してみると以下のような結果になります。
-
リソースの重複
resource "aws_instance" "sample" { ami = "ami-0a1c2ec61571737db" subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t3.micro" } # sample という名前のリソースが重複している resource "aws_instance" "sample" { ami = "ami-0a1c2ec61571737db" subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t3.micro" }
-
○(検出可)
terraform validate
$ terraform validate Error: Duplicate resource "aws_instance" configuration on compute.tf line 8: 8: resource "aws_instance" "sample" { A aws_instance resource named "sample" was already declared at compute.tf:1,1-33. Resource names must be unique per type in each module.
-
○
tflint
$ docker run --rm -v (pwd):/data -t wata727/tflint Failed to load configurations. 1 error(s) occurred: Error: Duplicate resource "aws_instance" configuration on compute.tf line 9, in resource "aws_instance" "sample": 9: resource "aws_instance" "sample" { A aws_instance resource named "sample" was already declared at compute.tf:1,1-33. Resource names must be unique per type in each module.
-
-
必須パラメータの指定なし
resource "aws_instance" "sample" { # ami = "ami-0a1c2ec61571737db" # 必須パラメータであるAMI IDを与えない subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t3.micro" }
-
○
terraform validate
$ terraform validate Error: Missing required argument on compute.tf line 1, in resource "aws_instance" "sample": 1: resource "aws_instance" "sample" { The argument "ami" is required, but no definition was found.
-
×
tflint
$ docker run --rm -v (pwd):/data -t wata727/tflint #=> 出力無し(返り値0)
-
-
存在しないパラメータを与える
resource "aws_instance" "sample" { ami = "ami-0a1c2ec61571737db" subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t3.micro" hogehoge = "fugafuga" # 存在しないパラメータを指定している }
-
○
terraform validate
$ terraform validate Error: Unsupported argument on compute.tf line 7, in resource "aws_instance" "sample": 7: hogehoge = "fugafuga" # 存在しないパラメータ An argument named "hogehoge" is not expected here.
-
×
tflint
$ docker run --rm -v (pwd):/data -t wata727/tflint #=> 出力無し(返り値0)
-
-
パラメータの内容誤り
resource "aws_instance" "sample" { ami = "ami-0a1c2ec61571737db" subnet_id = aws_subnet.private.id key_name = "test" instance_type = "t3x.micro" # t3x.micro というインスタンスタイプは存在しない }
-
×
terraform validate
$ terraform validate Success! The configuration is valid.
-
○
tflint
$ docker run --rm -v (pwd):/data -t wata727/tflint 1 issue(s) found: Error: "t3x.micro" is an invalid value as instance_type (aws_instance_invalid_type) on compute.tf line 5: 5: instance_type = "t3x.micro" # t3x.micro というインスタンスタイプは存在しない
-
お互いに検出領域に違いがあることが分かるので、これらは併用するのが良さそうです。
CloudFormation
CloudFormationにも標準機能として、 validate があります。
また、 cfn-python-lint というlinterもあります。
※ js製の cfn-lint もあるようですが、全く別物らしいです
ではこちらも上述のパターンで実際にNG検出を試してみます。cfn-lintには、開発がより活発であるcfn-python-lintの方を使うことにします。
-
リソースの重複
--- AWSTemplateFormatVersion: 2010-09-09 Resources: sampleInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0a1c2ec61571737db SubnetId: !ImportValue test-PrivateSubnet KeyName: test InstanceType: t2.micro # sampleInstance という名前のリソースが重複している sampleInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0a1c2ec61571737db SubnetId: !ImportValue test-PrivateSubnet KeyName: test InstanceType: t2.micro
-
×
cloudformation validate
$ aws cloudformation validate-template --template-body file://./compute.cfn.yml { "Parameters": [] }
-
○
cfn-lint
$ cfn-lint compute.cfn.yml E0000 Duplicate resource found "sampleInstance" (line 12) compute.cfn.yml:12:3
-
-
必須パラメータの指定なし
--- AWSTemplateFormatVersion: 2010-09-09 Resources: sampleInstance: Type: AWS::EC2::Instance Properties: # ImageId: ami-0a1c2ec61571737db # 必須パラメータであるAMI IDを与えない SubnetId: !ImportValue test-PrivateSubnet KeyName: test InstanceType: t2.micro
-
×
cloudformation validate
$ aws cloudformation validate-template --template-body file://./compute.cfn.yml { "Parameters": [] }
-
○
cfn-lint
$ cfn-lint compute.cfn.yml E2522 At least one of [ImageId, LaunchTemplate] should be specified for Resources/sampleInstance/Properties compute.cfn.yml:7:5
-
-
存在しないパラメータを与える
--- AWSTemplateFormatVersion: 2010-09-09 Resources: sampleInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0a1c2ec61571737db SubnetId: !ImportValue test-PrivateSubnet KeyName: test InstanceType: t2.micro HogeHoge: fugafuga # 存在しないパラメータを指定している
-
×
cloudformation validate
$ aws cloudformation validate-template --template-body file://./compute.cfn.yml { "Parameters": [] }
-
○
cfn-lint
$ cfn-lint compute.cfn.yml E3002 Invalid Property Resources/sampleInstance/Properties/HogeHoge compute.cfn.yml:13:7
-
-
パラメータの内容誤り
--- AWSTemplateFormatVersion: 2010-09-09 Resources: sampleInstance: Type: AWS::EC2::Instance Properties: ImageId: ami-0a1c2ec61571737db SubnetId: !ImportValue test-PrivateSubnet KeyName: test InstanceType: t3x.micro # t3x.micro というインスタンスタイプは存在しない
-
×
cloudformation validate
$ aws cloudformation validate-template --template-body file://./compute.cfn.yml { "Parameters": [] }
-
○
cfn-lint
$ cfn-lint compute.cfn.yml E3030 You must specify a valid value for InstanceType (t3x.micro). Valid values are ["a1.2xlarge", "a1.4xlarge", "a1.large", "a1.medium", "a1.metal", "a1.xlarge", "c1.medium", "c1.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", "c3.large", "c3.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "c4.large", "c4.xlarge", "c5.12xlarge", "c5.18xlarge", "c5.24xlarge", "c5.2xlarge", "c5.4xlarge", "c5.9xlarge", "c5.large", "c5.metal", "c5.xlarge", "c5a.12xlarge", "c5a.16xlarge", "c5a.24xlarge", "c5a.2xlarge", "c5a.4xlarge", "c5a.8xlarge", "c5a.large", "c5a.xlarge", "c5d.12xlarge", "c5d.18xlarge", "c5d.24xlarge", "c5d.2xlarge", "c5d.4xlarge", "c5d.9xlarge", "c5d.large", "c5d.metal", "c5d.xlarge", "c5n.18xlarge", "c5n.2xlarge", "c5n.4xlarge", "c5n.9xlarge", "c5n.large", "c5n.metal", "c5n.xlarge", "c6g.12xlarge", "c6g.16xlarge", "c6g.2xlarge", "c6g.4xlarge", "c6g.8xlarge", "c6g.large", "c6g.medium", "c6g.metal", "c6g.xlarge", "cc2.8xlarge", "cr1.8xlarge", "d2.2xlarge", "d2.4xlarge", "d2.8xlarge", "d2.xlarge", "f1.16xlarge", "f1.2xlarge", "f1.4xlarge", "g2.2xlarge", "g2.8xlarge", "g3.16xlarge", "g3.4xlarge", "g3.8xlarge", "g3s.xlarge", "g4dn.12xlarge", "g4dn.16xlarge", "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.8xlarge", "g4dn.metal", "g4dn.xlarge", "h1.16xlarge", "h1.2xlarge", "h1.4xlarge", "h1.8xlarge", "hs1.8xlarge", "i2.2xlarge", "i2.4xlarge", "i2.8xlarge", "i2.xlarge", "i3.16xlarge", "i3.2xlarge", "i3.4xlarge", "i3.8xlarge", "i3.large", "i3.metal", "i3.xlarge", "i3en.12xlarge", "i3en.24xlarge", "i3en.2xlarge", "i3en.3xlarge", "i3en.6xlarge", "i3en.large", "i3en.metal", "i3en.xlarge", "inf1.24xlarge", "inf1.2xlarge", "inf1.6xlarge", "inf1.xlarge", "m1.large", "m1.medium", "m1.small", "m1.xlarge", "m2.2xlarge", "m2.4xlarge", "m2.xlarge", "m3.2xlarge", "m3.large", "m3.medium", "m3.xlarge", "m4.10xlarge", "m4.16xlarge", "m4.2xlarge", "m4.4xlarge", "m4.large", "m4.xlarge", "m5.12xlarge", "m5.16xlarge", "m5.24xlarge", "m5.2xlarge", "m5.4xlarge", "m5.8xlarge", "m5.large", "m5.metal", "m5.xlarge", "m5a.12xlarge", "m5a.16xlarge", "m5a.24xlarge", "m5a.2xlarge", "m5a.4xlarge", "m5a.8xlarge", "m5a.large", "m5a.xlarge", "m5ad.12xlarge", "m5ad.24xlarge", "m5ad.2xlarge", "m5ad.4xlarge", "m5ad.large", "m5ad.xlarge", "m5d.12xlarge", "m5d.16xlarge", "m5d.24xlarge", "m5d.2xlarge", "m5d.4xlarge", "m5d.8xlarge", "m5d.large", "m5d.metal", "m5d.xlarge", "m5dn.12xlarge", "m5dn.16xlarge", "m5dn.24xlarge", "m5dn.2xlarge", "m5dn.4xlarge", "m5dn.8xlarge", "m5dn.large", "m5dn.metal", "m5dn.xlarge", "m5n.12xlarge", "m5n.16xlarge", "m5n.24xlarge", "m5n.2xlarge", "m5n.4xlarge", "m5n.8xlarge", "m5n.large", "m5n.metal", "m5n.xlarge", "m6g.12xlarge", "m6g.16xlarge", "m6g.2xlarge", "m6g.4xlarge", "m6g.8xlarge", "m6g.large", "m6g.medium", "m6g.metal", "m6g.xlarge", "p2.16xlarge", "p2.8xlarge", "p2.xlarge", "p3.16xlarge", "p3.2xlarge", "p3.8xlarge", "p3dn.24xlarge", "r3.2xlarge", "r3.4xlarge", "r3.8xlarge", "r3.large", "r3.xlarge", "r4.16xlarge", "r4.2xlarge", "r4.4xlarge", "r4.8xlarge", "r4.large", "r4.xlarge", "r5.12xlarge", "r5.16xlarge", "r5.24xlarge", "r5.2xlarge", "r5.4xlarge", "r5.8xlarge", "r5.large", "r5.metal", "r5.xlarge", "r5a.12xlarge", "r5a.16xlarge", "r5a.24xlarge", "r5a.2xlarge", "r5a.4xlarge", "r5a.8xlarge", "r5a.large", "r5a.xlarge", "r5ad.12xlarge", "r5ad.24xlarge", "r5ad.2xlarge", "r5ad.4xlarge", "r5ad.large", "r5ad.xlarge", "r5d.12xlarge", "r5d.16xlarge", "r5d.24xlarge", "r5d.2xlarge", "r5d.4xlarge", "r5d.8xlarge", "r5d.large", "r5d.metal", "r5d.xlarge", "r5dn.12xlarge", "r5dn.16xlarge", "r5dn.24xlarge", "r5dn.2xlarge", "r5dn.4xlarge", "r5dn.8xlarge", "r5dn.large", "r5dn.metal", "r5dn.xlarge", "r5n.12xlarge", "r5n.16xlarge", "r5n.24xlarge", "r5n.2xlarge", "r5n.4xlarge", "r5n.8xlarge", "r5n.large", "r5n.metal", "r5n.xlarge", "r6g.12xlarge", "r6g.16xlarge", "r6g.2xlarge", "r6g.4xlarge", "r6g.8xlarge", "r6g.large", "r6g.medium", "r6g.metal", "r6g.xlarge", "t1.micro", "t2.2xlarge", "t2.large", "t2.medium", "t2.micro", "t2.nano", "t2.small", "t2.xlarge", "t3.2xlarge", "t3.large", "t3.medium", "t3.micro", "t3.nano", "t3.small", "t3.xlarge", "t3a.2xlarge", "t3a.large", "t3a.medium", "t3a.micro", "t3a.nano", "t3a.small", "t3a.xlarge", "u-12tb1.metal", "u-18tb1.metal", "u-24tb1.metal", "u-6tb1.metal", "u-9tb1.metal", "x1.16xlarge", "x1.32xlarge", "x1e.16xlarge", "x1e.2xlarge", "x1e.32xlarge", "x1e.4xlarge", "x1e.8xlarge", "x1e.xlarge", "z1d.12xlarge", "z1d.2xlarge", "z1d.3xlarge", "z1d.6xlarge", "z1d.large", "z1d.metal", "z1d.xlarge"] compute.cfn.yml:11:7
-
…と、正直標準機能のvalidateはsyntaxレベルまでしか見ていないようなので使い物にならない気がします。
その代わり(?)、cfn-lintは優秀っぽいのでそちらを使いましょう。
参考
本エントリを記載するにあたってとんでもなく参考にさせていただいた情報です。先人たちのアウトプットに感謝します。