「コンソールをクリックするだけ」では限界が来たとき
去年の初め、チームでかなり痛い障害が発生した。staging環境が誤って削除されてしまい、ゼロから再構築するのに約3日かかった。infrastructureが複雑だったわけではなく、誰も正確に何を設定したか把握していなかったのが原因だ——各自がバラバラにクリックしていて、明確なドキュメントもなかった。そのときから、Infrastructure as Codeを真剣に調べ始めた。
6ヶ月後、本番環境は安定して稼働している——そして語るべきことがかなりたまった。
考察:なぜ「ClickOps」は危険な落とし穴なのか
AWS Consoleは直感的で、結果もすぐ出る——始めたばかりの人には便利に感じる。しかし代償は後になってからやってきて、たいていは高くつく:
- Configuration drift:dev、staging、productionが誰も知らぬうちにじわじわと異なる状態になっていく。本番のsecurity groupにport 22を
0.0.0.0/0で開けるルールが残っているのを発見したことがある——誰かが「ちょっとテスト」して閉め忘れたまま、約2ヶ月間放置されていた。 - 再現不可能:productionと全く同じ環境を新たに作りたい?答えは最初にセットアップした人の記憶次第だ。
- ディザスタリカバリが遅い:regionで障害が発生してmigrateが必要になったら、どのくらいかかる?ClickOpsだと、答えはたいてい「わからない」だ。
- 意味のあるaudit trailがない:CloudTrailは誰が何を変更したかは記録できるが、なぜ変更したかはわからない——そしてもっと重要なのは、簡単にrollbackできないことだ。
根本的な問題:インフラがコードではなく、人の頭の中にある状態だ。
IaCの選択肢——そしてなぜTerraformが勝ったのか
Terraformにコミットする前に、他の選択肢も試してみた:
AWS CloudFormation
AWSネイティブで追加インストール不要。しかしYAML/JSONのsyntaxが非常に冗長で、シンプルなstackでも300行を超えることがある。さらに重要なのは、AWSに完全にlock-inされることだ。
Ansible
configuration management(パッケージのインストール、既存サーバーのconfig file編集)には優れているが、infrastructureをprovisionするためのツールではない。Ansibleには「state」の概念がなく、現在どのリソースが存在するかをtrackできない。
Pulumi
infrastructureをPython/TypeScriptで書けるのは魅力的に聞こえる。しかしlearning curveが高く、ecosystemもまだ小さく、Terraformに比べて具体的なサンプルを探すのにかなり時間がかかった。
Terraform
Multi-cloud(AWS、GCP、Azure…)対応で、CloudFormationよりHCL syntaxが読みやすく、state managementも優れている。コミュニティが非常に大きく、ほぼどのAWSリソースでもTerraform Registryにサンプルが揃っている。
Terraformのインストールと初期設定
Ubuntu/Debianへのインストール——後のアップデートを楽にするため、HashiCorpの公式apt repoを使う:
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform --version
次はAWS credentialsの設定だ。IAM userをまだ作成していなければ、今がやるべきタイミング——least-privilege原則で、必要な権限のみを付与する:
aws configure
# AWS Access Key ID: AKIA...
# AWS Secret Access Key: ...
# Default region name: ap-northeast-1
# Default output format: json
最初のTerraform設定:EC2インスタンスを作成する
これが最もシンプルな出発点となる設定だ——東京リージョンのEC2インスタンス1台で、Terraformに何を求めているかを伝えるのに十分な宣言:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1" # 東京
}
resource "aws_instance" "web_server" {
ami = "ami-0d52744d6551d851e" # Ubuntu 22.04 LTS 東京
instance_type = "t3.micro"
tags = {
Name = "web-server-production"
Environment = "production"
ManagedBy = "terraform"
}
}
output "instance_ip" {
value = aws_instance.web_server.public_ip
}
基本的なworkflowはたった3つのコマンドだ。この3つのコマンドを6ヶ月間、毎日使い続けている:
# 初期化 — プロバイダーをローカルにダウンロード
terraform init
# 変更のプレビュー(実行しない)
terraform plan
# 変更を適用
terraform apply
terraform planの出力は明確だ:どのリソースが作成(+)、削除(-)、変更(~)されるかが一目でわかる。applyする前に必ずplanをしっかりレビューする——これが最も重要なsafety netだ。
State File——ほとんどのチュートリアルが見落とす最重要ポイント
Terraformはインフラの状態をterraform.tfstateファイルに保存する。このファイルはTerraformが管理しているリソースを把握するための「信頼できる情報源」だ。
最もよくある間違い:state fileをローカルマシンに保存すること。その結果:
- チームの他のメンバーがTerraformを実行できない
- マシンを失う = stateを失う = Terraformが何を管理しているかわからなくなる
- 2人が同時に
terraform applyを実行するとconflictが発生する——それを防ぐものが何もない
解決策:S3 + DynamoDBを使ったremote backendでstateをロックする:
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "production/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
まずS3 bucketとDynamoDB tableを作成する(最初の一回だけ手動で):
aws s3 mb s3://my-terraform-state-bucket --region ap-northeast-1
aws s3api put-bucket-versioning \
--bucket my-terraform-state-bucket \
--versioning-configuration Status=Enabled
aws dynamodb create-table \
--table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ap-northeast-1
実プロジェクトのためのTerraformコード構成
何度かrefactorした後、現在使っている構成がこれだ——シンプルだが、environmentを追加する際にもscaleできる:
infrastructure/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── modules/
├── vpc/
├── ec2/
└── rds/
Variablesを使うとhardcodeを避け、コードを複製せずにdevとproductionの違いを簡単に管理できる:
# variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Environment name"
type = string
}
# terraform.tfvars (production)
instance_type = "t3.medium"
environment = "production"
本番環境6ヶ月で学んだこと
これは自分が学んだこと——そのほとんどは少なくとも一度失敗してから得た教訓だ:
terraform fmtとterraform validateを毎commit前に実行する——syntax errorのあるコードをpushしないよう、pre-commit hookかCIに組み込む。- state fileを手動で編集しない——操作が必要な場合は
terraform state mvまたはterraform state rmを使う。 - 全リソースにtagを付ける:
ManagedBy = "terraform"、Environment、Project。3ヶ月後にAWS Consoleを見たとき、どのリソースが誰に管理されているかが一目でわかる。 - CI/CDでplanをレビューする:PRが来たときに
terraform planを自動実行するGitHub Actionsをセットアップする。結果が自動的にPRにコメントされ、レビュアーはmerge前にinfrastructureの変更をすぐ確認できる——お互い確認し合う手間が省ける。 - environmentごとにstateを分割する:
dev/terraform.tfstateとproduction/terraform.tfstateを別々に管理する——誤ったコマンドでproductionをdestroyするリスクを避けるため。
ちょっとした便利ツール:Terraformのoutputをデバッグしたりterraform output -jsonのJSONを確認したいとき、toolcraft.app/ja/tools/developer/json-formatterにペーストしてすぐフォーマットできる——JSONを見るためだけにIDEを開くより断然楽だ。
どこから始めるべきか?
cloudで何かを動かしていてIaCをまだ使っていないなら、毎日密かにtechnical debtが積み上がっている。Terraformは最初の日からセットアップすべきだ——手動でクリックした50のリソースができてからではなく、後から全部importするのは本当に大変だ。
始める最も簡単な方法:今のプロジェクトにあるシンプルなリソース——S3 bucketでも、security groupでも——を一つ選んで、それのTerraformを書いてみることだ。terraform destroyしてからterraform applyして、全てが全く同じように再作成されるのを目の当たりにする感覚は、どんな記事よりも説得力があるはずだ。
