どうしてもGitHub ActionsのEnvironmentを使いたいから、CodePipelineの承認を流用することにした


日記

ゆ「今日は新規開発の環境づくりか......CI/CDはActionsでいいしEnvironment使って承認プロセス追加して...」カタカタ

しばらく後

ゆ「このOrganization、Enterpriseじゃないじゃん!!!!!」

GitHub ActionsのEnvironmentとは

GitHub Actionsから呼び出すことで、devやprodなど各環境ごとの処理を実行することができます。パブリックリポジトリかEnterprise版でのみ利用可能です。
設定できる内容は下記の通りです。

  • protection rule
    • reviewer → 指定した人数の承認を貰うまではActionsをストップできる
    • timer → 指定した時間Actionsをストップする
    • branch → 指定したブランチでのみActionsを実行できる
  • secrets → 環境ごとのsecretを設定できる(リポジトリのsecretsとマージされる)

secretsの方は DEV-AWS-SECRET みたいに各環境ごとに設定すればなんとかできるのでいいとして、reviewerの方はないと困りました。

AWS CodePipelineの方にも承認プロセスはあるので、そちらをActionsから叩ければ万事OKなのでやってみます。

CodePipelineの構築

こちらによると、

  • パイプラインには少なくとも2つのステージを含める
  • パイプラインの最初のステージにはソースアクションが1つ以上ある
  • ソースアクションはパイプラインの最初にのみ含める

とあるので、ソース+承認(approval)のみのパイプラインを作ります。
なお、ソースとして1番手っ取り早いのはS3なので、S3を使います。

terraformだとこんな感じです

main.tf
resource "aws_codepipeline" "pipeline" {
  name = "approval-pipeline"
  role_arn = aws_iam_role.codepipeline_role.arn

  artifact_store {
    location = aws_s3_bucket.source.bucket
    type = "S3"
  }

  stage {
    name = "Start"

    action {
      name = "Source"
      category = "Source"
      owner = "AWS"
      provider = "S3"
      version = "1"
      output_artifacts = [ "source" ]

      configuration = {
        "S3Bucket" = aws_s3_bucket.source.bucket
        "S3ObjectKey" = "source.txt"
        "PollForSourceChanges" = false
      }
    }
  }

  stage {
    name = "Approval"

    action {
      name = "Approval"
      category = "Approval"
      owner = "AWS"
      provider = "Manual"
      version = "1"
    }
  }
}

ここで指定した configuration.S3ObjectKey のファイル名でS3に適当なファイルをアップロードしておいてください。

GitHub Actionsの構築

要はさっきのpipelineをGitHub Actionsから起動して、承認をもらうまで(=成功するまで)待てばいいわけです。
ワンライナーだときついと思うのでシェルスクリプトを書きます。

wait-approval.sh
#!/bin/bash

# 指定したcodepipelineを実行して、完了するまで待つ。

if [ ! $# -eq 1 ]; then
    echo '呼び出し失敗'
    exit 1
fi

# 引数取得
pipeline_name=$1

# pipelineを実行する
execution_id=$(aws codepipeline start-pipeline-execution --name "${pipeline_name}" \
            | jq -r '.pipelineExecutionId')

# 環境変数に登録(GitHub Actions)
echo "codepipeline_execution_id=${execution_id}" >> $GITHUB_ENV

echo "
次に進むためには下記のpipelineを承認してください。
pipeline name: ${pipeline_name}
execution id: ${execution_id}
"

status=""
# 空文字(最初) OR InProgress の時に繰り返す
while [ -z "$status" ] || [ "$status" = "InProgress" ]; do
    sleep 10
    # 現在の状況
    status=$(aws codepipeline get-pipeline-execution --pipeline-name "${pipeline_name}" --pipeline-execution-id "${execution_id}" \
            | jq -r '.pipelineExecution.status')

    now=$(date "+%Y-%m-%dT%H:%M:%S")

    echo "[${now}] 現在の状態: ${status} execution_id: ${execution_id}"
done

if [ "$status" = "Succeeded" ]; then
    echo "承認を確認しました"
    exit 0
elif [ "$status" = "Stopped" ]; then
    echo "停止されました"
    exit 1
elif [ "$status" = "Superseded" ]; then
    echo "後発のpipelineが優先されました"
    exit 1
elif [ "$status" = "Failed" ]; then
    echo "失敗・却下しました"
    exit 1
else
    echo "想定外のステータスです"
    exit 1
fi

あとはこれを呼び出せるようにworkflowを書くだけです。この時、上記スクリプトを呼ぶstepは絶対にtimeoutを設定してください。お金がまずいです。

github/workflows/approval.yml
name: approval
on:
  workflow_dispatch
jobs:
  approval:
    runs-on: ubuntu-latest
    steps:
      -
        uses: actions/checkout@v2
      -
        name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
      -
        name: wait approval
        timeout-minutes: 2
        run: ./bin/wait-codepipeline-approval.sh approval-pipeline

CodePipelineを正しく停止させる

ここまででやっても承認プロセスはうまく使えますが、CodePipelineは同じパイプラインを同時に動かすと1つのみキューに溜まり、2つ以上になった場合古いものは捨てられてしまいます。GitHub Actionsから投げっぱなしだと、timeoutした場合のパイプラインが留まってしまい、問題が発生します。
ので、timeoutで終了した場合にパイプラインを止めます。

cancel-codepipeline.sh
#!/bin/bash

if [ ! $# -eq 1 ]; then
    echo '呼び出し失敗'
    exit 1
fi

# 引数取得
pipeline_name=$1
execution_id=$codepipeline_execution_id

status=$(aws codepipeline get-pipeline-execution --pipeline-name "${pipeline_name}" --pipeline-execution-id "${execution_id}" \
            | jq -r '.pipelineExecution.status')

if [ "$status" = "InProgress" ]; then
    execution_id=$(aws codepipeline stop-pipeline-execution --pipeline-name "${pipeline_name}" --pipeline-execution-id "${execution_id}" \
                | jq -r '.pipelineExecutionId')
    echo "停止しました:${execution_id}"
fi

コード内の $codepipeline_execution_id はGitHub Actionsの環境変数を使って受け渡しています。
完成形がこれです。

github/workflows/approval.yml
name: approval
on:
  workflow_dispatch
jobs:
  approval:
    concurrency:
      group: approval
      cancel-in-progress: true
    runs-on: ubuntu-latest
    steps:
      -
        uses: actions/checkout@v2
      -
        name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
      -
        name: wait approval
        timeout-minutes: 2
        run: ./bin/wait-codepipeline-approval.sh approval-pipeline
      -
        name: stop pipeline
        if: failure()
        run: ./bin/cancel-codepipeline.sh approval-pipeline

stepに設定したtimeoutによって終了した場合も、失敗としてマークされるので、以後の if: failure() でキャッチできます。
GitHub ActionsからAWSにアクセスする部分などは他の人の方が詳しく書いてあると思うのでそちらを見てください。
これとか ↓

実行時の画面

これだけ見てもしょうがないと思いますが、一応載せておきます。
承認した場合

却下した場合

TimeOutした場合