👼

EC2上で動かしてる神をGitHub Actionsから操作する

2024/09/12に公開

神の前史

リブセンスのインフラグループにはインフラ神様(以後神と呼びます)と呼ばれる存在がいます。
実態は人工無能のbotでたまに会話できたり大体できなかったりします。他色々な機能を持っており、例えばAWS上に存在するEC2を教えてくれたり、便利な存在です。
彼の中身はGitHubで管理されており、EC2上でサービスとして動いています。コードの更新が発生した場合、SSMでログインして以下のオペレーションする必要がありました。

cd /opt/god-place # 神の場所に移動
git pull
systemctl restart infra-god.service

神のデプロイ自動化

以前GitHubのissueにAmazon Inspectorから取得したEC2の脆弱性情報を転記し、そこにトリガーワードをコメントすることで対象のEC2に存在する脆弱性をアップデートする記事を書きました。

https://made.livesense.co.jp/entry/2024/02/05/080000

ふと、これ流用すれば上記のオペレーションを自動化できることに気がつきました。ということで作りました。
※本来ならCodeDeployのようなものを使うのが一般的かと思いますが、あまり真面目なサービスでもないので真剣にCIを作らず、過去の実装を流用しました。

IAMロールの準備

特定のリポジトリからOIDCでAWSのリソースを操作できるように設定しておきます。

GitHub Actionsのワークフロー

発火条件等

マージしたら発火する他、開発中の機能についてはリモートブランチを指定してworkflow_dispatchにて適用することを想定しています。これによりこっそりテストできます。

name: god deploy

on:
  workflow_dispatch:
  pull_request:
    branches:
      - main
    types:
      - closed

jobs:
  deploy:
    name: deploy
    # PRマージのトリガーだけだと気軽に試せないからworkflow_dispatchもサポート
    if: >
      (github.event_name == 'workflow_dispatch') ||
      (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      id-token: write
      contents: read
      issues: write
      actions: write
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

各ステップ解説

OIDCでインスタンスの操作の権限を取得

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${account_id}:role/${role_name} # 各自で置き換えてください
          aws-region: ap-northeast-1

インスタンスの存在確認

存在しない場合は失敗させてPRのコメントに追記します。


      - name: Get Instance ID
        id: get_instance_id
        run: |
          instance_id=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=dev-infra-bots" --query "Reservations[].Instances[].InstanceId" --output text)
          echo "instance_id=${instance_id}" | tee -a "$GITHUB_OUTPUT"

      # インスタンス存在確認
      - name: Check instance
        id: check_instance
        run: |
          instance_id="${{ steps.get_instance_id.outputs.instance_id }}"
          aws ec2 describe-instances --instance-ids "$instance_id" --output json
          if [ $? -ne 0 ]; then
            exit 1
          fi

      # 存在しない場合はコメントで追記
      - name: Comment if instance not found
        if: steps.check_instance.outcome == 'failure'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `Instance not found. Exiting.`
            })

結果の貼り付け

コマンドを発行し、結果を取得する。PRマージがトリガーだった場合、PRのコメントに追記する。

      - name: send command
        id: send_command
        run: |
          command_id=$(aws ssm send-command \
            --document-name "AWS-RunShellScript" \
            --instance-ids "${{ steps.get_instance_id.outputs.instance_id }}" \
            --parameters commands="systemctl status infra-slackbot.service && \
                                   cd /opt/infra-slackbot && \
                                   git stash save && \
                                   git pull && \
                                   systemctl restart infra-slackbot.service && \
                                   systemctl status infra-slackbot.service" \
            --output json|jq -r '.Command.CommandId') &&
          echo "command_id=${command_id}" | tee -a "$GITHUB_OUTPUT"

      - name: get command status
        id: get_command_status
        run: |
          command_id="${{ steps.send_command.outputs.command_id }}"
          status=""

          while [[ "$status" != "Success" && "$status" != "Failed" ]]
          do
              output=$(aws ssm list-command-invocations --command-id "$command_id" --details --output json)
              status=$(echo $output | jq -r ".CommandInvocations[0].Status")
              sleep 10 # sleep for 10 seconds
          done

          echo "Final command status: $status"
          echo "Full command output: $output" #debug

          if [[ "$status" == "Failed" ]] ; then
            echo "Command failed. Exiting."
            exit 1
          fi

          # 複数行の出力を取得するためにEOFを使う
          # https://qiita.com/Ets/items/f93a48d40c81ea1e0c99
          EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
          echo "report<<$EOF" | tee -a "$GITHUB_OUTPUT"
          jq -r ".CommandInvocations[0].CommandPlugins[0].Output" <<< "$output" | tee -a "$GITHUB_OUTPUT"
          echo "$EOF" | tee -a "$GITHUB_OUTPUT"

      - name: Post result to issue
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            const output = `${{steps.get_command_status.outputs.report}}`;
            github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: `Here is the result of the command execution:\n\`\`\`${output}\`\`\``
              });

      - name: Comment Result
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        env:
          GHA_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
          AWS_URL: https://ap-northeast-1.console.aws.amazon.com/systems-manager/run-command/${{ steps.send_command.outputs.command_id }}/${{ steps.get_instance_id.outputs.instance_id }}?region=ap-northeast-1
        with:
          script: |
            const output = `## ${ context.workflow }
            #### Command Result 📖\`${{ steps.get_command_status.outcome }}\`
            *↑successじゃなかったら何かがおかしいよ↑、GHA Result Detailsで確認してね*

            *GHA Result Details: [${ process.env.GHA_URL }](${ process.env.GHA_URL })*;
            *AWS Result Details: [${ process.env.AWS_URL }](${ process.env.AWS_URL })*`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

神の自動化による成果

インフラグループにデプロイをお願いしなくて良くなったためメソッドたくさん生えました!おもしろ機能の追加によりコミュニケーションの活性化につながります!

宣伝

ということでCI/CDの勉強会を開くのでご興味のある方はぜひ!
https://livesense.connpass.com/event/328856/

Livesense Engineers

Discussion