カテゴリー
アーカイブ

01.21
2026

Github Actions の手動デプロイを設定してみた〜その2〜

  • LINE

はじめに

前回の投稿(Github Actions の手動デプロイを設定してみた)で記載したDEV環境用のデプロイyamlの設定を、本番用のものを用意する際に環境差異でハマりポイントがあったので投稿。

DEV環境と本番環境の違い

DEV環境ではデプロイユーザーの秘密鍵でsshログインを行っていたが、本番ではsshログイン後にデプロイユーザーに切り替える必要があった。
・DEV環境: sshユーザー・デプロイユーザー共に DEV_EC2_USER ユーザー
・本番環境: sshユーザーが PROD_EC2_USER、デプロイユーザーが www ユーザー

最初にやったこと

最初に以下の設定でデプロイを実行し、権限エラーでデプロイに失敗。

name: Deploy to Production

on:
  workflow_dispatch:
    inputs:
      run_migrate:
        description: 'migrate を実行する'
        type: boolean
        default: true

      run_pip_install:
        description: 'pip install を実行する'
        type: boolean
        default: true

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Set up SSH key
        env:
          EC2_KEY: ${{ secrets.PROD_EC2_KEY }}
        run: |
          mkdir -p ~/.ssh
          printf '%s\n' "$EC2_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -t rsa ${{ secrets.PROD_EC2_HOST }} >> ~/.ssh/known_hosts 2>&1 || true
          cat ~/.ssh/known_hosts

      - name: Deploy to EC2
        env:
          RUN_MIGRATE: ${{ github.event.inputs.run_migrate }}
          RUN_PIP_INSTALL: ${{ github.event.inputs.run_pip_install }}
        run: |
          echo "Deploying to ${{ secrets.PROD_EC2_HOST }}..."

          # 入力値をチェック
          RUN_MIGRATE=${{ inputs.run_migrate }}
          RUN_PIP_INSTALL=${{ inputs.run_pip_install }}

          # SSH接続テスト
          ssh -i ~/.ssh/id_rsa -o ConnectTimeout=10 ${{ secrets.PROD_EC2_USER }}@${{ secrets.PROD_EC2_HOST }} "echo 'SSH connection successful'" || exit 1

          # コードを転送(古いファイルを削除して更新)
          rsync -avz --exclude '.git' -e "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no" ./ ${{ secrets.PROD_EC2_USER }}@${{ secrets.PROD_EC2_HOST }}:${{ secrets.PROD_EC2_APP_DIR }}

          # EC2上でコマンドを実行
          ssh -i ~/.ssh/id_rsa ${{ secrets.PROD_EC2_USER }}@${{ secrets.PROD_EC2_HOST }} << "EOF"
            set -e
            sudo su www  # ← ここが追加のコマンド!
            cd ${{ secrets.PROD_EC2_APP_DIR }}

            echo "Activating virtualenv..."
            source ${{ secrets.PROD_EC2_ACTIVATE_PATH }}

            if [ "${RUN_PIP_INSTALL}" = "true" ]; then
              echo "Installing requirements..."
              pip install -r requirements.txt
            else
              echo "Skipped pip install..."
            fi

            if [ "${RUN_MIGRATE}" = "true" ]; then
              echo "Applying migrations..."
              python manage.py migrate --noinput --settings=${{ secrets.PROD_DJANGO_SETTINGS_MODULE }}
            else
              echo "Skipped migrations..."
            fi

            echo "Restarting Gunicorn..."
            sudo systemctl restart gunicorn

            echo "Restarting Nginx..."
            sudo systemctl restart nginx

            echo "Deployment completed!"
          EOF

      - name: Cleanup sensitive files
        if: always()
        run: |
          echo "Cleaning up sensitive files..."
          rm -f ~/.ssh/id_rsa
          rm -f ~/.ssh/known_hosts
          rm -f ~/.ssh/config
          echo "Cleanup completed!"

エラーの原因

DEV用のyamlとの違いは、50行目の以下のコードを追加し、sshログイン後のユーザーの切り替えを行おうとしていたが、この書き方だと後続の処理では www で実行されずに PROD_EC2_USER で実行されてしまうことが原因だった。

修正方法

後続の処理を www で実行させるには、heredoc を多重化させて www でコマンドを実行させる必要があった。
さらにデプロイ時のオプション設定(inputの内容)を引き継いで www ユーザーに実行させるには定数の再定義が必要だったため、その処理の追加も行った。

修正後のyaml

修正後の yaml に気を付けなければいけない内容を、該当箇所にコメントで記載しているので参考にしてもらえれば幸いです。

name: Deploy to Production

on:
  workflow_dispatch:
    inputs:
      run_migrate:
        description: 'migrate を実行する'
        type: boolean
        default: true

      run_pip_install:
        description: 'pip install を実行する'
        type: boolean
        default: true

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Set up SSH key
        env:
          EC2_KEY: ${{ secrets.PROD_EC2_KEY }}
        run: |
          mkdir -p ~/.ssh
          printf '%s\n' "$EC2_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -t rsa ${{ secrets.PROD_EC2_HOST }} >> ~/.ssh/known_hosts 2>&1 || true
          cat ~/.ssh/known_hosts

      - name: Deploy to EC2
        run: |
          echo "Deploying to ${{ secrets.PROD_EC2_HOST }}..."

          # 入力値をチェック
          RUN_MIGRATE=${{ inputs.run_migrate }}
          RUN_PIP_INSTALL=${{ inputs.run_pip_install }}

          # SSH接続テスト
          ssh -i ~/.ssh/id_rsa -o ConnectTimeout=10 ${{ secrets.PROD_EC2_USER }}@${{ secrets.PROD_EC2_HOST }} "echo 'SSH connection successful'" || exit 1

          # コードを転送
          rsync -avz --exclude '.git' --rsync-path="sudo -u www rsync" -e "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no"  ./ ${{ secrets.PROD_EC2_USER }}@${{ secrets.PROD_EC2_HOST }}:${{ secrets.PROD_EC2_APP_DIR }}

          # EC2上でコマンドを実行
          ssh -i ~/.ssh/id_rsa ${{ secrets.PROD_EC2_USER }}@${{ secrets.PROD_EC2_HOST }} << EOF
            set -e

            RUN_MIGRATE="${RUN_MIGRATE}"  # ← 入力値を引き継ぐために再度設定
            RUN_PIP_INSTALL="${RUN_PIP_INSTALL}"  # ← 入力値を引き継ぐために再度設定

            sudo -u www bash << 'APP_EOF'  # ← www ユーザーに切り替えて実行するコマンドを heredoc で定義
              set -e
              cd ${{ secrets.PROD_EC2_APP_DIR }}

              echo "Activating virtualenv..."
              source ${{ secrets.PROD_EC2_ACTIVATE_PATH }}

              echo "RUN_PIP_INSTALL: $RUN_PIP_INSTALL"
              echo "RUN_MIGRATE: $RUN_MIGRATE"

              if [ "$RUN_PIP_INSTALL" = "true" ]; then
                echo "Installing requirements..."
                pip install -r requirements.txt
              else
                echo "Skipped pip install..."
              fi

              if [ "$RUN_MIGRATE" = "true" ]; then
                echo "Applying migrations..."
                python manage.py migrate --noinput --settings=${{ secrets.PROD_DJANGO_SETTINGS_MODULE }}
              else
                echo "Skipped migrations..."
              fi
              python manage.py collectstatic --noinput --settings=${{ secrets.PROD_DJANGO_SETTINGS_MODULE }}
          APP_EOF  # ← 余計なインデントを入れるとスペースも文字列として捉えられエラーとなるため、EOF と同じインデントで記載
            echo "Restarting Gunicorn..."
            sudo systemctl restart gunicorn.service

            echo "Restarting Nginx..."
            sudo systemctl restart nginx

            echo "Deployment completed!"
          EOF

      - name: Cleanup sensitive files
        if: always()
        run: |
          echo "Cleaning up sensitive files..."
          rm -f ~/.ssh/id_rsa
          rm -f ~/.ssh/known_hosts
          rm -f ~/.ssh/config
          echo "Cleanup completed!"

最後に

今回の本番用 yaml の設定で、コマンドの影響範囲や変数のスコープをようやく理解することができた。
今後とも GitHub Actions を使って積極的に自動化を行っていき、新たな知見を得たら投稿していきます。

"全員が技術者" ベイストリームは横浜発のPythonのプロフェッショナル集団です。
積極採用中!尖ったPythonエンジニアへの第一歩はこちらから