シリーズ目次
| 回 | タイトル |
|---|---|
| 第1回(本記事) | IaC とは何か / Terraform の基本 / S3 ハンズオン |
| 第2回(近日公開) | AWS 構築編 — VPC から ECS Fargate まで |
| 第3回(近日公開) | Cloudflare + CI/CD 編 — エッジルーティングと GitHub Actions |
| 第4回(近日公開) | 運用・保守・スケール編 — 監視・コスト最適化・水平展開 |
はじめに
以前「Spring Boot + Nuxt 3 のモノリスを AWS + Cloudflare に載せるまでの全手順」という記事を書きました。あの記事は「何を直してどの順番でインフラを作ればいいか」の全体像を扱いましたが、実際に手を動かしてインフラを構築する手順は詰め込めませんでした。
本シリーズはその続編として、コード(HCL)でインフラを管理する「IaC」の入口から、AWS + Cloudflare の本番環境を自力で建てて運用できるようになるところまでを、4 回に分けて丁寧に書いていきます。
想定読者
- IaC が何の略かわからないレベルから始めたい方
- Spring Boot + Nuxt 3 などの Web アプリをローカル Docker Compose で動かしていて、そろそろ本番に載せたい方
- AWS のコンソールをポチポチして「なんとか動いた」状態から、再現可能な構成管理に移行したい方
シリーズ全体の目標アーキテクチャ
全4回を通じて、以下の構成を Terraform でコードとして管理できる状態を目指します。
ユーザー ──── HTTPS ────▶ Cloudflare(DNS + CDN/WAF)
│
┌───────────────┼──────────────────┐
│ パスで振り分け │ │
▼ ▼ ▼
/api/**, /ws それ以外 画像・添付
┌──────────────┐ ┌──────────────────┐ ┌────────────┐
│ AWS ALB │ │ Cloudflare Pages │ │ R2 ストレージ│
│ ↓ │ │ (Nuxt 3 SSR) │ └────────────┘
│ ECS Fargate │ └──────────────────┘
│ (Spring Boot)│
│ ↓ │
│ RDS MySQL │
│ ElastiCache │
│ (Valkey) │
└──────────────┘
第1回では AWS の S3 バケット 1 個を作って消すところから始め、Terraform の基本を体で覚えることを目的にします。
1. IaC(Infrastructure as Code)とは何か
IaC は「インフラをコードで管理する」という考え方の総称です。
「AWS を使うなら AWS のコンソールで設定すれば十分では?」と思うかもしれません。実際、最初の 1〜2 台のサーバーであれば、コンソールのポチポチ操作(手作業構築)で十分に動きます。ただし、本番運用が始まると、手作業構築の罠にハマります。
手作業構築の3つの罠
罠①: 再現できない
コンソールで設定した内容をどこかに記録していない限り、「あのとき何を設定したか」が誰にもわかりません。「ステージング環境と全く同じ構成を本番に作って」と言われても、記憶と画面のスクリーンショットが頼りになります。半年後の自分には別人も同然です。
罠②: 戻せない
「セキュリティグループのルールを変えたら本番がつながらなくなった」というとき、変更前の設定が記録されていなければ復元できません。コンソールの操作履歴(CloudTrail)を遡ることはできますが、「変更前の完全な状態」を一発で復元するのは難しいです。
罠③: レビューできない
チームで開発している場合、コンソール上の変更は「誰かがこっそりルールを変えた」状態になります。コードと違ってプルリクエストでレビューを受けることができません。
コード化で得られるもの
IaC では、インフラの構成をテキストファイル(コード)として書き、Git で管理します。
- 過去のある時点の構成を
git checkoutで再現できる - 変更は diff として見えるので、プルリクエストでレビューできる
-
applyという操作でコードと実際のインフラを一致させるため、「あれ、誰かが手で直した?」が起きない - 複数環境(ステージング・本番)を同じコードから作れる
PR レビュー = インフラの変更履歴という状態が、IaC を導入する最大の目的と言えます。
2. ツールの地図 — Terraform を選ぶ理由
IaC ツールはいくつかあります。主要な3つを簡単に比較します。
Terraform(HashiCorp)
HCL(HashiCorp Configuration Language)という専用言語でインフラを宣言します。AWS・GCP・Azure・Cloudflare など約 3,000 のプロバイダーに対応しており、AWS と Cloudflare を同じ言語・同じワークフローで管理できます。エコシステムが成熟しており、日本語の情報量も最多です。2023 年にライセンスが BSL に変わりましたが、個人・スタートアップ規模の使用では実質的に制限はありません。
AWS CDK(Cloud Development Kit)
Python・TypeScript などの汎用プログラミング言語でインフラを書きます。AWS 専用で、Cloudflare などの他社サービスは扱えません。プログラミング言語の if 文やループが使えるため、複雑なロジックを持つインフラには強いです。
AWS CloudFormation
AWS 公式のサービスで、YAML または JSON でインフラを定義します。AWS ネイティブで追加費用なしで使えますが、記述量が多く冗長になりがちです。CDK の内部でも CloudFormation が動いています。
本シリーズが Terraform を選ぶ理由
前出のアーキテクチャでは AWS(バックエンド)と Cloudflare(エッジ・フロントエンド)の両方を管理する必要があります。Terraform はどちらも同一の言語・同一のワークフローで扱えるため、最も素直な選択です。加えて、日本語の書籍・ブログ・Stack Overflow の情報が最も多く、詰まったときに調べやすいという実利上の理由もあります。
3. Terraform の3つの中核概念
Terraform を使い始める前に、3 つの概念だけ押さえておきます。この3つが腑に落ちると、あとは「コードを書いて試す」だけです。
①「あるべき状態」を宣言する(HCL)
Terraform のコードは「この状態にしてほしい」という宣言を書くものです。「S3 バケットを作る」のではなく「S3 バケット my-app-assets が存在する」と書きます。手順(操作手順)ではなく状態(あるべき姿)を書く、という発想の転換が Terraform の肝です。
# 「このバケットが存在する状態にしてほしい」という宣言
resource "aws_s3_bucket" "assets" {
bucket = "my-app-assets"
}
②state(台帳)
Terraform は terraform.tfstate というファイルに「今、実際に何が作られているか」を記録しています。このファイルを state(ステート、台帳) と呼びます。
plan や apply を実行するとき、Terraform は以下の3つを照合します。
- コードに書いた「あるべき状態」
- state に記録されている「前回 apply 時の状態」
- クラウド上の「実際の現在の状態」
この差分から「何を追加・変更・削除すればよいか」を計算します。state は Terraform の頭脳です。
③ plan → apply のライフサイクル
Terraform の操作は次の4ステップで回ります。
| コマンド | 何をするか |
|---|---|
terraform init |
必要なプロバイダーをダウンロード(初回・プロバイダー追加時に実行) |
terraform plan |
「今 apply するとどんな変更が起きるか」をドライランで表示 |
terraform apply |
plan の内容を実際に実行してクラウドに反映 |
terraform destroy |
このコードで管理しているリソースをすべて削除 |
plan は必ず読む習慣をつけてください。 apply は「plan で確認した内容を実行する」という操作です。plan を読まずに apply するのは、内容を確認せずにデプロイボタンを押すのと同じです。最初のうちは短い plan 出力でも必ず一行一行読む癖をつけることをお勧めします。
plan の出力には次の記号が使われます。
-
+ create— 新しく作成される -
~ update in-place— 既存のリソースを変更する(削除せずに変更できる) -
-/+ replace— 一度削除してから再作成する(ダウンタイムに注意) -
- destroy— 削除される
-/+(replace)が出たときは要注意です。本番の RDS インスタンスや ECS タスクに -/+ が出たら、変更内容を再確認してから apply してください。
4. 環境準備
Terraform のインストール
Windows(winget 使用)
winget install HashiCorp.Terraform
# インストール後、新しいターミナルを開いて確認
terraform -version
Mac(Homebrew 使用)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform -version
バージョンは 1.x 系であれば問題ありません。本シリーズのコード例は 1.5 以上を前提にしています。
AWS CLI のインストールと認証情報の設定
Terraform が AWS を操作するには、AWS CLI の認証情報が必要です。
AWS CLI のインストール
# Windows: 公式インストーラーをダウンロードするか winget で
winget install Amazon.AWSCLI
# Mac
brew install awscli
# インストール確認
aws --version
※インストール後、PowerShellを再起動すること。
IAM ユーザーとアクセスキーの作成
AWS コンソール → IAM → ユーザー → 「ユーザーを作成」で、ハンズオン用のユーザーを作成します。
具体的な手順(今の画面からの操作)
1. 「ユーザーをグループに追加」を選択
2. グループが1つもないはずなので、その場の 「グループを作成」 ボタンを押す
3. グループ名: admins(任意)、ポリシーで AdministratorAccess にチェック → グループ作成
紛らわしい同類が並ぶので2点だけ注意:
- AdministratorAccess-Amplify や AdministratorAccess-AWSElasticBeanstalk など名前が似た別物が検索に出ます。無印の AdministratorAccess を選ぶこと
- PowerUserAccess("...but does not allow management of Users and groups")では不足です。bootstrap は IAM ロール(CI 用 OIDC
ロール3種)を作るため、IAM 操作まで含む無印 AdministratorAccess が必要です
4. 4. 作ったグループにチェックを入れてユーザー作成を完了
⚠️ ここを飛ばすと権限ゼロのユーザーができます。「グループに追加」を選んだのに空のグループ(またはどのグループにも入れない)だと、後で terraform
apply が AccessDenied で全滅します。bootstrap は S3・予算・IAM ロールを作るため、実質管理者権限が必要です。
付随の注意2点
- 「コンソールへのアクセスを提供」のチェックは不要です。このユーザーの用途は
CLI(terraform)だけなので、パスワードは作らずアクセスキーのみで運用するのが安全。作成後に「セキュリティ認証情報」タブ → アクセスキー作成(用途:
CLI)へ進んでください
- ルートユーザー(アカウント作成時のメールのユーザー)では作業しないこと。ルートには MFA を掛けて金庫にしまい、日常は今作る admins
ユーザーを使う、が鉄則です
ポリシーはまず AdministratorAccess をアタッチしても構いません(本番で使う IAM ユーザーではなく、あくまで学習用と割り切ります)。「セキュリティ認証情報」タブからアクセスキーを発行し、Access Key ID と Secret Access Key を控えておきます。
注意: アクセスキーは本番インフラを管理するための長期的な手段としては推奨されません。第3回で GitHub Actions + OIDC を使ったキーレス認証に「卒業」します。今は練習用と割り切ってください。
認証情報の設定
aws configure
プロンプトに従って、以下を入力します。
AWS Access Key ID: (発行したアクセスキー ID)
AWS Secret Access Key: (発行したシークレットアクセスキー)
Default region name: ap-northeast-1
Default output format: json
設定は ~/.aws/credentials と ~/.aws/config に保存されます。Terraform はデフォルトでこのファイルを読みます。
動作確認
aws sts get-caller-identity
自分のアカウント ID と IAM ユーザー情報が表示されれば成功です。
5. ハンズオン①: S3 バケットを1個作って消す
Terraform の一周を体験するために、S3 バケットを 1 個作って消します。課金は極めて少額(空のバケットが数分存在しても 1 円未満)ですが、最後に必ず terraform destroy で片付けます。
ファイル構成
作業ディレクトリを作って、main.tf という名前のファイルを 1 つ作ります。
terraform-hands-on/
└── main.tf
main.tf の中身
# Terraform 本体の設定と使用するプロバイダーを宣言する
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# AWS プロバイダーの設定(リージョン)
provider "aws" {
region = "ap-northeast-1"
}
# S3 バケットを宣言する
# "aws_s3_bucket" がリソースの種類、"my_bucket" がこのコード内での名前(ラベル)
resource "aws_s3_bucket" "my_bucket" {
bucket = "terraform-hands-on-20260612-あなたの名前など"
# S3 バケット名はグローバルで一意である必要があります
# 他の人と被らないよう日付や名前を入れてください
}
S3 バケット名はグローバルで一意である必要があるため、terraform-hands-on-20260612- の後に自分だけの文字列(名前のイニシャルや乱数など)を入れてください。
一周してみる
Step 1: init(プロバイダーのダウンロード)
cd terraform-hands-on
terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.x.x...
Terraform has been successfully initialized!
.terraform/ ディレクトリが作られ、AWS プロバイダーのバイナリがダウンロードされます。
Step 2: plan(ドライラン)
terraform plan
Terraform will perform the following actions:
# aws_s3_bucket.my_bucket will be created
+ resource "aws_s3_bucket" "my_bucket" {
+ bucket = "terraform-hands-on-20260612-xxx"
+ id = (known after apply)
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
+ create が1件、変更・削除がゼロであることを確認します。これが今 apply したら何が起きるかの全量です。
Step 3: apply(実際に作成)
terraform apply
plan と同じ内容が表示され、最後に確認プロンプトが出ます。
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
yes と入力すると、数秒で S3 バケットが作成されます。
aws_s3_bucket.my_bucket: Creating...
aws_s3_bucket.my_bucket: Creation complete after 3s
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
AWS コンソールの S3 画面を確認すると、バケットが作られているはずです。
同時に、作業ディレクトリに terraform.tfstate というファイルが生成されています。中身は JSON で、「今このバケットが存在する」という情報が記録されています。
Step 4: destroy(片付け)
terraform destroy
# aws_s3_bucket.my_bucket will be destroyed
- resource "aws_s3_bucket" "my_bucket" {
- bucket = "terraform-hands-on-20260612-xxx"
...
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
Enter a value: yes
- destroy が 1 件であることを確認してから yes と入力します。バケットが削除されます。
課金防止のルール: 本シリーズのハンズオンは毎回最後に terraform destroy を実行して片付けます。「後で見ようと思って放置」が予期しない課金の原因になります。
6. ハンズオン②: variable / output / data を足す
ハンズオン①では値をコードに直書きしていました。Terraform には変数・出力・外部データの仕組みがあります。実際のプロジェクトではこれらを使って柔軟に管理します。
variable — 入力変数
変数を使うとバケット名をコード外から渡せます。main.tf を以下のように書き換えます。
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
# 変数の宣言
variable "bucket_name" {
description = "作成する S3 バケットの名前(グローバルで一意である必要があります)"
type = string
}
resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name # var.<変数名> で参照する
}
plan / apply 時にバケット名を渡す方法は3通りあります。
# 方法①: 実行時に対話入力
terraform plan
# → "var.bucket_name" の入力を求められる
# 方法②: コマンドラインフラグで渡す
terraform plan -var="bucket_name=terraform-hands-on-20260612-xxx"
# 方法③: terraform.tfvars ファイルに書く(推奨)
# terraform.tfvars の内容:
# bucket_name = "terraform-hands-on-20260612-xxx"
terraform plan # 自動で terraform.tfvars を読む
terraform.tfvars が最もよく使われる方法です。環境ごとに production.tfvars / staging.tfvars を用意してコード本体と分離することもできます。
output — 出力値
apply 後に確認したい値(ARN など)を出力できます。
# apply 後にバケットの ARN を表示する
output "bucket_arn" {
description = "作成した S3 バケットの ARN"
value = aws_s3_bucket.my_bucket.arn
}
terraform apply
# ...
# Outputs:
# bucket_arn = "arn:aws:s3:::terraform-hands-on-20260612-xxx"
output は後述の S3 backend 設定や、モジュール間でリソースの参照を渡すときにも活用します。
data — 外部データの参照
data ブロックを使うと、Terraform 外で存在するリソースや情報を参照できます。よく使う例として、現在の AWS アカウント ID の取得があります。
# 現在の AWS アカウント情報を取得する
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
他には、VPC ID を名前で検索して取得したり、最新の AMI ID を動的に引いたりするのに data ブロックを使います。resource が「作る」のに対し、data は「読む(参照する)」と覚えておけば十分です。
ハンズオン②も終わったら terraform destroy で片付けます。
terraform destroy -var="bucket_name=terraform-hands-on-20260612-xxx"
7. state の正体と置き場所
ローカル state の危険性
ハンズオンではローカルの terraform.tfstate に state が保存されていました。これには深刻な問題が2つあります。
問題①: PC が壊れると台帳が消える
terraform.tfstate が失われると、Terraform は「今クラウドに何が存在するか」を把握できなくなります。コードを再 apply すると「既存リソースと同じ名前のものを新規作成しようとしてエラー」か「リソースが存在するのに管理外扱いになって操作できない」という状態になります。
問題②: チームで共有できない
ローカルファイルなので、他のメンバーが apply すると state が分裂します。
S3 backend への移行
state の置き場所を S3 バケットに移すことで、チームでの共有とバックアップが解決します。これを remote backend(リモートバックエンド) と呼びます。
まず state 保存用の S3 バケットを手動(またはハンズオン①のコードで)作成します。バケット名の例: my-app-terraform-state
バージョニングを有効にしておくと、state の誤変更も巻き戻せます(コンソールでバケットのバージョニングを ON にしてください)。
main.tf の terraform ブロックに backend の設定を追加します。
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# state を S3 バケットに保存する
backend "s3" {
bucket = "my-app-terraform-state" # 作成した state 用バケット名
key = "hands-on/terraform.tfstate" # バケット内のファイルパス
region = "ap-northeast-1"
}
}
backend を変更した後は terraform init を再実行します。ローカル state を S3 に移行するか聞かれるので yes と答えると、既存の state が S3 にコピーされます。
state を git にコミットしてはいけない
terraform.tfstate は git の管理対象から外すことが必須です。
state ファイルには、RDS のパスワード・アクセスキー・秘密鍵など、機密情報が平文で含まれる場合があります。うっかり git push してしまうと、GitHub のパブリックリポジトリに秘密情報が載ることになります。
.gitignore に必ず追加してください。
# Terraform の state ファイルとバックアップ
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl # これは逆にコミット推奨(プロバイダーバージョンの固定)
補足: .terraform.lock.hcl(プロバイダーのバージョンロックファイル)はコミットしてください。チームメンバーやCI環境で init したときに同じバージョンのプロバイダーが使われるようになります。*.tfstate とは別の扱いです。
8. ディレクトリ構成の基本形
ハンズオンでは main.tf 1ファイルに全部書きましたが、実際のプロジェクトでは役割ごとにファイルを分けます。
基本の3ファイル構成
environments/production/
├── main.tf # リソース定義(resource / data / provider)
├── variables.tf # 変数の宣言(variable ブロック)
├── outputs.tf # 出力の宣言(output ブロック)
└── terraform.tfvars # 変数の実際の値(git 管理してもOK。秘密情報はここに書かない)
Terraform は同一ディレクトリ内の .tf ファイルをすべて読むので、ファイル名の分け方はあくまで人間のための整理です。「どこに書いても動く」が「どこに何があるかわかりやすくするために分ける」という考え方です。
環境とモジュール(第2回への布石)
大きなプロジェクトでは次のような構成がよく使われます。
.
├── environments/
│ ├── production/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── staging/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── modules/
├── vpc/ # ネットワーク設定のモジュール
├── ecs-service/ # ECS サービスのモジュール
└── rds/ # RDS の設定モジュール
modules/ に共通のリソース構成を書いておくと、environments/staging/ と environments/production/ で同じモジュールを呼び出してパラメーターだけ変えられます。「コードを書いて,複数環境に同じ構成を展開する」というのが IaC のゴールの一つです。
第2回では VPC・ECS・RDS を実際にこの構成で書いていきます。今回は「こういう構成になる」と頭の片隅に置いておくだけで十分です。
まとめ
第1回で押さえたことを振り返ります。
| 概念 | 一言まとめ |
|---|---|
| IaC | インフラをコードで管理する考え方。再現・ロールバック・PR レビューが可能になる |
| Terraform | AWS も Cloudflare も同一言語で管理できる IaC ツール |
| HCL | 「あるべき状態」を宣言する Terraform の言語 |
| state | 今何が作られているかの台帳。git 管理禁止・S3 backend に置く |
| plan → apply | 変更のドライラン確認 → 実際に反映。plan は必ず読む |
| destroy | 学習後は必ず実行して課金を防ぐ |
よくある最初のつまずき
- バケット名がすでに使われている: S3 バケット名はグローバルで一意です。日付 + 自分の名前など、ユニークな文字列を入れてください
-
認証エラー:
aws configureの設定を確認してください。aws sts get-caller-identityが通れば Terraform も動きます -
terraform initを忘れて plan が失敗する: プロバイダーの追加・変更後は必ずinitを再実行してください
第2回の予告
第2回では、いよいよ本番に向けた AWS 構築を Terraform で行います。VPC(ネットワーク)の設計から始め、RDS MySQL・ElastiCache(Valkey)・ECS Fargate(Spring Boot)・ALB を順番に建てていきます。
「どのリソースをどの順番で作るか」という依存関係の整理と、ネットワーク設計(パブリック・プライベートサブネット)の考え方を丁寧に扱う予定です。
本シリーズは個人開発プロジェクトの実際の構成を題材にしています。コード例は最小限の動作する形を優先しているため、プロダクション利用時はセキュリティグループの絞り込み・IAM の最小権限付与・ログ設定などを別途検討してください。
