ローカルPC＋GitHubActionsでAPIOpsを実装する

APIの開発を行う場合、APIの仕様書に従ってアプリケーションを実装し、それを更に開発者自身やテストチーム・QAチームがテストする。
![1710124851737.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/9f3f64ab-ea6a-ff31-6c37-b2aa2807885f.png)
APIの仕様が変更された場合やアプリケーションの実装を変更した場合にはテストを毎回行うが、これを手動でやるのは面倒なのでCI/CDパイプラインを実装して自動化してみようと思う。
なお、APIの開発・デプロイにGitOps・DevOpsの概念を適用したものをAPIOpsと呼ぶ模様。今回の実装はAPIOpsをどう小さく実現するかの検証にもなっている。

ここでは以下のような条件で自動化する。

- APIはアプリ側で管理せず、API Gatewayで管理する
- API Gatewayの設定も自動で変更する
- ラボは用意せず、自身のPCで完結させる（テストなどは自PC上で動くようにし、API Gatewayも自PC上に立てる）
- GitOps部分はGitHubを利用する

流れとしては以下のようにする。
![1710124336593.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/3f1cf5e7-3057-cba2-fa2c-e7301dc9ce79.png)

ユーザがPullRequest(PR)かPushをすると、GitHubがRunnerをキックして自PC内のツールを使ってテストや設定のデプロイなどを自動で行う。
なお、**Runnerが実行するコードをミスると自PCにも影響を及ぼす可能性がある**ため、この構成を推奨している訳ではない点は注意。

また、前提として以下で進める。
- APIの仕様は[OpenAPIのSpecification](https://spec.openapis.org/oas/latest.html)に準じてYAML形式で作成する
- APIの仕様は[Insomnia](https://github.com/Kong/insomnia)で作成する
- 対象とするInsomniaのプロジェクトは[こちら](https://qiita.com/ipppppei/items/8226ebe27ac658eb7e7c)の記事で作成したQiitaAPIとする
- API Gatewayには[Docker版Kong](https://github.com/Kong/kong)を使用する

今回、動作確認で使う自PCは以下のものを利用した。
- MacBook Pro M2 (32GBRAM)

メモリ使用量を見ている感じだと16GBでも行けそうではある。

手順としては以下の順番で進める。
1. Kong Gatewayの構築
1. GitOpsに使うリポジトリ・Runnerの用意
1. テストやデプロイに使うCLIの用意
1. CLIの動作確認
1. GitHubActionsのYAMLの作成と動作確認

## Kong Gatewayの構築
[GitHubのKongのGetting Started](https://github.com/Kong/kong?tab=readme-ov-file#getting-started)に従ってMac上に起動すると各コンテナは正常に起動するのだが、何故かDBlessモードと判断されて、後々以下のエラーが出てGateway Servicesの作成に失敗する。
```sh
Error: 1 errors occurred:
	while processing event: Create service Qiita_API failed: HTTP status 405 (message: "cannot create or update 'services' entities when not using a database")
```
なので、ここではGitHubで提供されているやり方ではなく、[KongのDocs](https://docs.konghq.com/gateway/latest/install/docker/)で提供されているやり方でKong Gatewayを起動する。
以下でスクリプトを生成する。
```sh
cat <<EOF > ./run.sh
#!/bin/bash
COMM=$1
if [ "$COMM" == "start" ]; then
	docker network create kong-net
	docker run -d --name kong-database \
	  --network=kong-net \
	  -p 5432:5432 \
	  -e "POSTGRES_USER=kong" \
	  -e "POSTGRES_DB=kong" \
	  -e "POSTGRES_PASSWORD=kongpass" \
	  postgres:13
	docker run --rm --network=kong-net \
	  -e "KONG_DATABASE=postgres" \
	  -e "KONG_PG_HOST=kong-database" \
	  -e "KONG_PG_PASSWORD=kongpass" \
	  -e "KONG_PASSWORD=test" \
	  kong/kong-gateway:3.6.1.1 kong migrations bootstrap
	docker run -d --name kong-gateway \
	  --network=kong-net \
	  -e "KONG_DATABASE=postgres" \
	  -e "KONG_PG_HOST=kong-database" \
	  -e "KONG_PG_USER=kong" \
	  -e "KONG_PG_PASSWORD=kongpass" \
	  -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
	  -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
	  -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
	  -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
	  -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
	  -e "KONG_ADMIN_GUI_URL=http://localhost:8002" \
	  -e KONG_LICENSE_DATA \
	  -p 8000:8000 \
	  -p 8443:8443 \
	  -p 8001:8001 \
	  -p 8444:8444 \
	  -p 8002:8002 \
	  -p 8445:8445 \
	  -p 8003:8003 \
	  -p 8004:8004 \
	  kong/kong-gateway:3.6.1.1
elif [ "$COMM" == "stop" ]; then
	docker kill kong-gateway
	docker kill kong-database
	docker container rm kong-gateway
	docker container rm kong-database
	docker network rm kong-net
else
	echo "Unknown argument."
fi
EOF
chmod +x ./run.sh
```
スクリプトを実行する。なお、止めたい時は引数を`stop`に変更する。
```sh
./run.sh start
```
起動したらブラウザで[http://localhost:8002](http://localhost:8002)につなげばKong ManagerのUIが確認できる。
![1710223888417.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/f1a267aa-26ab-d6e2-27a3-a19ad6eb3f7d.png)


## リポジトリとRunnerの準備
GitHub上に適当なリポジトリをPrivateで作成する。
![1710119836631.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/1392222a-09f8-5254-dde5-a6a17775dcb9.png)

Runnerを登録する。
`Settings`->`Actions`->`Runners`から`New self-hosted runner`を選択し、Runnerを自PCで動かすための設定を行う。
![1710120854510.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/d3bba549-44b3-15f4-af8d-2593162370f6.png)

ここではMacで進めるため、Macを選択して表示されたコードを実行する。
```sh
mkdir actions-runner && cd actions-runner
curl -o actions-runner-osx-x64-2.314.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.314.1/actions-runner-osx-x64-2.314.1.tar.gz
tar xzf ./actions-runner-osx-x64-2.314.1.tar.gz
./config.sh --url https://github.com/imurata/sandbox-actions --token AOSRR5YX6xxxxxxxxxxxx
```
`config.sh`を実行すると色々聞かれるが、これは全てデフォルトのままでOK。
公式手順では最後に`run.sh`を実行するが、サービス化して自動実行したい人は「[セルフホストランナーアプリケーションをサービスとして設定する](https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service?platform=mac)」を参考に`svc.sh`を使ってサービスをインストール・実行するとよい。
問題なく追加できると、GitHub上でStatusが緑で表示されるようになる。
![1710121253141.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/921ccfae-64e5-4c8a-cc33-fc9b93077425.png)

## RunnerのホストにCLIをインストール
API作成・管理の前提としてInsomnia、Kongを利用するとしていたが、これらを使っている場合、それらの機能を利用するためのCLIが
Insomniaには[`inso`](https://github.com/Kong/insomnia/tree/develop/packages/insomnia-inso)コマンド、Kongには[`decK`](https://github.com/Kong/deck)コマンドとして用意されている。
これら各コマンドをRunnerに実行させて自動化を進める。
自動化したい内容とCLIのマッピングは以下となる。


|自動化項目|コマンド|
|---|---|
|Linterでのチェック|inso lint spec|
|APIのテスト|inso run test|
|API Gatewayの設定の生成・チェック|inso export spec<br>deck file openapi2kong<br>deck gateway validate|
|API Gatewayの設定の反映|deck gateway sync|
|動作確認|http --check-status|

ということで、上記のコマンドをインストールする。
```sh
brew install inso
brew install deck
brew install httpie
```
## コマンドの確認
Runnerに実行させる前に一度手でコマンドを実行する。
### Linterでのチェック
`inso`CLIの前提として、`.insomnia`ディレクトリがない場合は以下のディレクトリを参照する。
|OS|場所|
|---|---|
|Windows|`%APPDATA%\Insomnia`|
|Linux|`$XDG_CONFIG_HOME/Insomnia`か`~/.config/Insomnia`|
|Mac|`~/Library/Application\ Support/Insomnia`|

今回は開発端末上で`inso`コマンドを実行するので問題ないが、Dockerコンテナ上で`inso`コマンドを使う場合は注意。（参考：[公式のDockerでの利用手順](https://docs.insomnia.rest/inso-cli/inso-on-docker)）
またコマンドのリファレンスは[CLI Command Reference](https://docs.insomnia.rest/inso-cli/cli-command-reference)に記載がある。
APIのSpecのチェックは[`inso lint spec`](https://docs.insomnia.rest/inso-cli/cli-command-reference/inso-lint-spec)で行うことが出来、引数にInsomniaのプロジェクト作成後に作ったドキュメントの名前を渡すことで、そのドキュメントのチェックが行われる。また、`--verbose`をつけると詳細情報が出るので、これも併せて渡しておく。
```sh
inso lint spec my-spec.yaml --verbose
```
実行した結果は以下のようになった。
```sh
$ inso lint spec  my-spec.yaml --verbose
› Data store configured from app data directory at /Users/imurata/Library/Application Support/Insomnia
› Load api specification with identifier my-spec.yaml from data store
› Found 1.
› Linting specification from database contents
No linting errors or warnings.
$ echo $?
0
```
なお、コマンドの引数にファイル名を渡さない場合、対話型でファイルを指定することが出来る。
###  APIのテスト
テストは[`inso run test`](https://docs.insomnia.rest/inso-cli/cli-command-reference/inso-run-test)で実行できる。
基本的にはテストスイートの名前と環境変数の箱の名前を指定して実行する。またこちらも`--verbose`による詳細化が可能なのでつけるようにしておく。
```sh
inso run test "Qiita Test Suite" --env myenv --verbose
```
ここでは紹介しないがオプションでテストケースを絞ったり、テスト失敗時の挙動を変更したり出来るので、CI/CDの中でテストケースを弄りたい場合はどういうオプションがあるかを確認しておくとよい。
実行結果は以下となる。
```sh
$ inso run test "Qiita Test Suite" --env myenv --verbose
› Data store configured from app data directory at /Users/imurata/Library/Application Support/Insomnia
› Load api specification with identifier Qiita Test Suite from data store
› Found 0.
› Load workspace with identifier Qiita Test Suite from data store
› Found 0.
› Load unit test suite with identifier Qiita Test Suite from data store
› Found 1.
› Load base environment for the workspace wrk_cb60ad72964441d2bc9656c516bf46b5 from data store
› Found 1.
› Load sub environment with identifier myenv from data store
› Found 1


  Qiita Test Suite
[network] Response succeeded req=req_59c8b171e7f74c6d8cec4c09d06ecc99 status=200
    ✔ Returns 200 (1019ms)
[network] Response succeeded req=req_f1c6f73cd4f945e89bc6ca5d1acc3386 status=200
    ✔ Returns 200 (206ms)
[network] Response succeeded req=req_77c3a7e52760490fa13707f8348eb084 status=200
    ✔ Returns 200 (210ms)


  3 passing (1s)
```
ちなみにテストに失敗した時はInsomniaのUIから実行したときと比べてCLIで実行した時の方が細かくエラー情報が表示される。

### API Gatewayの設定の生成・チェック
ここでは以下の手順で設定ファイルを生成・チェックしていく。
1. OpenAPIの仕様をファイル化（[`inso export spec`](https://docs.insomnia.rest/inso-cli/cli-command-reference/inso-export-spec)）
2. OpenAPIの仕様が書かれたファイルからKongの設定ファイルを生成（[`deck file openapi2kong`](https://docs.konghq.com/deck/1.35.x/reference/deck_file_openapi2kong/)
3. Kongと通信して設定ファイルの有効性を検証（[`deck gateway validate`](https://docs.konghq.com/deck/1.35.x/reference/deck_gateway_validate/)）

#### OpenAPIの仕様をファイル化
以下のコマンドでInsomniaのDB内にあるOpenAPIの仕様をYAMLファイルとして出力する。
```sh
inso export spec my-spec.yaml --verbose --output ./my-spec-export.yaml
```
こちらも`--verbose`をつけて詳細情報を出すようにしておく。また、`--output`オプションがないと標準出力に表示するため、`--output`をつけてファイル化する。

#### OpenAPIの仕様が書かれたファイルからKongの設定ファイルを生成
ここからは`deck`コマンドで行う。`deck`コマンドはKong API Gatewayの設定を管理するためのCLIであり、コマンド仕様が`inso`とは少し異なる。
OpenAPIの仕様をKongの設定ファイルに変換するには以下のコマンドを実行する。
```sh
 cat my-spec-export.yaml | deck file openapi2kong --inso-compatible --output-file my-spec-kong.yaml --verbose 1
```
`deck file openapi2kong`は標準出力を入力として受け取るため、`cat`で吐いたものをパイプで渡す。
また、互換性のため`--inso-compatible`をつけることが推奨されているため、こちらも付与しておく。
#### Kongと通信して設定ファイルの有効性を検証
以下のコマンドで先ほど作成したKongの設定ファイルの検証を行う。
```sh
deck gateway validate my-spec-kong.yaml --verbose 1
```
`--verbose 1`については、つけておくと確認先のKong Gatewayのアドレスが表示されるので、トラシュ用につけている。
なお、参照先のKong Gatewayのアドレスはデフォルトで`http://localhost:8001`となっている。
これは自PCにKongを立ち上げている人は問題ないが、別の環境にKongを立てている人は`--kong-addr`もしくは環境変数`DECK_KONG_ADDR`で変更してあげる必要がある。

<!-- inso generate config
deck file convert -->

### API Gatewayの設定の反映
最後に[`deck gateway sync`](https://docs.konghq.com/deck/1.35.x/reference/deck_gateway_sync/)で設定を反映する。
ただ、ここではお試しで確認するので、反映後に元に戻せるようにしておきたい。
そのために、先にバックアップを取っておく。バックアップは[`deck gateway dump`](https://docs.konghq.com/deck/1.35.x/reference/deck_gateway_dump/)で取得できる。
```sh
deck gateway dump -o backup.yaml
```
また、設定を適用したらどう変わるかを確認したい場合は[`deck gateway diff`](https://docs.konghq.com/deck/1.35.x/reference/deck_gateway_diff/)で確認できる。
```sh
$ deck gateway diff my-spec-kong.yaml
creating service Qiita_API
creating route Qiita_API-getitems
creating route Qiita_API-getitemsid
creating route Qiita_API-getuserinfo
Summary:
  Created: 4
  Updated: 0
  Deleted: 0
```
バックアップや変更点の確認が終わったら、実際にコマンドを実行して設定を反映する。
```sh
deck gateway sync my-spec-kong.yaml
```
問題なければ以下のような出力が得られる。
```sh
creating service Qiita_API
creating route Qiita_API-getitems
creating route Qiita_API-getuserinfo
creating route Qiita_API-getitemsid
Summary:
  Created: 4
  Updated: 0
  Deleted: 0
```
[http://localhost:8002](http://localhost:8002)を開いてKong Managerから`Gateway Services`を確認すると、Qiita_APIというのが作成されているのがわかる。
![1710224465994.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/723319df-e61d-626c-5b8c-30146426d911.png)
以下のようにRouteも作成されており、Kong経由でアクセスできるようになった。
![1710224567052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/946fdb9b-b8cb-55bf-8485-6a4b84154a9c.png)

### 動作確認
`http`コマンドは`curl`の上位版みたいなコマンドで、このコマンドの`--check-status`がCI/CDパイプラインに使いやすいのでここではこのコマンドで動作確認する。
Kong Gatewayはデフォルトでポート8000にアクセスすることで、先ほどKong ManagerのUIで確認したRoute内のパスにルーティングしてくれるので、以下のような感じで各リクエストを発行する。
```sh
http -h --check-status localhost:8000/items
http -h --check-status localhost:8000/items/bf1a1aa9f2a609864e7e
http -h --check-status --auth-type bearer --auth 56738b89775bcxxxxxx localhost:8000/authenticated_user 
```
`-h`オプションでヘッダのみ表示し、`--check-status`オプションで200以外の値の時に非ゼロでコマンドの戻り値を返すようにしている。
認証のチェックでは`--auth-type`と`--auth`でBearerの指定およびトークン渡しを実施している。
問題なければそれぞれ`HTTP/1.1 200 OK`が返ってくる。
確認が終えたら先程のバックアップを使ってKong Gatewayの設定を元に戻す。
```sh
$ deck gateway sync backup.yaml
deleting route Qiita_API-getuserinfo
deleting route Qiita_API-getitems
deleting route Qiita_API-getitemsid
deleting service Qiita_API
Summary:
  Created: 0
  Updated: 0
  Deleted: 4
```

CLIでの手動確認は以上となる。

## GitHubActionsの設定
ここからは先程確認したCLIの内容をGitHubActionsに落とし込んでいく。
最初にBearerのトークンをYAMLに直接書くのを避けるために、トークンをActionsのSecretととして定義する。
リポジトリの`Setting`->`Secrets and variables`->`Actions`から`New repository secret`を選択して値を作成する。
![1710226316206.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/c9c83078-aed1-1d50-6546-e3f9b1f72193.png)

次にActionsのYAMLを書いていく。
先ほど用意したリポジトリをCloneしてディレクトリに移動する。
```sh
git clone https://github.com/imurata/sandbox-actions.git
cd sandbox-actions
```
GitHub Actionsは`.github/workflows`の下のYAMLを解釈して動作するため、ディレクトリを作成してYAMLをその中に作成する。
[ワークフロー構文](https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions)を見ながら以下のような感じで作成する。
```yaml
mkdir -p .github/workflows
cat <<'EOF' > ./.github/workflows/push.yaml
name: APIOps Test

on:
  push:
    branches:
      - main

jobs:
  apiops_test:
    runs-on: self-hosted
    name: Test, Generate Config, Deploy
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Linting
        run: inso lint spec my-spec.yaml --verbose
      - name: Testing
        run: inso run test "Qiita Test Suite" --env myenv
      - name: Export OpenAPI Spec
        run: inso export spec my-spec.yaml --verbose --output ./my-spec-export.yaml
      - name: Convert from OpenAPI Spec to Kong Confing
        run: cat my-spec-export.yaml | deck file openapi2kong --inso-compatible --output-file my-spec-kong.yaml --verbose 1
      - name: Validate Kong Config
        run: deck gateway validate my-spec-kong.yaml --verbose 1
      - name: Deploy Kong Config
        run: deck gateway sync my-spec-kong.yaml
      - name: Confirmation
        run: |
          sleep 5
          http -h --check-status GET localhost:8000/items
          http -h --check-status GET localhost:8000/items/bf1a1aa9f2a609864e7e
          http -h --check-status --auth-type bearer --auth ${{secrets.QIITA_API_TOKEN}} GET localhost:8000/authenticated_user
EOF
```
今回はガチ実装ではなくAPIOps実装の検証目的であるため、PR時の処理は見送った。
基本的に手作業で確認した際のコマンドをコピペしているが、Bearerトークンの箇所だけ`${{secrets.QIITA_API_TOKEN}}`に置き換えている点に注意。
また、Kongへの設定反映後、アクセスが早すぎるとRouteが作成されていなくて`http`コマンドが失敗するため、sleepを入れている。
あと原因が分からないが、Runnerから実行した場合に限り`http`コマンドがなぜか`POST`で動いて上手く動かなかったので、明示的に`GET`で動くよう引数を追加している。
上記ファイルを作成後、pushする。
```sh
git add .
git commit -m "initial commit"
git push origin main
```
PushするとGitHub Actionsが動き出し、上記で定義したYAMLの内容が実行される。
![1710231498922.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/683257b2-4301-65ce-fbf9-e4d9d648fc82.png)
走りきった後にはKong ManagerからServiceやRouteも確認できる。
![1710229804602.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/bf13689c-f150-ff6d-9e74-5f3cd0659504.png)

## 終わりに
ということで、GitHubの力を借りて自PCだけでAPIOps的なCI/CDパイプラインを実装することが出来た。

と言いつつ、大事な点に触れていなかったことにお気づきだろうか。
OpenAPIのYAMLをGitで管理していないので、パイプラインのトリガーがOpenAPIの仕様変更に対応していなかったことに。
実はInsomniaのVersion8で[フリー版からGit Sync機能が削除された](https://github.com/Kong/insomnia/issues/6583)ので、Insomnia利用時にAPIの仕様変更契機で直接キックすることが出来なさそう。
Insomniaを使った厳密なGitOpsをやりたい場合はVersion7系を使うか、有料版を使う必要がある点は注意が必要となる。
（※Insomniaを無理して使わず、OpenAPIのYAMLをリポジトリ上で管理してしまえばもっと簡単に実現は出来る）
その点を置いておけば、GitHubをGitLabに置き換えQiitaのAPIをローカルなサービスに置き換えれば、自PCで全て完結するオフラインでも利用可能なAPIOps環境なんかも作れそうなので、それなりに使えそうではある。