介護士がTerraform + GitHub Actions + OIDCでブログのCI/CDを構築した話
Next.jsの静的ブログをS3 + CloudFrontにデプロイするCI/CDパイプラインを、Terraform + GitHub Actions OIDCで構築した記録。介護士がインフラを組んだ、詰まったところも含めて残す。
git pushしたら、本番に反映される。
それだけのことを実現するために、かなりの時間を使った。
この記事では、Next.jsの静的ブログをS3 + CloudFrontにデプロイするCI/CDパイプラインをTerraform + GitHub Actions OIDCで構築した話を書く。
介護士がインフラを組んだ記録として、詰まったところも含めて残しておく。
![]()
全体構成
まず完成形の構成を示す。
TerraformがAWSリソース全体を管理し、GitHub Actionsがビルドとデプロイを自動化する。
Terraformで管理したリソース
S3バケット
静的ファイルの置き場。CloudFrontからのみアクセスを許可し、パブリックアクセスはすべてブロックする。
resource "aws_s3_bucket" "blog" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "blog" {
bucket = aws_s3_bucket.blog.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_policy" "blog" {
bucket = aws_s3_bucket.blog.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCloudFrontAccess"
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.blog.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.blog.arn
}
}
}
]
})
}CloudFront Distribution
S3の前段に置いてキャッシュと高速配信を担う。OAC(Origin Access Control)でS3との接続を制御する。
resource "aws_cloudfront_origin_access_control" "blog" {
name = "blog-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "blog" {
origin {
domain_name = aws_s3_bucket.blog.bucket_regional_domain_name
origin_id = "S3Origin"
origin_access_control_id = aws_cloudfront_origin_access_control.blog.id
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.url_rewrite.arn
}
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}CloudFront Function
ここが一番ハマったポイントだ。後述するが、ファイルを作るだけでは動かない。
Next.jsの静的エクスポートでは、/posts/helloというURLは/posts/hello.htmlまたは/posts/hello/index.htmlとして出力される。CloudFrontはデフォルトでこのマッピングを自動解決しないため、URLを書き換えるFunctionが必要になる。
resource "aws_cloudfront_function" "url_rewrite" {
name = "url-rewrite"
runtime = "cloudfront-js-2.0"
publish = true
code = file("${path.module}/functions/url-rewrite.js")
}// functions/url-rewrite.js
async function handler(event) {
const request = event.request;
const uri = request.uri;
// 拡張子がある場合はそのまま
if (uri.match(/\.[a-zA-Z0-9]+$/)) {
return request;
}
// トレイリングスラッシュがある場合はindex.htmlを追加
if (uri.endsWith('/')) {
request.uri += 'index.html';
return request;
}
// それ以外は/index.htmlを追加
request.uri += '/index.html';
return request;
}GitHub Actions用OIDCプロバイダーとIAMロール
アクセスキーを使わずにGitHub ActionsからAWSを操作するための設定。
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-blog-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:${var.github_repo}:ref:refs/heads/main"
}
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
}
}
]
})
}
resource "aws_iam_role_policy" "github_actions" {
name = "blog-deploy-policy"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.blog.arn,
"${aws_s3_bucket.blog.arn}/*"
]
},
{
Effect = "Allow"
Action = "cloudfront:CreateInvalidation"
Resource = aws_cloudfront_distribution.blog.arn
}
]
})
}GitHub Actionsのワークフロー
mainブランチへのpushをトリガーに、ビルドとデプロイを自動実行する。
# .github/workflows/deploy.yml
name: Deploy Blog
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Sync to S3
run: |
aws s3 sync ./out s3://${{ secrets.S3_BUCKET_NAME }} \
--delete \
--cache-control "public, max-age=31536000, immutable"
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"OIDCを使うポイントは2つ。
permissionsにid-token: writeを必ず追加するconfigure-aws-credentialsでrole-to-assumeを指定する
この2つが揃わないと認証に失敗する。最初はここで詰まった。
詰まったところ
OIDCのパーミッション設定漏れ
最初、permissionsブロックを書いていなかった。
エラーメッセージはError: Credentials could not be loaded。最初は原因が分からなかったが、id-token: writeがないとGitHub ActionsがOIDCトークンを発行できないことが分かった。
CloudFront Functionが有効化されていなかった問題
これが一番時間を使った。
terraform applyしたのに動かない。GitHub Actionsは成功しているのに個別記事だけ403になる。CloudFrontのキャッシュかと思って無効化しても直らない。S3のバケットポリシーを疑っても直らない。
JSファイルは存在する。Terraformのコードにもaws_cloudfront_functionリソースは定義されている。
でも、AWS上にFunctionが作成されていなかった。
Terraformのコード上では定義していたつもりだったが、実際にはterraform applyが正しく実行されておらず、AWS上にFunctionが存在しない状態だった。当然DistributionへのAssociationも行われていない。だからリクエストがFunctionを通らず、URLの書き換えが発生しないまま403を返し続けていた。
terraform planで改めて確認すると、Functionがcreateされていないことが分かった。terraform applyを正しく実行し直してFunctionを作成、Distributionに関連付けることで解消した。
修正はfunction_associationブロックを正しく書き直すだけだった。でも原因特定に半日かかった。この話は次の記事で詳しく書く。
なお、この構成は記事③で紹介したClaude CodeのPlanner・Generator・Evaluatorチームと一緒に構築した。私自身は要件整理・レビュー・検証・トラブルシューティングを担当した。CloudFront Functionの問題のように「何が起きているか分からない」局面では、EvaluatorにTerraformの差分を読ませて原因を絞り込んだ。全部を自分で理解しながら進めたわけではないが、実際に動く環境を作り、エラーを解消し、本番公開までたどり着いた経験は大きな学びになった。
やってみて分かったこと
TerraformとGitHub Actionsを組み合わせると、インフラの状態がコードで管理できる。
「なんとなく動いている」状態から、「なぜ動いているか分かる」状態になった。
それは介護記録に似ていると思った。口頭で申し送りをするより、記録に残す方が、あとで読んだ人が状態を理解できる。インフラもコードに残すことで、未来の自分が読み返せる。
CloudFrontの403エラーで半日溶かした話は、次の記事に書く。
