0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub ActionsからSSHなしでEC2へデプロイする:OIDCとSSM Run Commandの構成

0
Last updated at Posted at 2026-09-28

この記事で扱うこと

GitHub ActionsからAmazon EC2上のLaravelアプリケーションへデプロイする際に、次の構成へ移行したときの考え方と実装例を整理します。

  • AWSの長期アクセスキーをGitHubに保存せず、OpenID Connect(OIDC)で一時認証する
  • GitHub-hosted runnerからEC2へSSH接続せず、AWS Systems Manager Run Commandで処理を実行する
  • コマンドの標準出力・標準エラーをCloudWatch Logsへ残す
  • send-commandの受付だけで成功扱いにせず、コマンド終了まで待って結果を判定する

実案件の識別情報を含めないため、リポジトリ名、IAMロール名、EC2インスタンスID、ロググループ名、配置先はすべてダミーです。

対象読者

  • GitHub ActionsからEC2へデプロイしたい方
  • SSH秘密鍵や長期のAWSアクセスキーをGitHub Actionsで管理したくない方
  • Private SubnetのEC2へ踏み台サーバーなしでコマンドを実行したい方
  • デプロイ失敗時の出力をCloudWatch Logsで追跡したい方

背景と困っていたこと

EC2上のLaravelアプリケーションをGitHub Actionsから更新するにあたり、当初はSSH接続を前提に考えていました。しかし、GitHub-hosted runnerからPrivate SubnetのEC2へ直接接続するには、踏み台、接続元制御、SSH鍵の配布・ローテーションなどを追加で設計する必要があります。

また、AWS CLIを実行するためにIAMユーザーのアクセスキーをGitHubへ長期間保存すると、漏えい時の影響や定期ローテーションの運用が増えます。

そこで、役割を次のように分けました。

GitHub Actions
  └─ OIDCで短期認証情報を取得
       └─ SSM SendCommandを実行
            └─ EC2上のデプロイスクリプトを実行
                 └─ stdout / stderrをCloudWatch Logsへ送信

GitHubの公式ドキュメントでも、OIDCを使うことで長期のAWS認証情報をGitHubの保存値として持たずにAWSリソースへアクセスできると説明されています。

前提環境

今回の実装で確認できている前提は次のとおりです。

  • GitHub ActionsからAWS IAMロールをOIDCで引き受ける構成を作成済み
  • EC2をSystems Managerのマネージドノードとして登録済み
  • SSM Run Commandの出力先となるCloudWatch Logsのロググループを作成済み
  • EC2上でLaravelのデプロイ処理を実行する想定

OS、PHP、Laravel、AWS CLI、SSM Agentの正確なバージョンは記録から確認できなかったため、公開前に実環境の値を追記する場合は [要確認] です。バージョンを特定できない場合は、無理に記載せず「執筆時点のサポート対象バージョン」とする方が安全です。

調査と設計判断

1. AWS認証は固定キーではなくOIDCを使う

GitHub Actionsのジョブに id-token: write を与えると、ワークフローはGitHubのOIDCトークンを要求できます。これはAWSリソースへの書き込み権限そのものではなく、外部サービスの短期認証情報と交換するためのトークンを取得する権限です。

AWS側ではGitHubのOIDC Providerを信頼し、特定のリポジトリ・ブランチ・Environmentからだけ引き受けられるIAMロールを用意します。

2. EC2への命令はSSM Run Commandを使う

SSM Run Commandなら、GitHub-hosted runnerからEC2のSSHポートへ到達できなくても、AWS API経由でマネージドノードにコマンドを送れます。そのため、デプロイのためだけに踏み台サーバーやSSH秘密鍵をGitHub Actionsへ持たせる必要がありません。

ただし、EC2側には次が必要です。

  • SSM Agentが動作していること
  • EC2のインスタンスプロファイルにSystems Manager用の権限があること
  • SSMの各エンドポイントへ到達できること(インターネット向け経路またはVPC Endpoint)

3. send-commandの戻り値だけでは完了を判定しない

aws ssm send-commandが成功しても、それはコマンドの受付に成功した段階です。EC2上のデプロイ処理が最後まで成功したとは限りません。

そこで、返されたCommand IDを使って aws ssm wait command-executed で終了を待ち、get-command-invocation で最終ステータスを確認します。長い出力はCloudWatch Logsへ送り、GitHub Actions側ではステータスを中心に扱います。

実装例

IAMロールの信頼ポリシー

以下は、GitHub Environment production からのみIAMロールを引き受ける例です。

{
  "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-org/example-repo:environment:production"
        }
      }
    }
  ]
}

subを repo:example-org/example-repo:* のように広くしすぎると、意図しないブランチやイベントからもロールを引き受けられる可能性があります。可能な限りブランチまたはEnvironmentまで絞ります。

なお、2026年7月15日以降に作成されたGitHubリポジトリでは、OIDCの sub にowner IDとrepository IDを含む形式が既定になっています。既存リポジトリでも設定により形式が異なるため、実際のclaim形式は公開前・適用前にGitHubの最新ドキュメントと対象リポジトリで [要確認] です。

GitHub Actions用IAMロールの権限

まずは対象のSSM DocumentとEC2インスタンスを限定します。次の例では、実際のインスタンスIDへ置き換えてください。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SendDeployCommand",
      "Effect": "Allow",
      "Action": "ssm:SendCommand",
      "Resource": [
        "arn:aws:ssm:ap-northeast-1:123456789012:document/AWS-RunShellScript",
        "arn:aws:ec2:ap-northeast-1:123456789012:instance/i-0123456789abcdef0"
      ]
    },
    {
      "Sid": "ReadCommandResult",
      "Effect": "Allow",
      "Action": [
        "ssm:GetCommandInvocation",
        "ssm:ListCommandInvocations"
      ],
      "Resource": "*"
    }
  ]
}

複数台へ配布する場合はEC2タグを使った --targets が便利ですが、IAMポリシーも同じ範囲に絞れるかを検証します。最初から ssm:* を許可するのではなく、ワークフローに必要な操作だけを追加します。

GitHub Actionsワークフロー

以下は手動実行できる最小例です。実際にはGitHub Environmentの承認ルールや、テスト完了後だけデプロイジョブを動かす依存関係も設定します。

name: Deploy to EC2

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    timeout-minutes: 20

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v6.3.0
        with:
          role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ap-northeast-1
          role-session-name: deploy-${{ github.run_id }}
          mask-aws-account-id: true

      - name: Send deploy command and wait
        env:
          INSTANCE_ID: ${{ vars.EC2_INSTANCE_ID }}
          LOG_GROUP_NAME: /example/ssm/deploy
        run: |
          set -Eeuo pipefail

          parameters=$(jq -nc \
            --arg sha "$GITHUB_SHA" \
            '{commands: ["sudo -u deploy /opt/example/bin/deploy.sh " + $sha]}')

          command_id=$(aws ssm send-command \
            --document-name AWS-RunShellScript \
            --instance-ids "$INSTANCE_ID" \
            --comment "Deploy from GitHub Actions" \
            --parameters "$parameters" \
            --cloud-watch-output-config \
              "CloudWatchOutputEnabled=true,CloudWatchLogGroupName=$LOG_GROUP_NAME" \
            --query 'Command.CommandId' \
            --output text)

          echo "SSM command: $command_id"

          aws ssm wait command-executed \
            --command-id "$command_id" \
            --instance-id "$INSTANCE_ID"

          status=$(aws ssm get-command-invocation \
            --command-id "$command_id" \
            --instance-id "$INSTANCE_ID" \
            --query 'Status' \
            --output text)

          echo "SSM status: $status"
          test "$status" = "Success"

記事執筆時点の公式READMEでは aws-actions/configure-aws-credentials@v6.3.0 が例示されています。公開時点の最新版と、既存ワークフローで採用しているバージョンは [要確認] です。運用ではメジャーバージョン指定ではなく、commit SHAへ固定する方法も検討します。

EC2側のデプロイスクリプト

GitHub Actions内に長いシェルスクリプトを埋め込まず、EC2側のスクリプトを呼び出す形にしました。これにより、Run Commandへ渡す内容を短く保てます。

#!/usr/bin/env bash
set -Eeuo pipefail

readonly revision="${1:?revision is required}"
readonly app_dir="/var/www/example-app"

cd "$app_dir"

git fetch --prune origin
git checkout --detach "$revision"

composer install \
  --no-dev \
  --no-interaction \
  --prefer-dist \
  --optimize-autoloader

php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart

この例は構成を示すための最小版です。実運用では、現在のリリースを保持してsymlinkを切り替える方式、失敗時のロールバック、共有ディレクトリ、メンテナンスモード、フロントエンドビルドなどを環境に合わせて設計します。

特に php artisan migrate --force はアプリケーションのロールバックだけでは元に戻せない場合があります。DB変更を同じ自動処理に含めるか、個別承認にするかは別途判断が必要です。

動作確認

構成としては、GitHub Actions → OIDC/IAM → SSM → EC2 → CloudWatch Logsまで作成しました。公開前には、匿名化したサンプルと実環境の差を踏まえて次を再確認します。

  • [要確認] 対象のGitHub EnvironmentからだけIAMロールを引き受けられること
  • [要確認] 許可していないブランチまたはEnvironmentからの引き受けが拒否されること
  • [要確認] SSM AgentがOnlineで、対象EC2がマネージドノードとして表示されること
  • [要確認] 正常なrevisionでコマンドの最終ステータスが Success になること
  • [要確認] 存在しないrevisionを指定した場合にGitHub Actionsも失敗になること
  • [要確認] CloudWatch Logsにstdoutとstderrが保存され、機密値が出力されていないこと
  • [要確認] Laravelの疎通、キュー、DB migration、キャッシュ更新に問題がないこと
  • [要確認] ロールバック手順が実行できること

Run Commandの出力はCloudWatch Logs上で、概ね CommandID/InstanceID/PluginID/stdout と stderr のストリームに分かれます。標準エラーがない場合はstderrのストリームが作られない点にも注意します。

つまずきやすい点

OIDCでロールを引き受けられない

次を順番に確認します。

  1. ワークフローに id-token: write があるか
  2. OIDC ProviderのURLが https://token.actions.githubusercontent.com か
  3. Audienceが sts.amazonaws.com か
  4. 信頼ポリシーの sub が実際のリポジトリ、ブランチ、Environmentと一致しているか
  5. 新しいimmutable subject claim形式の対象になっていないか

TargetNotConnected になる

GitHub Actions側ではなく、EC2側のSystems Manager接続を確認します。

  • SSM Agentの稼働状態
  • インスタンスプロファイルの権限
  • EC2からSystems Manager関連エンドポイントへの経路
  • 対象リージョンとインスタンスIDの組み合わせ

SSMコマンドを送れたのにデプロイ失敗を見逃す

send-commandの終了コードだけでジョブを終えると、EC2上のコマンド完了前に成功扱いになることがあります。Command IDを保存し、waiterと最終ステータス確認を必ず入れます。

また、デプロイスクリプト側でも set -Eeuo pipefail を使い、途中の失敗を後続処理で上書きしないようにします。

CloudWatch Logsに出力されない

--cloud-watch-output-config の指定だけでなく、マネージドノード側がCloudWatch Logsへ書き込む権限を持っているかを確認します。カスタムIAMポリシーを使う場合は、ロググループ・ログストリームの作成とログイベント送信に必要な権限を追加します。

なお、コマンドやアプリケーションが認証情報、環境変数、実データを標準出力へ書けば、その内容もログへ保存されます。set -x の常用や .env の表示は避け、ロググループの保持期間と閲覧権限も設定します。

まとめ

GitHub ActionsからEC2へのデプロイをOIDCとSSM Run Commandで構成すると、次のように責務を分離できます。

  • GitHub ActionsはOIDCで短期認証情報を取得する
  • IAMロールは特定のリポジトリ・Environmentと必要なSSM操作だけを許可する
  • EC2への命令はSSM経由で送り、SSH鍵や踏み台への依存を減らす
  • デプロイの詳細はEC2側スクリプトにまとめる
  • Command IDで完了を待ち、CloudWatch Logsに実行記録を残す

「SSHを使わない」だけでなく、「どのGitHubコンテキストが、どのEC2に、どのコマンドを送れるか」をIAMで限定し、実行結果を追跡できるところまで含めて設計するのが重要でした。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?