インフラの3R — AWSリソースの設計品質
インフラもコードである
「3Rはコードだけの話じゃない」
田中さんがAWSのマネジメントコンソールを開いた。本番環境のリソース一覧には、誰が作ったかわからないEC2インスタンス、意味不明な名前のセキュリティグループ、用途不明のS3バケットが散らばっていた。
「これは誰が作ったんですか?」ユイが聞いた。
「わからない。3年前の担当者が退職して、ドキュメントも残っていない。変えようとすれば何かが壊れるかもしれないので、誰も触れない。インフラのレガシーコードだ」
これはコードの世界で見たFat Modelと同じ問題だ。「何をしているかわからない」「変えると怖い」「再利用できない」——3Rの問題が、インフラにも現れている。
「インフラも可読性・再利用性・リファクタリング可能性が必要だ。それを実現するのが**IaC(Infrastructure as Code)**だ」
INFO
IaC(Infrastructure as Code)とは、インフラの構成をコードで管理する手法です。AWS CloudFormation、Terraform、AWS CDKなどがある。インフラをコードで管理することで、3Rをインフラにも適用できます。「コンソールでポチポチ作る」ClickOpsから卒業しましょう。
なぜClickOpsが問題なのか
ClickOps(マネジメントコンソールでの手動操作)の問題:
Readability: 誰もコンソールを見に行かないと設定がわからない
コンソールのスクリーンショットはすぐ古くなる
Reusability: ステージング環境を作るとき、また同じ手順でポチポチ
「確かこうだったはず」という記憶頼りの再現
Refactorability: 変更前の状態に戻せない(Undo がない)
変更の履歴が残らない(誰が何をいつ変えたかわからない)
変更をレビューできない(PRがない)
IaC(Terraform / CloudFormation)の利点:
Readability: コードを読めばインフラ構成がわかる
コードがドキュメント(ドキュメントが古くなる問題がない)
Reusability: モジュール化して複数環境で再利用できる
`staging` の環境を作るのが `terraform apply` の1コマンド
Refactorability: git で変更履歴が残る
PRでインフラ変更のレビューができる
`terraform destroy` で確実に削除できる
R1: インフラの可読性
悪いCloudFormation: 何をするかわからない
# Before: リソース名が意味を持たない(コードのx, y, zと同じ問題)
AWSTemplateFormatVersion: '2010-09-09'
Resources:
R1:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0123456789abcdef0
InstanceType: t3.medium
SecurityGroupIds:
- !Ref SG1
SG1:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: sg
VpcId: vpc-12345678
B1:
Type: AWS::S3::Bucket
Properties:
BucketName: my-bucket-12345
# → R1がEC2サーバーで、SG1がセキュリティグループで、B1がS3...
# 読んでもすぐに意味がわからない# After: リソース名と説明が意図を表す(コードのorder, userと同じ原則)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ECサイト本番環境 — アプリケーションサーバー層(ECS Fargate + ALB)'
Parameters:
EnvironmentName:
Type: String
AllowedValues: [production, staging, development]
Description: 'デプロイ環境名(リソース名に使用される)'
AppImageUri:
Type: String
Description: 'ECRのコンテナイメージURI'
Resources:
# アプリケーションサーバー用セキュリティグループ
AppServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'アプリサーバー用セキュリティグループ — ALBからのHTTPS通信のみ許可'
VpcId: !ImportValue !Sub '${EnvironmentName}-VpcId'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3000
ToPort: 3000
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
Description: 'ALBからのトラフィックのみ許可(直接アクセス不可)'
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-app-server-sg'
- Key: Purpose
Value: 'アプリケーションサーバーへのアクセス制御'
# 静的ファイル用S3バケット
StaticAssetsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${EnvironmentName}-ec-site-static-assets'
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled # バージョン管理でアセットのロールバックが可能
Tags:
- Key: Purpose
Value: 'フロントエンドの静的ファイル(JS, CSS, 画像)の配信用'命名規則を決める:
リソース命名規則: {環境名}-{プロジェクト名}-{リソース種別}[-{用途}]
例:
production-ec-site-alb # 本番のALB
staging-ec-site-ecs-app # ステージングのECSタスク
production-ec-site-rds-primary # 本番のRDSプライマリ
production-ec-site-sg-app-server # 本番のアプリサーバー用SG
production-ec-site-s3-static # 本番の静的ファイル用S3
R2: インフラの再利用性
Terraformモジュールで共通パターンを再利用する
RailsのService ObjectやGemと同様に、Terraformでも「よく使うパターン」をモジュール化する。
# modules/rails_fargate_app/main.tf
# 再利用可能なRails on ECS Fargateアプリモジュール
variable "app_name" {
description = "アプリケーション名(例: ec-site, admin-panel)"
type = string
}
variable "environment" {
description = "環境名 (production/staging/development)"
type = string
validation {
condition = contains(["production", "staging", "development"], var.environment)
error_message = "environmentはproduction, staging, developmentのいずれかにしてください"
}
}
variable "cpu" {
description = "ECSタスクのCPUユニット数(256=0.25vCPU, 512=0.5vCPU, 1024=1vCPU)"
type = number
default = 256
}
variable "memory" {
description = "ECSタスクのメモリ(MB)"
type = number
default = 512
}
variable "container_image" {
description = "ECRイメージURI"
type = string
}
variable "environment_variables" {
description = "コンテナの環境変数"
type = map(string)
default = {}
}
variable "secrets" {
description = "AWS Secrets Managerから取得するシークレット"
type = list(object({
name = string
valueFrom = string
}))
default = []
}
# ECSクラスター
resource "aws_ecs_cluster" "app" {
name = "${var.app_name}-${var.environment}"
setting {
name = "containerInsights"
value = "enabled" # CloudWatchダッシュボードで監視可能に
}
tags = {
Name = "${var.app_name}-${var.environment}-cluster"
Environment = var.environment
ManagedBy = "Terraform"
}
}
# CloudWatch ロググループ(コンテナログの保存先)
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${var.app_name}-${var.environment}"
retention_in_days = var.environment == "production" ? 90 : 7
tags = {
Environment = var.environment
}
}
# ECSタスク定義
resource "aws_ecs_task_definition" "app" {
family = "${var.app_name}-${var.environment}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.cpu
memory = var.memory
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([{
name = "app"
image = var.container_image
portMappings = [{
containerPort = 3000
protocol = "tcp"
}]
environment = [
for key, value in var.environment_variables : {
name = key
value = value
}
]
secrets = var.secrets
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "ecs"
}
}
}])
}
output "cluster_arn" {
value = aws_ecs_cluster.app.arn
description = "ECSクラスターのARN(他のモジュールから参照可能)"
}
output "task_definition_arn" {
value = aws_ecs_task_definition.app.arn
description = "タスク定義のARN"
}# 本番環境でのモジュール使用(EC サイト)
# environments/production/main.tf
module "ec_site" {
source = "../../modules/rails_fargate_app"
app_name = "ec-site"
environment = "production"
cpu = 1024 # 本番は1vCPU
memory = 2048 # 本番は2GB
container_image = "${aws_ecr_repository.ec_site.repository_url}:${var.image_tag}"
environment_variables = {
RAILS_ENV = "production"
RAILS_LOG_LEVEL = "warn"
DATABASE_POOL = "20"
}
secrets = [
{
name = "DATABASE_URL"
valueFrom = "arn:aws:secretsmanager:ap-northeast-1:${data.aws_caller_identity.current.account_id}:secret:production/ec-site/database_url"
},
{
name = "RAILS_MASTER_KEY"
valueFrom = "arn:aws:secretsmanager:ap-northeast-1:${data.aws_caller_identity.current.account_id}:secret:production/ec-site/rails_master_key"
}
]
}
# ステージング環境での同じモジュール再利用
module "ec_site_staging" {
source = "../../modules/rails_fargate_app"
app_name = "ec-site"
environment = "staging"
cpu = 256 # ステージングは最小構成
memory = 512 # コスト削減
container_image = "${aws_ecr_repository.ec_site.repository_url}:${var.staging_image_tag}"
environment_variables = {
RAILS_ENV = "staging"
RAILS_LOG_LEVEL = "debug" # ステージングは詳細ログ
DATABASE_POOL = "5"
}
secrets = [
{
name = "DATABASE_URL"
valueFrom = "arn:aws:secretsmanager:ap-northeast-1:${data.aws_caller_identity.current.account_id}:secret:staging/ec-site/database_url"
}
]
}Railsの discount_engine Gemと同じ考え方だ。「共通のパターンを1箇所にまとめ、変更は1箇所で済む」。ECSタスクのデフォルト設定を変えたいとき、rails_fargate_app モジュールを1箇所変えれば、全環境に反映される。
R3: インフラのリファクタリング可能性
Terraformの terraform plan でインフラ変更のプレビュー
コードのテストに相当するのが terraform plan だ。適用前に「何が変わるか」を確認できる。
# 現状の確認
terraform show
# 変更のプレビュー(実際には変更しない)
terraform plan -out=tfplan
# 出力例(変更内容が明確に表示される)
# Plan: 2 to add, 1 to change, 0 to destroy.
# Changes to Outputs:
# ~ alb_dns_name = "production-ec-site-alb-1234567890.ap-northeast-1.elb.amazonaws.com"
# → "production-ec-site-alb-new.ap-northeast-1.elb.amazonaws.com"
# # aws_ecs_task_definition.app will be updated in-place
# ~ resource "aws_ecs_task_definition" "app" {
# ~ cpu = "256" → "512" # ← CPUを0.25vCPU → 0.5vCPUに変更
# ~ memory = "512" → "1024" # ← メモリを512MB → 1024MBに変更
# }インフラのCI/CDパイプライン
# .github/workflows/infrastructure.yml
name: Infrastructure CI/CD
on:
push:
branches: [main]
paths: ['infrastructure/**'] # インフラ関連ファイルの変更時のみ実行
pull_request:
paths: ['infrastructure/**']
jobs:
# PR時: plan の結果をPRにコメントする
terraform_plan:
name: Terraform Plan(変更プレビュー)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.7.0'
- name: Terraform Init
run: terraform init
working-directory: infrastructure/production/
- name: Terraform Validate(構文チェック)
run: terraform validate
working-directory: infrastructure/production/
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan 2>&1 | tee plan_output.txt
working-directory: infrastructure/production/
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-northeast-1
- name: PRにPlanの結果をコメントする
uses: actions/github-script@v7
with:
script: |
const fs = require('fs')
const plan = fs.readFileSync('infrastructure/production/plan_output.txt', 'utf8')
const body = `## Terraform Plan(ステージング→本番)
\`\`\`hcl
${plan.substring(0, 3000)}
\`\`\`
> 本番適用は main マージ後に自動実行されます`
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
})
# mainマージ時: 実際に適用する
terraform_apply:
name: Terraform Apply(本番適用)
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # GitHub Environmentで承認者を設定可能
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
working-directory: infrastructure/production/
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: infrastructure/production/
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-northeast-1Infrastructure Testingでインフラの変更を安全に
Rubyのテストと同様に、インフラにもテストがある。
# spec/infrastructure/production_spec.rb
# Awspec: AWSリソースをRSpecでテストするライブラリ
require 'awspec'
RSpec.describe 'Production インフラ' do
describe security_group('production-ec-site-sg-app-server') do
it 'セキュリティグループが存在する' do
should exist
end
it 'ALBからのHTTP(3000番ポート)のみ許可している' do
should have_ingress_rule(
ip_protocol: 'tcp',
from_port: 3000,
to_port: 3000
)
end
it 'SSHを直接許可していない(踏み台サーバー経由のみ)' do
should_not have_ingress_rule(ip_protocol: 'tcp', from_port: 22, to_port: 22)
end
it '全開放(0.0.0.0/0)を許可していない' do
should_not have_ingress_rule(cidr_blocks: ['0.0.0.0/0'])
end
end
describe s3_bucket('production-ec-site-static-assets') do
it '存在する' do
should exist
end
it 'バージョニングが有効になっている' do
should have_versioning_enabled
end
it 'パブリックアクセスがブロックされている' do
should have_block_public_acls
should have_block_public_policy
should have_ignore_public_acls
should have_restrict_public_buckets
end
end
describe rds_db_instance('production-ec-site-rds-primary') do
it 'マルチAZが有効になっている(本番の可用性要件)' do
should be_multi_az
end
it '自動バックアップが7日間保存される' do
should have_automated_backups_enabled(7)
end
it 'ストレージが暗号化されている' do
should be_encrypted
end
end
endAWS Config Rules で継続的なコンプライアンスチェック
# インフラが「正しい状態を維持しているか」を継続監視する
Resources:
# 暗号化されていないEBSボリュームを検知
EbsEncryptionCheck:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub '${EnvironmentName}-ebs-encryption-check'
Source:
Owner: AWS
SourceIdentifier: ENCRYPTED_VOLUMES
Description: 'すべてのEBSボリュームが暗号化されているか確認'
# S3バケットがパブリックアクセスをブロックしているか確認
S3PublicAccessCheck:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub '${EnvironmentName}-s3-public-access-check'
Source:
Owner: AWS
SourceIdentifier: S3_BUCKET_PUBLIC_READ_PROHIBITED
Description: 'S3バケットが意図せずパブリックアクセスを許可していないか確認'
# RDSがマルチAZになっているか(本番のみ)
RdsMultiAzCheck:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub '${EnvironmentName}-rds-multi-az-check'
Source:
Owner: AWS
SourceIdentifier: RDS_MULTI_AZ_SUPPORT
Scope:
ComplianceResourceTypes:
- AWS::RDS::DBInstance
# IAMユーザーにMFAが設定されているか
IamMfaCheck:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub '${EnvironmentName}-iam-mfa-check'
Source:
Owner: AWS
SourceIdentifier: MFA_ENABLED_FOR_IAM_CONSOLE_ACCESS実践: ECサイトのインフラを3Rで整理する
ユイはAWSの現状を整理した。
Before: ClickOps(マネジメントコンソールで手動作成の惨状)
Readability:
❌ EC2インスタンスが5つ: 何のサーバーか不明(r1, r2, r3...)
❌ セキュリティグループ8つ: ルールの意図が不明
❌ RDS: バックアップ設定が各環境でバラバラ
❌ S3バケット: 命名規則なし(my-bucket-12345, temp-files-backup)
Reusability:
❌ ステージング環境の再構築: 数日かかる(手順書が古い)
❌ 新プロジェクトのインフラ: またゼロから手動構築
Refactorability:
❌ 変更の履歴がない(コンソール操作はログに残らない)
❌ PRレビューができない
❌ 「本番と同じ設定」でステージングを作れない
❌ 壊れた時に元に戻せない
After: IaC + CI/CDパイプライン
Readability:
✅ すべてのリソースはコードで定義(リソース名に意図がある)
✅ コードを読めばインフラ構成がわかる(コンソールを見に行く必要なし)
✅ タグで環境・用途・所有者が明確
Reusability:
✅ Terraformモジュールで新環境を数時間で構築
✅ 本番環境と同じ設定のステージングを `terraform apply` で作成
Refactorability:
✅ すべての変更はPRでレビュー
✅ `terraform plan` で変更内容を事前確認
✅ git でロールバック可能(git revert → terraform apply)
✅ インフラのテスト(Awspec)で変更後の確認が自動化
インフラの3Rサマリー
| 観点 | Before (ClickOps) | After (IaC) |
|---|---|---|
| Readability | コンソールを見て回るしかない | コードを読めばわかる |
| Reusability | 毎回同じ設定を手動で繰り返す | モジュールで瞬時に再現 |
| Refactorability | 変更が怖い(戻せない) | git管理でロールバック可能 |
| レビュー | レビューできない | PRでインフラ変更をレビュー |
| テスト | 本番で確認するしかない | terraform plan + Awspecで事前確認 |
| ドキュメント | コンソールスクショが陳腐化 | コードがドキュメント |
「コードとインフラは別物ではない」ユイは気づいた。「どちらも『変化に耐えるシステムを作る』という同じ課題を持っている。3Rはその答えだ」
「インフラをコードで管理すると、アプリと同じように3Rを適用できる」田中さんが言った。「最後の章で、これをどう習慣化するか話そう。3Rの旅のゴールは『一度やり遂げること』じゃなく、『毎日の習慣にすること』だ」