前回の記事では、S3とCloudFrontで公開したAstroブログに、Route 53とACMを使って独自ドメインとHTTPSを設定する流れをまとめました。 まだそこまで進んでいない場合は、先にAstroブログに独自ドメインを設定してみる!Route 53・ACM・CloudFrontでHTTPS化から読むとつながりやすいです。

その前段として、Astroでブログを作ってみる!記事を書くまでの流れを初心者向けに解説と、AstroブログをAWSで公開してみる!S3とCloudFrontで静的サイトを配信する流れもあります。

今回はその続きです。 ローカルで毎回 npm run build して、手作業でS3へアップロードするのはだんだん面倒になってきます。 そこで、GitHub ActionsからAWSへデプロイする流れを整理します。

ただし、最初に大事な注意です。 このブログ「あれこれびより」の本番デプロイは、現時点では push しただけで勝手に動く完全自動デプロイではありません。 GitHubへpushしたあと、GitHub ActionsのWorkflowを手動実行する形にしています。

今回やること

今回のゴールは、GitHub ActionsからAstroブログをビルドして、AWSのS3とCloudFrontへ反映する流れを理解することです。

ざっくり書くと、こういう流れです。

ローカルで記事やコードを修正
  ↓
Git commit / push
  ↓
GitHub上のコードが更新される
  ↓
GitHub Actionsの本番デプロイWorkflowを手動実行
  ↓
npm ci
  ↓
記事・画像チェック
  ↓
npm run build
  ↓
OIDCでAWSのIAM Roleを一時的に引き受ける
  ↓
dist/ をS3へ同期
  ↓
CloudFrontのキャッシュを無効化
  ↓
本番サイトへ反映

今回のポイントは、AWSの長期Access KeyをGitHub Secretsへ保存しないことです。 GitHub Actions OIDCを使って、GitHub ActionsがAWSのデプロイ用IAM Roleを一時的に使う形にしています。

前回までの構成

前回までで、ブログの公開構成はだいたいここまで来ています。

Astro
  ↓ npm run build
dist/
  ↓
Amazon S3
  ↓
Amazon CloudFront
  ↓
Route 53
  ↓
https://example.com

Astroで生成した dist/ をS3へ置き、読者はCloudFront経由でアクセスします。 独自ドメインはRoute 53でCloudFrontへ向け、HTTPS用の証明書はACMで用意する構成でした。

ここまでできるとブログとしては公開できます。 ただ、記事を直すたびに手動でアップロードする運用だと、だんだんつらくなります。

毎回S3へ手動アップロードするのは面倒

記事を1本追加するたびに、手元でビルドして、S3へアップロードして、CloudFrontのキャッシュを更新する。 最初の1回ならまだよいですが、ブログを続けるならこれは地味に面倒です。

しかも、手作業だとミスもしやすいです。

  • npm run build を忘れる
  • 古い dist/ をアップロードしてしまう
  • 消した記事のファイルがS3に残る
  • CloudFrontのキャッシュ更新を忘れる
  • 本番に必要な環境変数を入れ忘れる

こういう作業は、GitHub Actionsに任せたほうが楽です。 人間は記事を書くことに集中して、決まった手順はWorkflowにやってもらう感じっすね。

GitHub Actionsって何?

GitHub Actionsは、GitHub上で処理を自動実行できる仕組みです。

たとえば、このブログでは次のような作業をWorkflowに書いています。

  • リポジトリをcheckoutする
  • Node.jsをセットアップする
  • npm ci で依存関係を入れる
  • 記事と画像をチェックする
  • Astroをビルドする
  • AWSへ認証する
  • S3へ dist/ を同期する
  • CloudFrontのキャッシュを無効化する

このような一連の作業を、.github/workflows/deploy.yml に書いています。

今回作るデプロイの流れ

GitHub ActionsからAWSへデプロイする流れは、かなりざっくり言うとこうです。

GitHub Actions
      │
      │ OIDC
      ▼
AWS IAM / STS
      │
      │ AssumeRole
      ▼
デプロイ用IAM Role
      │
      ├── S3
      │
      └── CloudFront

ここで出てくる言葉がちょっと多いですね。 でも、最初はこう考えると分かりやすいです。

GitHub ActionsがAWSへ「このWorkflowは、このリポジトリの、この本番デプロイです」と示します。 AWS側はTrust Policyで条件を確認し、問題なければデプロイ用IAM Roleを一時的に使わせます。 その一時的な権限で、S3同期やCloudFront Invalidationを実行する、という流れです。

AWSへどうやってログインする?

ここで初心者が気になりやすいのが、「誰がAWSへログインしているの?」というところです。

今回の構成では、人間のIAMユーザーでログインしているわけではありません。 また、AWS_ACCESS_KEY_ID や AWS_SECRET_ACCESS_KEY をGitHubへ保存しているわけでもありません。

GitHub ActionsがOIDCを使って、AWSのデプロイ用IAM Roleを一時的に引き受けています。

このときAWS側では、STSという仕組みを使って一時的な認証情報が発行されます。 STSはSecurity Token Serviceの略で、AWSで一時的な認証情報を扱うサービスです。 ただ、この記事では深追いしすぎず、「短時間だけ使えるAWSの権限を受け取る仕組み」くらいで大丈夫です。

Access KeyをGitHubへ保存しない

GitHub ActionsからAWSを操作する記事では、次のようなSecretsを登録する例もよく見かけます。

AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY

これは長期間使えるAccess KeyをGitHub側へ保存する方式です。 もちろん、適切に管理すれば使える構成ではあります。 ただ、Keyが長期間有効なぶん、漏れたときの影響も考える必要があります。

今回の構成では、この長期Access Key方式は使っていません。

従来の一例

GitHub Secrets
  ↓
長期間有効なAccess Key
  ↓
AWS

今回の構成はこうです。

GitHub Actions
  ↓
OIDC
  ↓
AWS STS
  ↓
一時的な認証情報
  ↓
AWS

長期Access KeyをGitHub側へ保存しなくてよいのが大きなメリットです。 ただし、OIDCにしたら何でも安全になる、というわけではありません。 IAM Roleの権限やTrust Policyをきちんと絞ることが大事っすね。

GitHub Actions OIDCって何?

OIDCはOpenID Connectの略です。 GitHub Actionsでは、OIDCを使って外部クラウドへ認証できます。

GitHub公式ドキュメントでも、AWS向けOIDCでは長期のAWS認証情報をGitHub Secretsへ保存せずにAWSへアクセスできる、と説明されています。 参考: Configuring OpenID Connect in Amazon Web Services

AWS側では、GitHubのOIDC Providerとして次のURLを使います。

https://token.actions.githubusercontent.com

Audienceは、公式の aws-actions/configure-aws-credentials を使う場合、通常は次の値です。

sts.amazonaws.com

このあたりは仕様に関わる部分なので、実際に作るときはAWSとGitHubの公式ドキュメントを確認するのが安全です。

AWS側にデプロイ用IAM Roleを用意する

GitHub ActionsからAWSを操作するには、AWS側にデプロイ用IAM Roleを用意します。

記事ではダミー名として、次のようなRole名で考えます。

arekore-blog-github-deploy-role

このRoleには、ブログのデプロイに必要な権限だけを付けます。 今回の代表例はこのあたりです。

  • 対象S3 Bucketを一覧する
  • 対象S3 Bucketへファイルを配置する
  • 対象S3 Bucketから不要ファイルを削除する
  • 対象CloudFront Distributionのキャッシュを無効化する

つまり、デプロイ用Roleだからといって AdministratorAccess のような強い権限を付ける必要はありません。 ブログのデプロイに必要な範囲へ絞るのが基本です。

Trust PolicyでGitHubからの利用を制限する

IAM Roleには、権限ポリシーとは別にTrust Policyがあります。

Trust Policyは、ざっくり言うと「このRoleを誰が使ってよいか」を決める設定です。 GitHub Actions OIDCの場合は、GitHubのOIDC Providerから来たリクエストを条件付きで許可します。

AWS公式ドキュメントでも、OIDC Provider用のRoleでは sts:AssumeRoleWithWebIdentity を使う説明があります。 参考: Create a role for OpenID Connect federation

記事用にかなり簡略化した例を書くと、こんな感じです。 実際のAWS Account IDやRepository名は必ず自分の環境に合わせます。 次の repo:example-user/example-blog:environment:production は、従来形式の sub を使っているRepository向けの例です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:example-user/example-blog:environment:production"
        }
      }
    }
  ]
}

GitHub Environmentを使うWorkflowでは、sub に environment:production のような形が出てきます。 GitHub公式ドキュメントでも、Environmentを使う場合の sub はEnvironment名を参照する形になる、と説明されています。

さらに、GitHub公式のOIDC referenceでは、2026年7月15日以降に作成されたRepositoryは、immutable subject claims形式がデフォルトになると説明されています。 この形式では、sub の中にowner IDとrepository IDが含まれます。 一方で、それ以前に作成されたRepositoryは、opt-inなどをしていなければ従来形式のままです。 また、2026年7月15日以降にRepositoryをrenameしたりtransferしたりした場合も、immutable形式へ移行する場合があります。

GitHub Environmentを使う場合は、従来形式でもimmutable形式でも、Environment名がsubjectへ反映されます。 なので、実際に作るときは、自分のRepositoryが発行する sub の形式とAWS側のTrust Policyを一致させる必要があります。

初心者向けに言うと、「GitHubなら何でもOK」ではなく、「このRepositoryの、このEnvironmentからだけOK」に絞るのが大事、ということっすね。

GitHub Environmentを用意する

このブログの本番デプロイWorkflowでは、GitHub Environmentとして production を使っています。

Workflowには次のように書かれています。

environment: production

Environmentを使うと、本番デプロイ用のVariablesやSecretsを分けて管理できます。 さらに、GitHub側で本番EnvironmentへデプロイできるBranchを制限したり、承認ルールを置いたりできます。

このブログのWorkflowでは、次のような値をEnvironment Variablesとして参照しています。

AWS_REGION
AWS_ROLE_ARN
S3_BUCKET
CLOUDFRONT_DISTRIBUTION_ID
PUBLIC_SITE_URL
PUBLIC_GA4_ID
PUBLIC_VIEWS_API_URL

ここで注意したいのは、Variablesに入れてよい値とSecretsにすべき値を分けることです。

たとえば、公開サイトURLやAWSリージョンのように知られても困りにくい値はVariablesで扱いやすいです。 一方で、API SecretやTokenのような機密情報はSecretsに入れるべきです。

また、Astroの PUBLIC_* はクライアントへ公開される前提の値です。 PUBLIC_* にAWS Secret Access Keyのような秘密情報を入れるのは絶対に避けます。

GitHub Actions Workflowを作る

このブログの本番デプロイWorkflowは、.github/workflows/deploy.yml にあります。

起動条件は workflow_dispatch です。

on:
  workflow_dispatch:

つまり、push時に自動実行されるのではなく、GitHub Actions画面から手動実行します。 この運用だと、pushしただけでは本番反映されません。

また、Workflowには同時実行を抑える設定もあります。

concurrency:
  group: deploy-production
  cancel-in-progress: true

本番デプロイが重なって動くとややこしいので、同じグループの実行を制御している形ですね。

permissionsでOIDC Tokenを使えるようにする

OIDCを使うには、Workflowに次のpermissionsが必要です。

permissions:
  contents: read
  id-token: write

contents: read は、Repositoryをcheckoutするために使います。

id-token: write は、GitHub ActionsがOIDC Tokenを取得するために必要です。 GitHub公式ドキュメントでも、OIDC ProviderがJWTを作るために id-token: write が必要だと説明されています。

ここでいう write は、GitHubのコードを書き換える権限という意味ではありません。 OIDC Tokenをリクエストできるようにするための権限です。 名前だけ見るとちょっと怖く見えますが、役割を分けて考えると分かりやすいです。

Astroをbuildする

このブログのWorkflowでは、まずRepositoryをcheckoutし、Node.jsをセットアップします。

- name: Checkout
  uses: actions/checkout@v4

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: npm

Node.jsは 22 を使っています。 cache: npm も設定しているので、npmのキャッシュを使って依存関係のインストールを速くしやすくしています。

依存関係のインストールは npm ci です。

- name: Install dependencies
  run: npm ci

npm ci は package-lock.json を前提に、CI向けに依存関係をきれいに入れるコマンドです。 package.json と package-lock.json がずれていると失敗することがあります。 本番デプロイ前に気づけるので、CIでは npm install より npm ci のほうが向いています。

そのあと、記事と画像のチェックを実行しています。

- name: Check content
  run: npm run check:content

- name: Check images
  run: npm run check:images

このブログ独自のチェックですね。 Frontmatterの不足、views管理、画像参照切れなどを見ています。

最後にAstroをビルドします。

- name: Build
  run: npm run build

package.json では、npm run build は次のようになっています。

{
  "build": "astro check && astro build"
}

つまり、Astroのチェックをしてから静的ファイルを生成します。 生成された dist/ が、S3へ同期する対象です。

OIDCでAWSのRoleを引き受ける

ビルドが終わったら、AWSへ認証します。

このブログでは、aws-actions/configure-aws-credentials@v4 を使っています。

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ env.AWS_ROLE_ARN }}
    aws-region: ${{ env.AWS_REGION }}
    audience: sts.amazonaws.com
    role-session-name: GitHubActions-${{ github.run_id }}

このActionは、GitHub ActionsのOIDC Tokenを使ってAWSのRoleを引き受け、以降の aws コマンドで使える認証情報を設定してくれます。 公式のActionのREADMEでも、OIDCを使ったRole引き受けの設定例が説明されています。 参考: aws-actions/configure-aws-credentials

この記事では、あれこれびよりで現在使用している v4 を掲載しています。 新しく導入する場合は、公式READMEで現在の推奨バージョンや最新バージョンを確認してください。

ここで実際に使うRole ARNは、GitHub EnvironmentのVariablesから渡しています。 記事本文では実値を書かず、必要なら次のようなダミーで説明します。

arn:aws:iam::123456789012:role/arekore-blog-github-deploy-role

distをS3へ同期する

AWS認証ができたら、dist/ をS3へ同期します。

このブログのWorkflowでは、通常ファイルと _astro 配下でCache-Controlを分けています。 考え方としては、次のようなコマンドです。

aws s3 sync dist "s3://example-blog-bucket" \
  --delete \
  --exclude "_astro/*" \
  --cache-control "public,max-age=0,must-revalidate"

if [ -d "dist/_astro" ]; then
  aws s3 sync dist/_astro "s3://example-blog-bucket/_astro" \
    --delete \
    --cache-control "public,max-age=31536000,immutable"
fi

--delete は、dist/ から消えたファイルをS3側からも削除するための指定です。 記事を削除したときに、S3に古いHTMLが残り続けるのを防ぎやすくなります。

また、_astro 配下はAstroが生成するCSSやJavaScriptなどのアセットが入ります。 ファイル名にハッシュが付くものは、長くキャッシュしやすいです。 一方でHTMLなどは更新されやすいので、短めに再確認する設定にしています。

ここは地味ですが、かなり大事なところです。 キャッシュ設定を雑にすると、「デプロイしたのに古い表示が残る」みたいなことが起きやすいっすね。

CloudFrontのキャッシュを更新する

S3へ新しいファイルを置いても、CloudFrontに古いキャッシュが残っている場合があります。

そのため、このブログではデプロイの最後にCloudFront Invalidationを実行しています。

aws cloudfront create-invalidation \
  --distribution-id "EXXXXXXXXXXXXX" \
  --paths "/*"

Distribution IDは環境ごとに違うので、記事ではダミー値にしています。

初心者向けに言うと、CloudFront Invalidationは「CloudFrontに残っている古いキャッシュを更新対象にする」作業です。 S3へ最新ファイルを置くだけではなく、CloudFront側も更新されるようにしているわけですね。

Workflowを実行してみる

このブログのWorkflowは手動実行です。 なので、流れとしてはこうです。

ローカルで記事やコードを修正
  ↓
git commit
  ↓
git push
  ↓
GitHub ActionsのDeploy Workflowを手動実行

ここで、Workflowには次の条件も入っています。

if: github.ref == 'refs/heads/main'

つまり、mainブランチ以外では本番デプロイJobが動かないようにしています。 さらにGitHub Environment production 側でも、mainだけ本番デプロイできるように制限しておくと安心です。

IAM側は「GitHub Environment productionからのOIDC」を見て、GitHub側は「mainだけproductionへデプロイできる」ようにする。 このように、AWS側とGitHub側で役割を分けて守るのがよいです。

デプロイできたか確認する

Workflowが成功したら、本番サイトを確認します。

見るポイントはこのあたりです。

  • トップページが更新されている
  • 新しい記事ページが開ける
  • CSSや画像が表示されている
  • 404にならない
  • 古い記事を消した場合、古いURLが残っていない
  • CloudFront経由で最新内容が見えている

もし反映が遅れて見える場合は、CloudFrontのキャッシュやブラウザキャッシュの影響もあります。 ただ、このブログのWorkflowでは最後に create-invalidation --paths "/*" を実行しているので、少なくともCloudFront側へ更新対象として伝える処理は入っています。

うまくいかないときに見るところ

GitHub Actions + AWS OIDCで詰まるときは、だいたい見る場所が決まっています。

まず、npm ci で落ちている場合です。 この場合はAWS以前の問題で、package.json と package-lock.json の不一致や依存関係の問題を見ます。

次に、npm run check:content や npm run check:images で落ちている場合です。 記事Frontmatter、views管理、画像ファイルの置き忘れなどを確認します。

AWS認証で落ちる場合は、次のあたりを見ます。

  • permissions に id-token: write があるか
  • aws-actions/configure-aws-credentials の設定が正しいか
  • AWS_ROLE_ARN がEnvironment Variablesに設定されているか
  • IAM RoleのTrust PolicyがWorkflowの条件と合っているか
  • GitHub Environment production をWorkflowで使っているか
  • mainブランチから実行しているか

S3同期で落ちる場合は、S3 Bucket名やIAM RoleのS3権限を見ます。 CloudFront Invalidationで落ちる場合は、Distribution IDや cloudfront:CreateInvalidation の権限を確認します。

エラーが出たら、まず「どのStepで落ちたか」を見るのが大事です。 最初から全部疑うとしんどいので、落ちたStepごとに切り分ける感じっすね。

OIDCにしたら何がよかった?

OIDCにした一番のメリットは、長期Access KeyをGitHubへ保存しなくてよいところです。

今回の構成では、GitHub ActionsがAWSへ直接パスワードのようなKeyを持っているわけではありません。 GitHub ActionsがOIDC Tokenを取得し、AWSが条件を確認し、問題なければ一時的な認証情報を発行します。

よかったところをまとめると、こんな感じです。

  • 長期Access KeyをGitHubへ保存しなくてよい
  • デプロイ専用IAM Roleに権限を絞れる
  • Trust Policyで利用元を制限できる
  • GitHub Environmentと組み合わせて本番デプロイを管理しやすい
  • Workflow側のAWS認証が分かりやすくなる

一方で、OIDCだから絶対安全というわけではありません。 Trust Policyが広すぎたり、IAM Roleに強すぎる権限を付けたりすると危ないです。

なので、OIDCは「Access Keyを置かないための便利な仕組み」ですが、IAM設計はちゃんと考える必要があります。

まとめ

今回は、GitHub ActionsとOIDCを使って、AstroブログをAWSへデプロイする流れを整理しました。

このブログでは、現時点で次のような運用にしています。

  • workflow_dispatch で本番デプロイWorkflowを手動実行する
  • mainブランチのときだけJobを実行する
  • GitHub Environment production を使う
  • Node.js 22でビルドする
  • npm ci で依存関係を入れる
  • check:content と check:images を実行する
  • npm run build で dist/ を作る
  • OIDCでAWSのデプロイ用IAM Roleを引き受ける
  • dist/ をS3へ同期する
  • _astro 配下は長期キャッシュ向けに分けて同期する
  • CloudFront Invalidationでキャッシュ更新を行う

これで、手元から毎回S3へアップロードしなくても、GitHub ActionsからAWSへ安全寄りにデプロイできるようになります。

ブログ運用では、記事を書くこと以外にも、ビルド、アップロード、キャッシュ更新など細かい作業が出てきます。 そこをWorkflowに任せられると、かなり気が楽になりますね。

今後は、閲覧数カウンターやLambda + DynamoDB、AWS監視まわりも少しずつ整理していきたいです。

ではでは、おつかれっした~。