GitHub Actions+Cfnでスイッチロール用ポリシーをサクッと作れるようにした話

この記事は <a href="https://qiita.com/advent-calendar/2023/mediba">mediba Advent Calendar 2023</a> 12日目の記事です。

## 20231215追記
re:iventで、gitから直接デプロイができるアップデートが発表されたようです。
こちらの方が全然サクッと作成できますね。
https://dev.classmethod.jp/articles/cloudformation-git-sync-update/
記事の方にもありますが、CI/CDをしっかり作成する場合はまだGitHubActionsもアリなのかもしれません。
今のところ承認機能の有無がこの記事内容との違いになりますが、もう少ししたらそれもアップデートで追加されそうですね。


## はじめに
株式会社medibaでインフラエンジニアをしております、馬淵と申します。

新規案件立ち上げ時に最初に作るIAM Role、Policy等の、ある程度形が決まっているが、ぽちぽち手作業が必要な作業って~~面倒臭い~~大変ですよね。

今回は、そういったリソースをパッと作るような仕組みを作ってみました。

例として、この仕組みで承認フローを挟んだ踏み台側のスイッチロールのポリシーを作ってみたので、それの紹介をしていきたいと思います。
同じようなものを作る方の参考になればと思います。

## 前提条件
GitHubActionsを使った事がある
対象環境にGitHubActions用のRoleが用意されている

## 構成図
![スクリーンショット 2023-12-10 14.15.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960719/09bfece0-e590-9545-8aeb-94199bd33016.png)


## この仕組みの動作のイメージ
１.IAMポリシーのCfnテンプレートを作成する
２.環境毎のファイルを作成する
３.tag pushでGitHubActionsを発火
4.slackに承認通知が飛ぶ
5.責任者が承認
6.作成完了！

## 作成するまでの流れ
１.事前準備
２.階層を作成
３.GitHubActions用テンプレートファイルを作成
４.GitHubでEnvironmentを作成
５.Cfnを動かすシェルを作成
６.各環境用ファイルを作成
７.Cfn用テンプレートファイルを作成
８.完了！

### １.事前準備
最初に、次の準備をします。
・リポジトリの用意
・GitHubActions用Roleの用意
・slackのWebhook URLの用意

■リポジトリの用意
リポジトリを作成します。説明は省略させていただきます:bow:

■GitHubActions用Roleの用意
踏み台側AWSアカウントにGitHubActions用Roleを用意します。
参考は<a href="https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-aws">こちら</a>
このRoleの名前は後述のactions用のソースコードで使用します。
    
■slackのWebhook URLの用意
slackのWebhook URLを用意します。
参考は<a href="https://qiita.com/seratch/items/28d09eacada09134c96c">こちら</a>
こちらで用意したURLは、GitHub><b>[Setting]</b>><b>[Actions secrets and variables]</b>><b>[Repository secrets]</b>に保存しておいてください。
後述のactionsで使います。

### ２.階層を作成
このような階層でフォルダ/ファイルを作成します。
フォルダの名前はなんでもいいですが、以降のコードにパスがあるので、そちらと併せてください。
![スクリーンショット 2023-12-10 14.03.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960719/bec0d709-e39e-195d-b6be-e6e61f4d3299.png)
switch_from_accountというフォルダの下に、それぞれのプロダクトを配置し、さらにその下に各環境ファイルがあるような階層になっております。

### 3.GitHubActions用テンプレートファイルを作成
次に、GitHubActions用のソースコードを用意します。
今回は、<b>review-(env)-(projectname)-(date)-(num)</b>とい名前でgitでtagをpushすると発火するように作りました。
OIDC_AWS_ROLEは事前準備で用意した時のロールのarnに書き換えてください。
```
name: GitHub Actions 
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on:
  push:
    tags:
      - 'review-**-**'
# review-(env)-(projectname)-(date)-(num)
# ex: review-dev-projectA-20231212-01

env:
  OIDC_AWS_ROLE: arn:aws:iam::0123456789:role/GitHubActionsOidcRole
  AWS_DEFAULT_REGION: ap-northeast-1

jobs:
  check_pushed_tag_name:
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    outputs:
      deploy_target_env: ${{ steps.deploy_target.outputs.env }}
      deploy_project_type: ${{ steps.deploy_project.outputs.type }}
    steps:
      - name: Filter pushed tag name
        uses: actions-ecosystem/action-regex-match@v2
        id: regex_match
        with:
          text: ${{ github.event.ref }}
          regex: '^refs\/tags\/review\-(dev|stg|prd)\-([a-z]+)\-(2[0-9]{3}(0[1-9]|1[0-2])(0[1-9]|1[0-9]|2[0-9]|3[01]))_[0-9]?[1-9]+$'

      - name: No matched available tag
        if: steps.regex_match.outputs.match == ''
        run: |
          exit 1

      - name: Set up deploy targeted environment
        id: deploy_target
        run: |
          echo "env=${{ steps.regex_match.outputs.group1 }}" >> $GITHUB_OUTPUT

      - name: Set up deploy artifacts type
        id: deploy_project
        run: |
          result=`echo -n '${{ steps.regex_match.outputs.group2 }}' | tr -d '-'`
          if [ -z $result ]; then
            result="all"
          fi
          echo "type=${result}" >> $GITHUB_OUTPUT

      - name: Notify pushed tag details
        run: |
          echo 'Target environment: ${{ steps.deploy_target.outputs.env }}'
          echo 'Artifacts type: ${{ steps.deploy_project.outputs.type }}'

  notice_deploy_target:
    needs:
      - check_pushed_tag_name
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    steps:
      - name: Send GitHub Action trigger data to Slack workflow
        id: slack
        uses: slackapi/slack-github-action@v1.16.0
        with:
          payload: |
            {
              "text": ":github: ビルド結果: ${{ job.status }}\n\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "switch roleの設定が申請されました。以下URLから承認してください\n"
                  }
                },
                {
                  "type": "context",
                  "elements": [
                    {
                      "type": "mrkdwn",
                      "text": "承認: <https://github.com/org/repository-url/actions/runs/${{ github.run_id }}|こちら>"
                    }
                  ]
                }
              ]
            }
        env:
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_INCOMING_WEBHOOK_URL }}

  approval:
    needs:
      - notice_deploy_target
    environment:
      name: approval_proper
    runs-on: ubuntu-latest
    steps:
      - name: approve deployment
        run: echo 'デプロイ確認 承認完了'

  deploy_cfn:
    needs:
      - check_pushed_tag_name
      - approval
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-session-name: github-actions-session
          aws-region: ${{ env.AWS_DEFAULT_REGION }}
          role-to-assume: ${{ env.OIDC_AWS_ROLE }}

      - run: ./cloudformation/switch_from_account/${{ needs.check_pushed_tag_name.outputs.deploy_project_type }}/deploy.sh -t "swith_from_policy" -e "cloudformation/switch_from_account/${{ needs.check_pushed_tag_name.outputs.deploy_project_type }}/env/${{ needs.check_pushed_tag_name.outputs.deploy_target_env }}/.env"

```
### 4.GitHubでEnvironmentを作成
GitHubの<b>[Setting]</b>><b>[Environment]</b>から、approval_properという名前で承認ルールを作成します。
上記のActionsを実行する際は、ここで設定したメールアドレスの人間に承認させるようにします。
![スクリーンショット 2023-12-10 15.38.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960719/8932b055-bb88-dd5c-4294-ec2f64c11b37.png)

### ５.Cfnを動かすシェルを作成
Actionsの最後に動かすシェルを作ります。
ここまでが共通部分となります。
これは特にいじるところはないと思います。
```
#!/usr/bin/env bash

set -euo pipefail
export LC_ALL=C

while getopts t:e: opt; do
   case $opt in
   "t") template_dir="${OPTARG}" ;;
   "e") target_env="${OPTARG}" ;;
   esac
done

script_dir=$(cd $(dirname $0); pwd)
source ${target_env}

# 各パラメーターの取得
stack_name=${account_alias_name}_$(cd ${script_dir}/${template_dir}; basename "$(pwd)")
echo "stack_name:"${stack_name}

# パラメーターチェック
if [ "${s3_cfn_bucketname}" = "" ];then
   echo "ERROR： Unable to get an 's3_cfn_bucketname'"
   exit 1
fi

if [ "${organization_name}" = "" ];then
   echo "ERROR： Enter organization_name in the .env file"
   exit 1
fi

# CloudFormation Validate
echo "Validate CloudFormation template..."
aws cloudformation validate-template \
   --template-body file://${script_dir}/${template_dir}/sam.yml

# CloudFormation Package
echo "Package CloudFormation template..."

mkdir -p ${script_dir}/${template_dir}/.packaged

aws cloudformation package \
   --template-file ${script_dir}/${template_dir}/sam.yml \
   --output-template-file ${script_dir}/${template_dir}/.packaged/cfn.yml \
   --s3-bucket "${s3_cfn_bucketname}" \
   --s3-prefix "${stack_name}"

# S3 upload
echo "Sync S3 CloudFormation templates..."

aws s3 sync ${script_dir}/${template_dir} \
   "s3://${s3_cfn_bucketname}/${stack_name}/"

echo "ok!"
```

### ６.各環境用ファイルを作成
ここからは環境別のファイルを作っていきます。
以下の項目を環境毎の情報に書き換えてください。
```
switch_from_account_no=0123456789
switch_to_account_no=9876543210
organization_name=mediba
s3_cfn_bucketname=cf-templates-ap-northeast-1
account_alias_name=projectAalias
env=dev
```

### ７.Cfn用テンプレートファイルを作成
最後に、作りたいAWSリソースのCfnテンプレートを用意します。
今回はスイッチロール用Policyなので、次のように、Admin、Developer、ReadOnlyという３つのポリシーについて書きました。
```
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  OrgName:
    Type: String

  SwitchFromAccountNo:
    Type: String

  SwitchToAccountNo:
    Type: String

  AccountAliasName:
    Type: String

  Env:
    Type: String

Resources:
  AdminPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub ${AccountAliasName}-${Env}-Admin
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action:
              - "sts:AssumeRole*"
            Resource:
              - !Sub "arn:aws:iam::${SwitchToAccountNo}:role/Admin"

  DeveloperPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub ${AccountAliasName}-${Env}-Developer
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action:
              - "sts:AssumeRole*"
            Resource:
              - "*"
              - !Sub "arn:aws:iam::${SwitchToAccountNo}:role/Developer"

  ReadOnlyPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub ${AccountAliasName}-${Env}-ReadOnly
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action:
              - "sts:AssumeRole*"
            Resource:
              - !Sub "arn:aws:iam::${SwitchToAccountNo}:role/ReadOnly"
```

### ８.完了！
これで完了です。
以下のようにtagをpushすれば動き、slackに通知が届くと思います。
```
$ git push origin review-dev-projectA-20231212-01 
```
![スクリーンショット 2023-12-10 16.40.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960719/f139a0fc-dd4e-e5bd-e0ca-fae7cbc6645b.png)


## 終わりに
以上になります。ここまで長い記事をご覧いただきありがとうございました。
日々の業務で触るうちに、Cfnの事が好きになってきました。
今回はスイッチロールでの例でしたが、当然、テンプレート次第で色々なパターンに対応できると思います。
どこかでトイル削減に役立てると幸いです。