GitHub Actions + Terraform で構築する S3 自動デプロイ環境
🌱 はじめに
GitHub Actions を使って静的サイトを AWS S3 に自動デプロイする CI/CD 環境を構築しました。本記事では、Terraform で IAM Role を作成し、GitHub Actions の OIDC 認証で安全にデプロイする方法を、実装時の工夫ポイントや検討事項を含めて解説します。
🚀 実現すること
Git にプッシュすると、自動的に S3 にデプロイ → CloudFront キャッシュ無効化を実行するワークフローを作成します。
開発者が develop ブランチに push
↓
GitHub Actions が起動
↓
OIDC 認証で AWS にアクセス
↓
S3 に全ファイルを同期
↓
CloudFront キャッシュを無効化
↓
デプロイ完了! ✨
前提条件
- ✅ S3 + CloudFront による静的サイトホスティング環境がすでに構築済み
- ✅ GitHub リポジトリに静的ファイル(HTML/CSS/JavaScript)が格納されている
- ✅ Terraform がローカルにインストールされている
🛠️ Terraform で IAM Role を作成
1. OIDC Provider の作成
GitHub Actions が AWS と連携するための OIDC プロバイダーを作成します。
resource "aws_iam_openid_connect_provider" "github_oidc" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# GitHub は AWS によって信頼されたプロバイダーのため
# thumbprint は実質的に無視されるが、API 要件のため設定
thumbprint_list = ["0000000000000000000000000000000000000000"]
}
2. IAM Role と Policy の作成
GitHub Actions が AssumeRole できる IAM Role を作成します。
# ------------------------------
# Variables
# ------------------------------
variable "aws_account_id" {
description = "AWS Account ID"
type = string
}
variable "s3_bucket_name" {
description = "S3 Bucket Name for static site"
type = string
}
variable "cloudfront_distribution_id" {
description = "CloudFront Distribution ID (optional)"
type = string
default = ""
}
variable "github_org" {
description = "GitHub Organization or Username"
type = string
}
variable "github_repo" {
description = "GitHub Repository Name"
type = string
}
variable "github_branch" {
description = "GitHub Branch Name"
type = string
default = "develop"
}
variable "environment" {
description = "Environment (e.g., dev, staging, prod)"
type = string
default = "dev"
}
# ------------------------------
# Assume Role Policy
# ------------------------------
data "aws_iam_policy_document" "github_actions_assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github_oidc.arn]
}
# GitHub Actions からの認証であることを確認
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
# 特定のリポジトリとブランチからのみ許可
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${var.github_org}/${var.github_repo}:ref:refs/heads/${var.github_branch}"]
}
}
}
# ------------------------------
# IAM Role
# ------------------------------
resource "aws_iam_role" "github_actions_s3_deploy" {
name = "github-actions-s3-deploy-${var.environment}"
assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
}
# ------------------------------
# S3 + CloudFront Policy
# ------------------------------
data "aws_iam_policy_document" "s3_deploy_policy" {
# S3 操作権限
statement {
sid = "S3Sync"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
]
resources = [
"arn:aws:s3:::${var.s3_bucket_name}",
"arn:aws:s3:::${var.s3_bucket_name}/*"
]
}
# CloudFront キャッシュ無効化権限(オプション)
dynamic "statement" {
for_each = var.cloudfront_distribution_id != "" ? [1] : []
content {
sid = "CloudFrontInvalidation"
effect = "Allow"
actions = [
"cloudfront:CreateInvalidation",
"cloudfront:GetInvalidation"
]
resources = [
"arn:aws:cloudfront::${var.aws_account_id}:distribution/${var.cloudfront_distribution_id}"
]
}
}
}
resource "aws_iam_policy" "s3_deploy_policy" {
name = "github-actions-s3-deploy-policy-${var.environment}"
description = "Policy for GitHub Actions to deploy to S3"
policy = data.aws_iam_policy_document.s3_deploy_policy.json
}
resource "aws_iam_role_policy_attachment" "attach_s3_deploy_policy" {
role = aws_iam_role.github_actions_s3_deploy.name
policy_arn = aws_iam_policy.s3_deploy_policy.arn
}
# ------------------------------
# Outputs
# ------------------------------
output "github_actions_role_arn" {
description = "IAM Role ARN for GitHub Actions"
value = aws_iam_role.github_actions_s3_deploy.arn
}
terraform apply 後、出力された github_actions_role_arn を控えます。この ARN を次のステップで GitHub Actions ワークフローに設定します。
🫧 GitHub Actions ワークフローの作成
1. ディレクトリ構成
.github/
└── workflows/
├── reusable-deploy.yml # 再利用可能なワークフロー
└── dev.yml # Dev 環境用トリガー
2. 再利用可能なワークフロー
このワークフローは Dev、Production など複数の環境で再利用できるように設計しています。
name: "S3 Static Site Deployment"
on:
workflow_call: # 他のワークフローから呼び出し可能にする
inputs:
aws_region:
required: true
type: string
description: AWS Region
role_arn:
required: true
type: string
description: IAM Role ARN to assume via OIDC
s3_bucket_name:
required: true
type: string
description: Target S3 Bucket Name
build_directory:
required: true
type: string
description: Directory containing static assets (e.g., 'dist')
cf_distribution_id:
required: false # CloudFront を使わない環境にも対応
type: string
description: CloudFront Distribution ID for invalidation (optional)
permissions:
id-token: write # OIDC トークンを取得するために必須
contents: read # リポジトリのコードを読み取るために必要
jobs:
deploy:
name: Deploy to S3
runs-on: ubuntu-latest
timeout-minutes: 10 # デプロイが長時間かかる場合は早期に失敗
env:
# 環境変数として定義することで、スクリプト内で簡潔に参照可能
AWS_REGION: ${{ inputs.aws_region }}
S3_BUCKET: ${{ inputs.s3_bucket_name }}
BUILD_DIR: ${{ inputs.build_directory }}
CF_DIST_ID: ${{ inputs.cf_distribution_id }}
steps:
# ----------------------------------------------------
# Step 1: Preparation
# ----------------------------------------------------
- name: Checkout Code
uses: actions/checkout@v4
- name: Generate AWS Session Name
id: session
run: |
# リポジトリ名から organization/user 名を除去
repo="${GITHUB_REPOSITORY#"${GITHUB_REPOSITORY_OWNER}"/}"
# "リポジトリ名-実行ID-試行回数" の形式でセッション名を生成
echo "name=${repo}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}"
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ inputs.role_arn }} # OIDC で IAM Role を引き受ける
aws-region: ${{ inputs.aws_region }}
role-session-name: ${{ steps.session.outputs.name }} # 追跡可能なセッション名
# ----------------------------------------------------
# Step 2: S3 Sync
# ----------------------------------------------------
- name: Sync files to S3
id: s3_sync # 後続ステップで outputs を参照するために ID を設定
run: |
echo "=========================================="
echo ">> Starting S3 Sync"
echo "=========================================="
echo "Target: s3://${S3_BUCKET}"
echo "Source: ${BUILD_DIR}"
echo ""
# S3 同期を実行してログを保存
# --delete: S3 側にあってローカルにないファイルを削除
# --no-progress: 進捗表示を無効化
# --no-cli-pager: ページャーを無効化
# tee: 標準出力とファイルの両方に出力
aws s3 sync "${BUILD_DIR}" "s3://${S3_BUCKET}" \
--delete \
--no-progress \
--no-cli-pager \
2>&1 | tee /tmp/s3-sync.log
# 統計情報をカウント(Summary 表示用)
UPLOADED_COUNT=$(grep -c "^upload:" /tmp/s3-sync.log 2>/dev/null || echo "0")
DELETED_COUNT=$(grep -c "^delete:" /tmp/s3-sync.log 2>/dev/null || echo "0")
# 改行や空白を削除(GITHUB_OUTPUT のフォーマットエラーを防ぐ)
UPLOADED_COUNT=$(echo "${UPLOADED_COUNT}" | tr -d '\n\r ')
DELETED_COUNT=$(echo "${DELETED_COUNT}" | tr -d '\n\r ')
# GitHub Actions の outputs に保存(後続ステップで使用)
echo "uploaded=${UPLOADED_COUNT}" >> "${GITHUB_OUTPUT}"
echo "deleted=${DELETED_COUNT}" >> "${GITHUB_OUTPUT}"
# 削除されたファイルのログを出力(視認性向上のため色付け)
if [ "${DELETED_COUNT}" -gt 0 ]; then
RED='\033[0;31m'
YELLOW='\033[1;33m'
BOLD='\033[1m'
NC='\033[0m'
echo ""
echo -e "${YELLOW}=========================================="
echo -e "${BOLD}>> Deleted Files (${DELETED_COUNT})${NC}"
echo -e "${YELLOW}==========================================${NC}"
grep "^delete:" /tmp/s3-sync.log | while IFS= read -r line; do
# S3 URL からパスのみ抽出(見やすくするため)
file_path=$(echo "$line" | sed 's/^delete: s3:\/\/[^\/]*\///')
echo -e "${RED}[-] ${file_path}${NC}"
done
echo -e "${YELLOW}==========================================${NC}"
fi
echo ""
echo "[OK] Sync completed! (Uploaded: ${UPLOADED_COUNT}, Deleted: ${DELETED_COUNT})"
# ----------------------------------------------------
# Step 3: CloudFront Cache Invalidation (Optional)
# ----------------------------------------------------
- name: Invalidate CloudFront Cache
if: env.CF_DIST_ID != '' # CloudFront が設定されている場合のみ実行
run: |
echo ""
echo "=========================================="
echo ">> CloudFront Cache Invalidation"
echo "=========================================="
echo "Distribution ID: ${CF_DIST_ID}"
echo ""
# CloudFront のキャッシュを全て無効化
aws cloudfront create-invalidation \
--distribution-id "${CF_DIST_ID}" \
--paths "/*"
echo ""
echo "[OK] Cache invalidation completed!"
# ----------------------------------------------------
# Step 4: Generate Deployment Summary
# ----------------------------------------------------
- name: Generate Deployment Summary
if: always() # 前のステップが失敗しても必ず実行
run: |
# 前のステップから統計情報を取得
UPLOADED="${{ steps.s3_sync.outputs.uploaded }}"
DELETED="${{ steps.s3_sync.outputs.deleted }}"
# デフォルト値(S3 Sync がスキップされた場合の安全策)
UPLOADED="${UPLOADED:-0}"
DELETED="${DELETED:-0}"
# GitHub Actions の Job Summary を生成
# GitHub UI の Summary タブに表示され、デプロイ結果を一目で確認できる
{
echo "## Deployment Summary"
echo ""
echo "| Item | Value |"
echo "|------|-------|"
echo "| **S3 Bucket** | \`${S3_BUCKET}\` |"
echo "| **Build Directory** | \`${BUILD_DIR}\` |"
echo "| **Session Name** | \`${{ steps.session.outputs.name }}\` |" # 追跡用
echo "| **Uploaded** | ${UPLOADED} files |"
echo "| **Deleted** | ${DELETED} files |"
# CloudFront の情報(設定されている場合のみ)
if [ -n "${CF_DIST_ID}" ]; then
echo "| **CloudFront** | \`${CF_DIST_ID}\` (cache invalidated) |"
fi
echo ""
# ステータスメッセージ
echo "> **Status:** Deployment completed successfully"
} >> "$GITHUB_STEP_SUMMARY"
ワークフローで工夫したポイント
-
パラメータ化 📦
- 環境固有の値(S3 Bucket、IAM Role など)を
inputsで外部から注入 - CloudFront は optional にすることで、CDN なしの環境にも対応
- 環境固有の値(S3 Bucket、IAM Role など)を
-
統計情報の可視化 📊
-
aws s3 syncの結果を解析し、アップロード・削除ファイル数を取得 - GitHub Actions の Job Summary に見やすい表形式で表示
- 削除されたファイルは別途色付きで表示
-
-
エラーハンドリング 🛡️
-
if: always()で Summary を必ず表示 - デフォルト値(
${UPLOADED:-0})でステップスキップ時も安全
-
-
追跡可能性 🔍
- AWS Session Name にリポジトリ名、実行 ID、試行回数を含める
- CloudTrail で後から特定のデプロイを追跡可能
3. Dev 環境用トリガー
name: "Deploy to S3"
on:
push:
branches:
- develop
jobs:
dev:
uses: ./.github/workflows/reusable-deploy.yml
with:
aws_region: 'ap-northeast-1'
role_arn: 'arn:aws:iam::123456789012:role/github-actions-s3-deploy-dev'
s3_bucket_name: 'my-static-website-bucket'
build_directory: 'dist'
cf_distribution_id: 'E1A2B3C4D5E6F7' # オプション
✅ 動作確認
静的サイトのファイルを変更して develop ブランチにプッシュすると、GitHub の Actions タブで以下のように表示されます。

GitHub Actions の Job Summary には以下のような表が表示されます。
| Item | Value |
|---|---|
| S3 Bucket | my-static-website-bucket |
| Build Directory | dist |
| Session Name | your-repo-123456789-1 |
| Uploaded | 150 files |
| Deleted | 1 files |
| CloudFront |
E1A2B3C4D5E6F7 (cache invalidated) |
Status: Deployment completed successfully
⚠️ 実装時の検討ポイント
1. デプロイ最適化の検討
気づいた挙動
GitHub Actions で 2 回目以降のデプロイでも、aws s3 sync のログに全ファイルが upload: と表示されることがわかりました。
upload: dist/index.html to s3://...
upload: dist/css/style.css to s3://...
upload: dist/js/app.js to s3://...
...(全ファイル)
原因
aws s3 sync はデフォルトで サイズと最終更新日時 を比較します。GitHub Actions の ephemeral 環境では以下の理由で全ファイルが再アップロードされます。
- 毎回クリーンな環境: ローカルに前回のデータがない
-
タイムスタンプの不一致:
- ローカルファイル: Git checkout 時のタイムスタンプ(通常は現在時刻)
- S3 ファイル: 前回アップロード時のタイムスタンプ
- → 常に「ローカルの方が新しい」と判断される
結果、内容が変わっていなくても全ファイルが再アップロードされてしまいます。
検討した選択肢
「更新したファイルのみアップロードできないか?」と考え、以下のような選択肢を検討しましたが、いずれも不採用としました。
| 解決策 | 概要 | 不採用の理由 |
|---|---|---|
--size-only |
ファイルサイズのみで比較 | サイズが同じでも内容が変わっている場合を検出できない |
--exact-timestamps |
タイムスタンプを厳密に比較 | Git checkout 時のタイムスタンプと S3 のアップロード時刻が常に異なる |
| Git commit hash 管理 | S3 に .deploy-manifest.txt を保存 |
静的サイト用バケットに管理ファイルを配置したくない |
| GitHub Actions Cache | 前回デプロイ情報をキャッシュ | 保持期間が 7 日間で実用性に欠ける |
aws s3api で事前比較 |
ETag を手動で比較 | 実装が複雑で、aws s3 sync が内部で同じことをしている |
採用した解決策: シンプルさを優先
aws s3 syncが数秒で完了していたため、複雑な差分管理の仕組みを導入するよりも、シンプルさと確実性を優先して全ファイルアップロードの方針にしました。
2. ワークフロー品質チェックのすすめ
GitHub Actions ワークフローを書く際、YAML 構文エラーやシェルスクリプトのエラーに遭遇することがあります。コミット前にチェックすることで、開発効率が向上します。
推奨ツール
1. actionlint 🔍
GitHub Actions ワークフローの構文チェックツールです。
# インストール(macOS)
brew install actionlint
# ワークフローファイルをチェック
actionlint .github/workflows/*.yml
検出できるエラー例
- YAML 構文エラー(例: 角括弧のクォート忘れ
name: [Dev]→name: "[Dev]") - 未定義の inputs や secrets の参照
- シェルスクリプトの静的解析(shellcheck と連携)
2. shellcheck 🛡️
シェルスクリプトの静的解析ツールです。actionlint を実行すると、内部的に shellcheck を呼び出してワークフロー内の run: セクションもチェックします。
# インストール(macOS)
brew install shellcheck
# actionlint を実行すると shellcheck も自動で動作
actionlint .github/workflows/dev.yml
📝 さいごに
この記事が、GitHub Actions + terraform の安全な CI/CD 環境構築の参考になれば幸いです! 🎉
えみり〜でした|ωΦ)ฅ
Discussion