面倒くさすぎるAPI Gateway SDKの更新作業を自動化した話



# はじめに
突然ですが、AWSのAPI Gatewayのクライアント用SDKって使ってますか？
そんでもってどのように取り込んでますか？

自分の場合だとこれまで、

1. AWSコンソールにログインしてAPIGatewayのAPIの中から目的のAPIを探す
    ![1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/49969a9b-c7c8-0111-ba66-fb70083f24f1.png)


2. ステージから目的のステージに移動する
    ![1-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/ce246d8c-e690-6c99-d360-f626b1ef36da.png)


3. 「SDKの生成」タブから対象のプラットフォームを選択して...
    ![1-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/c38673a6-2738-5f0c-db04-7c856e82c8eb.png)


4. zipファイルのダウンロードをして、入れ替えたいgitリポジトリ内のapigClient.jsを入れ替えて。。。
    ![1-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/eb4fe4af-b01b-91b8-6d84-d7792db2873c.png)


と、手動でSDKの入れ替え作業をしてました。

**面倒くさすぎる。。。！**

「このまま人力で更新するオペレーションを続けているといつかヒューマンエラーが発生するな・・・」と思いつつも時は過ぎ、
こないだフロントエンド-バックエンド間の疎通がうまく行かない事象が発生し、SDKの更新忘れが原因という事が分かり、ついに恐れていたことが起きてしまいました。
そして良い機会だと思ってSDKの自動更新のパイプラインを構築するために本腰を入れたのでした。

# 構築するパイプラインの仕様

- 1時間に1回スケジュール起動する
- AWSからSDKをダウンロードする
- 既存のapigClient.jsと、DLしたapigClient.jsを比較する
- apigClient.jsに差分があった場合、SDK更新用のブランチを作成する
- SDK更新用のブランチが既に存在していた場合、ブランチ作成はせずにファイル更新をする
- 既にSDK更新用のPullRequestが作成されているかどうかをチェックする
- SDK更新用PullRequestが存在しない場合、PullRequestを作成する
- PullRequestが作成されたことをSlackにチームメンション付きでRev依頼投稿する
- PR発行イメージ
    ![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/2efd1a8a-8017-3e82-9761-fec0aceb1d23.png)
- Slack通知イメージ
    ![2-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/86a70d0d-5360-fa2c-5771-4edfeac6946f.png)

# パイプライン全体

```yaml

name: update api gateway sdk

on:
  schedule:
    # 1時間ごと定期実行
    - cron: '0 * * * *'

  workflow_dispatch:

env:
  SYNC_BRANCH: hotfix/apigateway_sdk_update
  GH_TOKEN: ${{ github.token }}
  SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

permissions:
  id-token: write
  contents: write
  pull-requests: write

jobs:
  build:
    name: get sdk 
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure AWS credentials from account
        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: 加工場所を作る
        run: |
          mkdir ./tmp

      - name: API GatewayからSDKを取得
        id: get_sdk 
        run: |
          aws apigateway get-sdk --rest-api-id [YOUR REST ID] --stage-name [STAGE NAME] --sdk-type javascript ./tmp/apiGateway-js-sdk.zip

      - name: zip展開
        run: |
          unzip ./tmp/apiGateway-js-sdk.zip -d ./tmp

      # apigClient内のコードを調整する必要があればここでsedコマンドなどを使って書き換えていく
      - name: apigClient.jsの加工
        run: |
          echo "if Necessary, you can modify apigClient.js on this"

      - name: apigClient.jsのchecksumを取得
        run: |
          echo NEW_SDK_CHECKSUM="$(shasum ./tmp/apiGateway-js-sdk/apigClient.js | awk '{ print $1 }')" >> "$GITHUB_ENV"
          echo OLD_SDK_CHECKSUM="$(shasum ./[EXISTING SDK FILE PATH]/apigClient.js | awk '{ print $1 }')" >> "$GITHUB_ENV"
          

      # apigClient.jsだけでなく、同じくDLされるlibディレクトリもまとめてコピーしている
      - name: ファイル上書き
        if: env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM
        run: |
          \cp -f ./tmp/apiGateway-js-sdk/apigClient.js ./[EXISTING SDK FILE PATH]/
          \cp -rf ./tmp/apiGateway-js-sdk/lib ./[EXISTING SDK FILE PATH]/

      - name: ブランチ作成/更新
        id: create-sync-branch
        if: env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM
        run: |
          git config user.email "actions@github.com"
          git config user.name "Github Actions"
          branch=${{ env.SYNC_BRANCH }}
          git switch -C $branch
          git add .
          git commit -m "update AWS API Gateway SDK from Github actions"
          git push -f origin $branch

      - name: PR作成済みチェック
        id: check_pr
        if: env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM
        run: |
          pr_title='API Gateway SDK new Update🚀'
          base_branch='master'
          echo "::set-output name=count::$(gh pr list -S "${pr_title}"' in:title' -B $base_branch | wc -l)"
          echo "::set-output name=pr_title::$pr_title"
          echo "::set-output name=base_branch::$base_branch"

      - name: PR作成
        if: ${{ (env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM) && (steps.check_pr.outputs.count == 0) }}
        env:
          TZ: 'Asia/Tokyo' # タイムゾーン指定
        run: |
          CURRENT_DATETIME=$(date +'%Y-%m-%d %H:%M:%S')
          gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t "${{ steps.check_pr.outputs.pr_title }}" -b "🕰️make PR date: $CURRENT_DATETIME" 
          # レビューアーをアサインしたい場合はオプション追加:  -r [REVIEWER NAME ex: hoge,puga]
          # 本当はチーム指定したいが、現状のgithub actions上だとうまく行かない -r [org]/[team]

      # PR作成時はこちらのステップが実行される
      - name: Prepare Slack Message
        if: ${{ (env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM) && (steps.check_pr.outputs.count == 0) }}
        id: make-pr-slack-message-creator
        run: |
          SLACK_MESSAGE="
          <!subteam^[YOUR SLACK GROUP ID]>
          AWS API Gateway SDKの更新が確認されたため、
          PRを作成しました。
          Revをお願いします。"

          echo "::set-output name=slack-message::${SLACK_MESSAGE//$'\n'/'%0A'}"

      # PR作成時はこちらのステップが実行される
      - name: Slack Notification on Failure
        uses: rtCamp/action-slack-notify@v2.0.2
        if: ${{ (env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM) && (steps.check_pr.outputs.count == 0) }}
        env:
          SLACK_USERNAME: SDK更新通知
          SLACK_CHANNEL: [YOUR SLACK CHANNEL]
          SLACK_TITLE: AWS API Gateway SDKが更新されたためPRを作成
          SLACK_COLOR: good
          SLACK_LINK_NAMES: true
          SLACK_MESSAGE: '${{ steps.make-pr-slack-message-creator.outputs.slack-message }}'

      # 置き換えた時の資材を証跡として取っておく
      - uses: actions/upload-artifact@v2
        with:
          name: target_SDK.zip
          path: ./tmp/apiGateway-js-sdk/
```

また、`.gitignore`に以下を追加している
```
/tmp/
```

## githubに登録したsecret
|key|param|
|-|-|
|SLACK_WEBHOOK_URL| Slackのwebhook URL|
|AWS_ACCESS_KEY_ID| awsへアクセスするuser/roleのid|
|AWS_SECRET_ACCESS_KEY| awsへアクセスするuser/roleのkey|

# 詳細（pointだけ）

## AWSへ接続するための設定

- aws cliコマンドを使うために設定する
```yaml
  - name: Configure AWS credentials from account
    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
```

## API GatewayからSDKを取得

- キモの部分。`aws apigateway get-sdk`コマンドを使ってSDKを作成した一時ディレクトリにDLしている
- コマンドの詳細は[こちら(公式doc)](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-generate-sdk-cli.html)
- `[YOUR REST ID]`は下の画像の`xxxxxx`の部分
    ![1-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/2731ec6c-8282-ccaf-74fd-d6d8ca8a6edc.png)


```yaml
  - name: API GatewayからSDKを取得
    id: get_sdk 
    run: |
      aws apigateway get-sdk --rest-api-id [YOUR REST ID] --stage-name [STAGE NAME] --sdk-type javascript ./tmp/apiGateway-js-sdk.zip
```

## apigClient.jsを加工

- sampleとしてechoしか動かしてないけど、ここ大事だったりする
- apigClient.jsはステージを指定してDLしているため、中のロジックや定義も当たり前だがそのステージに合わせたものとなっている
- Reactのdevelopment,productionsなどenvによってAPIの接続先を切り替えたい場合などはそのままのapigClient.jsだと特定のステージにしか接続しにいかない
- よってsedコマンドなどを使って、apigiClient.jsをenvに対応するように修正が必要なケースがある

```yaml
  # apigClient内のコードを調整する必要があればここでsedコマンドなどを使って書き換えていく
  - name: apigClient.jsの加工
    run: |
      echo "if Necessary, you can modify apigClient.js on this"
```

## 既存ファイルとの比較

- apigClient.jsに対しての調整が終わった後、そのファイルと既存のファイルで比較を行う
- 比較には`shasum`コマンドを使ってチェックサムレベルでの比較を行う
- 後続のstepでのifで用いるため、ここではチェックサムを取るだけ

```yaml
  - name: apigClient.jsのchecksumを取得
    run: |
      echo NEW_SDK_CHECKSUM="$(shasum ./tmp/apiGateway-js-sdk/apigClient.js | awk '{ print $1 }')" >> "$GITHUB_ENV"
      echo OLD_SDK_CHECKSUM="$(shasum ./[EXISTING SDK FILE PATH]/apigClient.js | awk '{ print $1 }')" >> "$GITHUB_ENV"
```

## ファイルの上書き

- 既存ファイルと差分が出ていれば実行する
- apigClient.js以外に、一緒にDLされるlibディレクトリ配下のライブラリについてもまるごと換装している

```yaml
  # apigClient.jsだけでなく、同じくDLされるlibディレクトリもまとめてコピーしている
  - name: ファイル上書き
    if: env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM
    run: |
      \cp -f ./tmp/apiGateway-js-sdk/apigClient.js ./[EXISTING SDK FILE PATH]/
      \cp -rf ./tmp/apiGateway-js-sdk/lib ./[EXISTING SDK FILE PATH]/
```

## ブランチの作成・更新

- 既存ファイルと差分が出ていれば実行する
- `git switch -C`[コマンド](https://git-scm.com/docs/git-switch#Documentation/git-switch.txt--Cltnew-branchgt)で既にブランチがあろうと無かろうと強制的にブランチを作成する
- `git push -f origin $branch`コマンドで強制的に修正をブランチにpush
- `-f`オプションが無いと「リモートリポジトリから先にpullして同期取ってくれ」って怒られる
- SDK更新専用branchだし、毎回masterをcheckoutしてそこからbranch強制作成しているので問題ないと判断して `-f`オプションを許容する


```yaml
  - name: ブランチ作成/更新
    id: create-sync-branch
    if: env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM
    run: |
      git config user.email "actions@github.com"
      git config user.name "Github Actions"
      branch=${{ env.SYNC_BRANCH }}
      git switch -C $branch
      git add .
      git commit -m "update AWS API Gateway SDK from Github actions"
      git push -f origin $branch
```

## PullRequestが発行済みかチェック

- 既存ファイルと差分が出ていれば実行する
- `gh pr list`コマンドでmasterブランチに対して特定の文字列を含むタイトル名でPRが発行されているかを確認する
- 結果が1以上であれば発行済みと判断する

```yaml
  - name: PR作成済みチェック
    id: check_pr
    if: env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM
    run: |
      pr_title='API Gateway SDK new Update🚀'
      base_branch='master'
      echo "::set-output name=count::$(gh pr list -S "${pr_title}"' in:title' -B $base_branch | wc -l)"
      echo "::set-output name=pr_title::$pr_title"
      echo "::set-output name=base_branch::$base_branch"
```

## PullRequest作成

- 既存ファイルと差分が出ている かつ PRが発行されてなければ実行する
- `gh pr create`コマンドを使ってPRを発行する
- [公式](https://cli.github.com/manual/gh_pr_create)によるとteam指定でreviewerをアサインできるはずなのだが、2023/08/10現在時点でgithub actionsでは指定するとエラーになる
- [issue発行済み](https://github.com/cli/cli/issues/6395)。未だopenの状態
- 個人名をreviewerとして指定するのは可能
- bodyに何も記載がないのも寂しいので、PR発行した時の日時を入れてみた

```yaml
  - name: PR作成
    if: ${{ (env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM) && (steps.check_pr.outputs.count == 0) }}
    env:
      TZ: 'Asia/Tokyo' # タイムゾーン指定
    run: |
      CURRENT_DATETIME=$(date +'%Y-%m-%d %H:%M:%S')
      gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t "${{ steps.check_pr.outputs.pr_title }}" -b "🕰️make PR date: $CURRENT_DATETIME" 
      # レビューアーをアサインしたい場合はオプション追加:  -r [REVIEWER NAME ex: hoge,puga]
      # 本当はチーム指定したいが、現状のgithub actions上だとうまく行かない -r [org]/[team]
```

- PR発行するとこんな感じになる
    ![2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/2efd1a8a-8017-3e82-9761-fec0aceb1d23.png)


- 参考:team reviewer指定してエラーになるログ
```
could not request reviewer: [org]/[team] not found
Error: Process completed with exit code 1.
```

## PR発行したことをSlackに通知

- `rtCamp/action-slack-notify`pluginを使っている
- 既存ファイルと差分が出ている かつ PRが発行されてなければ実行する
- メッセージ部分と投稿部分を分けることでメッセージが自由に書ける
- `<!subteam^[YOUR SLACK GROUP ID]>`でSlackのチームに向けてメンションを送っている(画像の例では`@front`)
- Group Idの取り方はweb版のSlackを開いて、ユーザグループを確認したときにURLも末尾に表示される英数字がそれ
    ![2-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/a340c5d1-04d7-8482-3273-4cf1efe00c43.png)


```yaml
  # PR作成時はこちらのステップが実行される
  - name: Prepare Slack Message
    if: ${{ (env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM) && (steps.check_pr.outputs.count == 0) }}
    id: make-pr-slack-message-creator
    run: |
      SLACK_MESSAGE="
      <!subteam^[YOUR SLACK GROUP ID]>
      AWS API Gateway SDKの更新が確認されたため、
      PRを作成しました。
      Revをお願いします。"

      echo "::set-output name=slack-message::${SLACK_MESSAGE//$'\n'/'%0A'}"

  # PR作成時はこちらのステップが実行される
  - name: Slack Notification on Failure
    uses: rtCamp/action-slack-notify@v2.0.2
    if: ${{ (env.NEW_SDK_CHECKSUM != env.OLD_SDK_CHECKSUM) && (steps.check_pr.outputs.count == 0) }}
    env:
      SLACK_USERNAME: SDK更新通知
      SLACK_CHANNEL: [YOUR SLACK CHANNEL]
      SLACK_TITLE: AWS API Gateway SDKが更新されたためPRを作成
      SLACK_COLOR: good
      SLACK_LINK_NAMES: true
      SLACK_MESSAGE: '${{ steps.make-pr-slack-message-creator.outputs.slack-message }}'
```

- こんな感じになる
    ![2-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/86a70d0d-5360-fa2c-5771-4edfeac6946f.png)


# まとめ

定期的にSDKの更新を見に行って、更新用のPullRequestを自動で作成してくれるようになった。
必要なオペレーションはPRのマージボタンを押すだけに。
お茶を飲む時間が増えてめでたしめでたし。

`set-output`使っているところは後々` >> "$GITHUB_ENV"`に直そうかと思う。


# 参考

- [AWS CLI を使用して Ruby SDK を生成してダウンロードする](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-generate-sdk-cli.html)
- [【GitHub Actions】ブランチ間に差分が有った時に同期pull requestを発行する（baseブランチは新しく作成する）](https://qiita.com/wrongwrong/items/69a8200c9062a999599c)
- [GitHub Actionsでブランチに合わせてリリース用PRを自動生成する](https://zenn.dev/kshida/articles/auto-generate-release-pr-with-github-actions#%E6%97%A2%E3%81%AB%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E7%94%A8pr%E3%81%8C%E5%AD%98%E5%9C%A8%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AF%E3%82%B9%E3%82%AD%E3%83%83%E3%83%97%E3%81%99%E3%82%8B)
- [Slack通知のメンション先を指定する](https://help.fondesk.jp/ja/articles/6151270-slack%E9%80%9A%E7%9F%A5%E3%81%AE%E3%83%A1%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%B3%E5%85%88%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B)
- [Unable to set team as reviewer](https://github.com/cli/cli/issues/6395)