🚀

Terraform導入の壁「初期構築」をbashで自動化する

に公開

はじめに

Terraformを導入する最大の目的は、インフラのすべてをコードで管理し、環境の再現性を完全に保証することにあります。

しかし、その高尚な目的を阻む、皮肉な障害物が存在します。
それは、Terraformを使い始める瞬間に必ず発生する「初期構築作業」です。

Terraformの状態管理ファイル(tfstate)を保存するS3バケットや、排他制御(Locking)を行うDynamoDBテーブル。これら自体を管理するためには、Terraformを実行する前に手動でこれらを作成しなければなりません。「すべてをコードで」と言っておきながら、最初の一歩だけはAWSマネジメントコンソールをポチポチ操作するか、泥臭いCLIコマンドを叩く必要があるのです。

この「鶏と卵」の問題を解決し、初期構築すらも自動化してしまいましょう。
今回は、TerraformのRemote Backend(S3 + DynamoDB)を一撃で構築し、さらに不要になったら綺麗に削除するbashスクリプトを紹介します。


1. 作るもの

今回作成する自動化スクリプトの要件は以下の通りです。

  1. Backendの構築 (setup.sh):
    • tfstate保存用のS3バケット作成(バージョニング・暗号化有効)
    • Lock管理用のDynamoDBテーブル作成
    • 重要: 作成したリソース情報を記載した backend.tf を自動生成する
  2. Backendの破棄 (destroy.sh):
    • S3バケットとDynamoDBテーブルを安全に削除する
    • バージョニングされたオブジェクトも強制削除できるようにする

2. 初期構築スクリプト (setup.sh)

まずは構築用のスクリプトです。
プロジェクト名やリージョンを指定でき、バケット名は世界で一意になるようタイムスタンプを付与しています。最後に backend.tf を吐き出すのがポイントです。

setup-backend.sh
#!/bin/bash
set -e

# --- デフォルト値 ---
PROJECT_NAME="my-tf-project"
REGION="ap-northeast-1"
PROFILE_OPT=""
PROFILE_NAME=""

# --- ヘルプ関数 ---
usage() {
    echo "Usage: $0 [OPTIONS]"
    echo "Options:"
    echo "  -p, --profile <profile>  AWS CLI profile name"
    echo "  -n, --name <name>        Project name (used for bucket/table prefix)"
    echo "  -r, --region <region>    AWS Region (default: ap-northeast-1)"
    echo "  -h, --help               Show this help message"
    exit 1
}

# --- 引数の解析 ---
while [[ "$#" -gt 0 ]]; do
    case $1 in
        -p|--profile) PROFILE_NAME="$2"; PROFILE_OPT="--profile $2"; shift ;;
        -n|--name) PROJECT_NAME="$2"; shift ;;
        -r|--region) REGION="$2"; shift ;;
        -h|--help) usage ;;
        *) echo "Unknown parameter: $1"; usage ;;
    esac
    shift
done

# --- 変数定義 ---
# バケット名はグローバル一意にするためタイムスタンプを付与
BUCKET_NAME="${PROJECT_NAME}-tfstate-$(date +%s)"
DYNAMODB_TABLE="${PROJECT_NAME}-tf-lock"

echo "=== Terraform Backend Setup Start ==="
echo "Project : $PROJECT_NAME"
echo "Profile : ${PROFILE_NAME:-"(default)"}"
echo "Region  : $REGION"
echo "Bucket  : $BUCKET_NAME"
echo "Table   : $DYNAMODB_TABLE"
echo "-------------------------------------"

# 1. S3バケットの作成
echo "[1/4] Creating S3 Bucket..."
if [ "$REGION" == "us-east-1" ]; then
    aws $PROFILE_OPT s3api create-bucket --bucket "$BUCKET_NAME" --region "$REGION" > /dev/null
else
    aws $PROFILE_OPT s3api create-bucket --bucket "$BUCKET_NAME" --region "$REGION" \
    --create-bucket-configuration LocationConstraint="$REGION" > /dev/null
fi

# 2. S3のバージョニング有効化
echo "[2/4] Enabling S3 Versioning..."
aws $PROFILE_OPT s3api put-bucket-versioning --bucket "$BUCKET_NAME" --versioning-configuration Status=Enabled

# 3. S3の暗号化有効化
echo "[3/4] Enabling S3 Encryption..."
aws $PROFILE_OPT s3api put-bucket-encryption --bucket "$BUCKET_NAME" --server-side-encryption-configuration '{
  "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
}'

# 4. DynamoDBテーブルの作成
echo "[4/4] Creating DynamoDB Table for Locking..."
if aws $PROFILE_OPT dynamodb describe-table --table-name "$DYNAMODB_TABLE" --region "$REGION" > /dev/null 2>&1; then
    echo "  -> Table already exists. Skipping."
else
    aws $PROFILE_OPT dynamodb create-table \
        --table-name "$DYNAMODB_TABLE" \
        --attribute-definitions AttributeName=LockID,AttributeType=S \
        --key-schema AttributeName=LockID,KeyType=HASH \
        --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
        --region "$REGION" > /dev/null
    
    echo "  -> Waiting for DynamoDB table to be active..."
    aws $PROFILE_OPT dynamodb wait table-exists --table-name "$DYNAMODB_TABLE" --region "$REGION"
fi

# --- 結果出力 (backend.tf) ---
echo ""
echo "=== Setup Complete! ==="
echo "以下の内容を 'backend.tf' として保存してください。"
echo "※ 注意: Terraform実行時にもプロファイルの指定が必要です(後述)"
echo "---------------------------------------------------------"
cat << EOF
terraform {
  required_version = ">= 1.0.0"

  backend "s3" {
    bucket         = "${BUCKET_NAME}"
    key            = "terraform.tfstate"
    region         = "${REGION}"
    encrypt        = true
    dynamodb_table = "${DYNAMODB_TABLE}"
  }
}

provider "aws" {
  region  = "${REGION}"
}
EOF
echo "---------------------------------------------------------"

使い方の例

chmod +x setup-backend.sh
./setup-backend.sh -n my-service -p my-aws-profile

実行するとリソースが作成され、コンソールに backend.tf の中身が出力されます。これをファイルに保存すれば、即座に terraform init が可能です。


3. お片付けスクリプト (destroy.sh)

ハンズオンや検証環境などで、「作った環境を一掃したい」という場面も多々あります。
特にS3バケットはバージョニングを有効にしていると、中身(過去のバージョン含む)をすべて消さないとバケット自体を削除できません。これを手動でやるのは苦行なので、スクリプトで解決します。

destroy-backend.sh
#!/bin/bash
set -e

# --- デフォルト値 ---
PROJECT_NAME="my-tf-project"
REGION="ap-northeast-1"
PROFILE_OPT=""
TARGET_BUCKET=""

# --- ヘルプ関数 ---
usage() {
    echo "Usage: $0 [OPTIONS]"
    echo "Options:"
    echo "  -p, --profile <profile>  AWS CLI profile name"
    echo "  -n, --name <name>        Project name (used to find resources)"
    echo "  -r, --region <region>    AWS Region (default: ap-northeast-1)"
    echo "  -b, --bucket <bucket>    (Optional) Specify bucket name directly"
    echo "  -h, --help               Show this help message"
    exit 1
}

# --- 引数の解析 ---
while [[ "$#" -gt 0 ]]; do
    case $1 in
        -p|--profile) PROFILE_OPT="--profile $2"; shift ;;
        -n|--name) PROJECT_NAME="$2"; shift ;;
        -r|--region) REGION="$2"; shift ;;
        -b|--bucket) TARGET_BUCKET="$2"; shift ;;
        -h|--help) usage ;;
        *) echo "Unknown parameter: $1"; usage ;;
    esac
    shift
done

# DynamoDBテーブル名は固定ルール
DYNAMODB_TABLE="${PROJECT_NAME}-tf-lock"

echo "=== Terraform Backend Destruction (Force Mode) ==="
echo "Project : $PROJECT_NAME"
echo "Region  : $REGION"

# --- 1. S3バケットの特定 ---
if [ -z "$TARGET_BUCKET" ]; then
    echo "Searching for S3 bucket with prefix: ${PROJECT_NAME}-tfstate- ..."
    FOUND_BUCKETS=$(aws $PROFILE_OPT s3api list-buckets --query "Buckets[?starts_with(Name, '${PROJECT_NAME}-tfstate-')].Name" --output text)
    
    BUCKET_ARRAY=($FOUND_BUCKETS)
    COUNT=${#BUCKET_ARRAY[@]}

    if [ "$COUNT" -eq 0 ]; then
        echo "Error: No bucket found starting with '${PROJECT_NAME}-tfstate-'."
        exit 1
    elif [ "$COUNT" -gt 1 ]; then
        echo "Error: Multiple buckets found: ${FOUND_BUCKETS}"
        echo "Please specify bucket name manually using -b option."
        exit 1
    else
        TARGET_BUCKET="${BUCKET_ARRAY[0]}"
        echo "-> Found bucket: $TARGET_BUCKET"
    fi
else
    echo "-> Using specified bucket: $TARGET_BUCKET"
fi

echo "Target Table  : $DYNAMODB_TABLE"
echo "-------------------------------------"

# --- 2. 最終確認 ---
echo "WARNING: This will PERMANENTLY DELETE the S3 bucket (including all versions) and DynamoDB table."
read -p "Are you sure you want to proceed? (Type 'yes' to confirm): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
    echo "Aborted."
    exit 0
fi

echo ""

# --- 3. S3バケットの中身を完全消去 (バージョニング対応) ---
echo "[1/3] Emptying S3 Bucket..."

# バケットが存在するか確認
if aws $PROFILE_OPT s3api head-bucket --bucket "$TARGET_BUCKET" 2>/dev/null; then
    
    # 1. オブジェクトのバージョン(履歴)を削除
    echo "  -> Deleting object versions..."
    VERSIONS=$(aws $PROFILE_OPT s3api list-object-versions --bucket "$TARGET_BUCKET" --output=json --query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')
    if [ "$(echo "$VERSIONS" | grep -o 'Key')" ]; then
        aws $PROFILE_OPT s3api delete-objects --bucket "$TARGET_BUCKET" --delete "$VERSIONS" > /dev/null
    fi

    # 2. 削除マーカー(ゴミ)を削除
    echo "  -> Deleting delete markers..."
    MARKERS=$(aws $PROFILE_OPT s3api list-object-versions --bucket "$TARGET_BUCKET" --output=json --query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')
    if [ "$(echo "$MARKERS" | grep -o 'Key')" ]; then
        aws $PROFILE_OPT s3api delete-objects --bucket "$TARGET_BUCKET" --delete "$MARKERS" > /dev/null
    fi

    echo "  -> Bucket is now empty."

    # --- 4. S3バケットの削除 ---
    echo "[2/3] Deleting S3 Bucket..."
    aws $PROFILE_OPT s3 rb "s3://${TARGET_BUCKET}"
    echo "  -> Bucket deleted."

else
    echo "  -> Bucket not found or already deleted."
fi

# --- 5. DynamoDBテーブルの削除 ---
echo "[3/3] Deleting DynamoDB Table..."
if aws $PROFILE_OPT dynamodb describe-table --table-name "$DYNAMODB_TABLE" --region "$REGION" > /dev/null 2>&1; then
    aws $PROFILE_OPT dynamodb delete-table --table-name "$DYNAMODB_TABLE" --region "$REGION" > /dev/null
    echo "  -> Waiting for table deletion..."
    aws $PROFILE_OPT dynamodb wait table-not-exists --table-name "$DYNAMODB_TABLE" --region "$REGION"
    echo "  -> Table deleted."
else
    echo "  -> Table not found or already deleted."
fi

echo ""
echo "=== Destruction Complete ==="

ポイント

  • Safety Check: 誤操作を防ぐため、実行時に yes と入力しない限り削除は実行されません。
  • s3 rb --force: バージョニング有効化されたバケットでも、中身ごと強制削除するオプションを使用しています。
  • バケット自動検索: プロジェクト名(プレフィックス)から対象バケットを自動で見つけます。

💡コラム:エンタープライズ環境との違い

本記事の手法は、個人開発やスタートアップ、PoC(実証実験)フェーズに最適化しています。
数百のアカウントを管理するようなエンタープライズ環境では、AWS Control Tower (AFT) 等を用いて、アカウント発行時にこれらBackendリソースを自動生成するアーキテクチャ(Account Vending Machine)が一般的です。
しかし、そうした大規模な仕組みを作る前の「最初の一歩」や、手元の検証環境をサクッと作る用途において、今回紹介する「Bootstrapスクリプト」は依然として強力な武器になります。


おわりに

Terraformは強力ですが、使い始めるまでの準備が意外と面倒です。
このスクリプトをプロジェクトのリポジトリに scripts/ ディレクトリなどを作って置いておけば、新しいメンバーが参加した時や、検証環境をサクッと作りたい時に大いに役立つはずです。

初期構築の苦労を自動化して、本来注力すべき「インフラコードの記述」に時間を使いましょう!

Discussion