GitHub Actions - environment と Deployment protection rules

# environment

以下のようにjobを実行する環境を指定する  

> [!IMPORTANT]
> 環境上の variables、secrets を使用する場合のみ environment を指定する

```yaml
jobs:
  deployment:
    runs-on: ubuntu-latest
    environment: dev  # 環境指定
    env:
      account: ${{ vars.ACCOUNT_ID }}  # Environment variablesの使用
      role_name: ${{ secrets.ACTIONS_ASSUME_ROLE_NAME }}  # Environment secretsの使用
    steps:
      - name: deploy
```

environmentは、GitHub上に作成し、environmentごとにsecretsとvariablesを登録できる

![2024-07-15-15-55-59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/02d3c641-87b7-8978-5de7-b62b1fc28b77.png)

![2024-07-15-16-12-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/30b4d642-9d1e-7c7a-0d3a-ea96e9168ae2.png)

# 用途

よく使われるパターンとして、  
以下のようにpull request のマージ先によって環境を切り替える

```yaml:on_pr.yml
on:
  pull_request:

jobs:
  develop:
    name: Deploy Develop
    if: ${{ github.base_ref == 'develop' }}  # pull requestのマージ先で条件分岐
    uses: ./.github/workflows/reuse_deploy_develop.yml
    secrets: inherit
    with:
      environment_name: dev  # 環境指定
```

```yaml:workflows/reuse_deploy_develop.yml
on:
  workflow_call:
    inputs:
      environment_name:  # 使用する環境名を引数でもらう
        required: true
        type: string
jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment_name }}  # 環境指定
    env:
      account: ${{ vars.ACCOUNT_ID }}  # Environment variablesの使用
      role_name: ${{ secrets.ACTIONS_ASSUME_ROLE_NAME }}  # Environment secretsの使用
    steps:
      - name: deploy
```

# Deployment protection rules

[Deployment protection rules](https://docs.github.com/ja/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules) を環境に対して設定しておくことで、  
環境を使用するjob実行時に指定したレビュアーの承認が必要になり、承認されるまでWaiting状態となる

![2024-07-15-16-45-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/55d1b943-a3e3-a055-3f20-0bd5f742686e.png)

# environment指定の集約

environmentはjobに対して指定するが、  
可読性向上目的や `runs-on` の使い分け、[matrix](https://docs.github.com/ja/actions/using-jobs/using-a-matrix-for-your-jobs)を使う場合等、  
どうしてもworkflowが複数job構成となることが多い  

Deployment protection rulesを設定したenvironmentを以下のように3つのjobで指定すると、  
各jobごとに承認待ちとなり、計3回の承認操作が必要になる

```yaml
jobs:
  job-1:
    runs-on: ubuntu-latest
    environment: dev
    env:
      account: ${{ vars.ACCOUNT_ID }}
    steps:
      - name: step-1

  job-2:
    runs-on: ubuntu-latest
    environment: dev
    env:
      account: ${{ vars.ACCOUNT_ID }}
    steps:
      - name: step-1

  job-3:
    runs-on: ubuntu-latest
    environment: dev
    env:
      account: ${{ vars.ACCOUNT_ID }}
    steps:
      - name: step-1
```

## variables

承認操作を何回も行うのは面倒なので、  
以下のようにenvironmentを指定したjobのoutputsを使用することで、  
以降のjobでは承認操作が不要になる

```yaml
jobs:
  output_environment:
    name: Output Environment
    runs-on: ubuntu-latest
    environment: dev  # 環境指定
    outputs:
      accountId: ${{ vars.ACCOUNT_ID }}  # Environment variablesを出力する
    steps:
      - name: Checkout
        uses: actions/checkout@v4

  job-1:  # environmentは指定しない
    name: Job 1
    needs: [output_environment]
    runs-on: ubuntu-latest
    env:
      account: ${{ needs.output_environment.outputs.accountId }}   # Environment variablesの代わりにjobのoutputsを使用する
    steps:
      - name: step-1
```

## secrets

secretsをoutputするとマスクされた文字列`***`が出力され後続jobで使用できない  
これを解決するためにsecretsは暗号化してoutput、後続jobで復号化する  
暗号化にはbase64とGPGを組み合わせたものを使用（base64だけでは上手くいかず）  

ただしsecretsを暗号化/復号化を行うと全く別文字列扱いとなり、  
ログ等にマスクされていないsecretsが出力されてしまう  
そのため、復号化時に[`::add-mask::`](https://docs.github.com/ja/actions/using-workflows/workflow-commands-for-github-actions#masking-a-value-in-a-log)を使ってマスクする  

以下、暗号化/復号化の実装例

```yaml:.github/actions/encrypt-secret/action.yml
name: Encrypt secret

inputs:
  secret:  # 暗号化したいsecrets
    required: true
    type: string

outputs:
  enc_secret:  # 暗号化したsecrets
    value: ${{ steps.encrypt_secret.outputs.enc_secret }}

runs:
  using: 'composite'
  steps:
    - name: encrypt secret
      id: encrypt_secret
      shell: bash
      run: |
        enc_secret=$(gpg --symmetric --batch --passphrase "SECRET" --output - <(echo "${{ inputs.secret }}") | base64 -w0)
        echo "enc_secret=${enc_secret}" >> $GITHUB_OUTPUT
```

```yaml:.github/actions/decrypt-secret/action.yml
name: Decrypt secret

inputs:
  enc_secret:  # 暗号化されたsecrets
    required: true
    type: string

outputs:
  dec_secret:  # 復号化したsecrets
    value: ${{ steps.decrypt_secret.outputs.dec_secret }}

runs:
  using: 'composite'
  steps:
    - name: decrypt secret
      id: decrypt_secret
      shell: bash
      run: |
        dec_secret=$(gpg --decrypt --quiet --batch --passphrase "SECRET" --output - <(echo "${{ inputs.enc_secret }}" | base64 --decode))
        echo "::add-mask::$dec_secret"
        echo "dec_secret=${dec_secret}" >> $GITHUB_OUTPUT
```

以下secretsを出力した例

```yaml
jobs:
  output_environment:
    name: Output Environment
    runs-on: ubuntu-latest
    environment: dev  # 環境指定
    outputs:
      accountId: ${{ vars.ACCOUNT_ID }}
      enc_roleName: ${{ steps.encrypt_roleName.outputs.enc_secret }} # 暗号化したsecretsを出力
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: encrypt roleName
        id: encrypt_roleName
        uses: ./.github/actions/encrypt-secret
        with:
          secret: ${{ secrets.ACTIONS_ASSUME_ROLE_NAME }}

  job-1:  # environmentは指定しない
    name: Job 1
    needs: [output_environment]
    runs-on: ubuntu-latest
    env:
      account: ${{ needs.output_environment.outputs.accountId }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: decrypt roleName
        id: decrypt_roleName
        uses: ./.github/actions/decrypt-secret
        with:
          enc_secret: ${{ needs.output_environment.outputs.enc_roleName }}
```

# 参考

- [GitHub Actionsで "このアウトプットはシークレット含んでそうだから出力をスキップするよ"と言われてしまった時の対処法](https://zenn.dev/cactus_cat/articles/577a65d66f3157)
- [How to Pass Secrets Between Runners in GitHub Actions](https://nitratine.net/blog/post/how-to-pass-secrets-between-runners-in-github-actions/#the-solution)