Aurora Serverless の起動状態を調べる Liveness Probe を AppSync (GraphQL API) で実装してみた

**本記事は [NECソリューションイノベータ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/nec_solution_innovators) の 12/4 の記事です。**

> アドベントカレンダーにエントリーしたのに記事書くの忘れてた〜〜〜。と 11 月も終わりに差し掛かって思い出しちゃったので、パッと思いついた即興ネタをｗ

# はじめに

ここ２〜３年、AWS を利用した小規模なサービス開発を掛け持ちしていたりするのですが、開発環境のデータベースに Aurora Serverless を利用していると、開発作業に着手した一発目の API 呼び出しで、データベースが停止状態で 503 エラーになる。ということがしばしば。

よし、やるぞ！という気持ちを折りに来る此奴。もう起動したかな？とリトライしたら、やっぱりまだダメだったりとか。ようやく起動を確認して、作業を始めてタスクを終わらせて、別のプロジェクトのタスクをやろうとしたら、そっちでも同じ事が起きたり。

CloudWatch Event とかで指定時刻に起動。とか停止までの時間を伸ばしたり。とかすると、微々たるもんだけどコスト増えちゃいますしね。
なんとかならんのかなコレ。と考えていたときに………
そうだ！Spring Boot Actuator みたいな LivenessProbe あったら、それチェックして起動を待てるやん！
と以前思ったのを思い出したので、今回の即興ネタに採用。

ということで、遊び半分で作ってみた。
ソースコードは [GitHub リポジトリ](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness) に置いているので、気になる方はどうぞ。

# システム構成

![システム構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104325/d3a83356-f378-5f91-1f16-0777b896b7af.png)


- S3 (静的 Web サイトホスティング)

  - LivenessProbe を呼び出すデモサイト置き場 (React で実装)

- AppSync (GraphQL API)

  - LivenessProbe だけが乗ってる GraphQL API (GraphQL スキーマ + VTL リゾルバで実装)

- Aurora Serverless
  - MySQL 互換 (起動確認だけなので中身は無い)

# アプリケーション実装

ソースコードの一部をかいつまんで紹介します。

## GraphQL API

### GraphQL スキーマ

Spring Boot Actuator の LivenessProbe みたいな感じで、status フィールドで起動状態が返ってくる Query だけを定義しています。

[lib/api/schema.graphql](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness/blob/main/lib/api/schema.graphql)

```
type Liveness {
  status: String!
}

type Query {
  liveness: Liveness
}

schema {
  query: Query
}
```

### VTL リゾルバ

「[AppSync が JavaScript のリゾルバをサポートした！](https://aws.amazon.com/jp/about-aws/whats-new/2022/11/aws-appsync-javascript-graphql-api-resolvers/)」とみんな盛り上がってるときに VTL を使っているという (ﾉ Д`)ｼｸｼｸ
IaC に CDK を使ってるのだけど、CDK がまだ JavaScript リゾルバをサポートしてないので、とりあえずは VTL です。
CDK がサポートしたら記事を更新するかも〜。

#### リクエスト

空っぽの MySQL に対して起動状態を確認すると言ったらコレでしょ？

[lib/api/resolvers/liveness/request.vtl](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness/blob/main/lib/api/resolvers/liveness/request.vtl)

```
#set ($statement = "SELECT 1 FROM dual")

{
    "version": "2018-05-29",
    "statements": [
        $util.toJson(${statement})
    ]
}
```

#### レスポンス

ここが今回のキモなのだけど、いつも API が 503 エラーを返す裏でログに流れてる、憎っくきメッセージ(笑)の中にあるコードで判別してます。
`Communications link failure\\n\\nThe last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.; SQLState: 08S01`

[lib/api/resolvers/liveness/request.vtl](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness/blob/main/lib/api/resolvers/liveness/response.vtl)

```
#if ($ctx.error && $ctx.error.message.contains("08S01"))
  $utils.toJson({ "status": "DOWN" })
#else
  $utils.toJson({ "status": "UP" })
#end
```

## フロントエンド

### Liveness のチェック

`aws-amplify` と `@aws-amplify/api-graphql` を使ってリクエストを投げて、受信したステータス(UP/DOWN) or 例外(ERROR) をステートに保存しています。

[lib/web/src/api/Liveness.ts:31~49 行](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness/blob/main/lib/web/src/api/Liveness.ts#L31-L49)

```typescript
const livenessProbe = async () => {
  console.log('livenessProbe');
  try {
    const result = (await API.graphql(graphqlOperation(liveness))) as GraphQLResult<LivenessQuery>;
    if (result.errors) {
      setStatus(Status.ERROR);
      return;
    }
    const currentStatus = result.data?.liveness?.status;
    if (currentStatus) {
      setStatus(currentStatus);
      if (currentStatus === Status.DOWN) {
        setRetry(Date.now());
      }
    }
  } catch (e: unknown) {
    setStatus(Status.ERROR);
  }
};
```

### Liveness が UP になるまで待つ


3 秒ごとに状態が DOWN だったら LivenessProbe を実行して起動状態を確認。
ERROR か UP になったら LivenessProbe の繰り返しを終了。

setTimeout とか、ものすっっっごくひさびさに使ったけど、React の中だとこんな感じに使うんですね。これはちょっとだけ勉強になった。

[lib/web/src/api/Liveness.ts:17~29 行](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness/blob/main/lib/web/src/api/Liveness.ts#L17-L29)

```typescript
useEffect(() => {
  let timeout: NodeJS.Timeout;
  if (status === Status.DOWN) {
    timeout = setTimeout(() => {
      livenessProbe();
    }, 3000);
  }
  return () => {
    if (timeout) {
      clearTimeout(timeout);
    }
  };
}, [retry]);
```

### デモサイトの画面

なんか、最初は起動するのを待つのが楽しくなる感じの画面を作りたかったんだけど…
もうタイムリミットなんで、これでゆるして〜〜〜（相変わらずのセンスのなさよ…）。

[lib/web/src/App.tsx:10~20 行](https://github.com/r-hirakawa/appsync-aurora-serverless-liveness/blob/main/lib/web/src/App.tsx#L10-L20)

```typescript
{status === Status.DOWN && (
    <>
    <Typography>システムの準備中です…</Typography>
    <Typography>しばらくお待ち下さい</Typography>
    <Box width='100%' height='10%' margin='20px 0px'>
        <LinearProgress />
    </Box>
    </>
)}
{status === Status.ERROR && <Typography>システムエラーが発生しました</Typography>}
{status === Status.UP && <Typography>OK</Typography>}
```

# デモサイトの動作イメージ

![動作イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104325/7d948c7c-5282-243d-1101-b1d4f1fbac8c.gif)

長すぎるので、UP を検知するまでの 5 秒前位からにカットしちゃってます（；^ω^）
停止状態の Aurora Serverless にリクエストを送信してから、起動までにおよそ 30 秒程度かかるので、そりゃ時間がかかりますよね。
画面があるので確かに UP 状態になるタイミングがわかるんですが、それにしても待ちの体感が長すぎぃ〜。

# さいごに

デモサイトの画面は理想の姿から非常に遠いですが、開発環境の Aurora Serverless の起動状態を確認できる LivenessProbe ができましたね (๑• ̀д•́ )✧ﾄﾞﾔｯ
ぶっちゃけ、だからなんなん？って作り終えてから自分で思っちゃいましたが、勢いで始めた遊び半分なので、こんなものでしょう。

締まらない感じですが、これで今年の Qiita 締めとしたいと思いま〜す。
それではみなさん、良い年末を〜。

以上。