【2019年10月版】Cloud Functions + Cloud SQL で TypeGraphQL + Sequelize の Apollo Server を動かす

# TypeGraphSQL + Apollo Server の GraphQL エンドポイントを Cloud Functions で動かしてみる。

昨日の記事([【2019年10月版】Apollo + Sequelize で GraphQL やるなら TypeScript バッチリの TypeGraphQL がおすすめ!](https://qiita.com/koinori/items/17272aee571b15d4611e)) で構築した Apollo Server を Cloud Functioinsに上げてしまおう、というのが今回のテーマです。

というか、こちらが本題で昨日のは準備編だったのですよ・・・

さらに今回は Sequelize のRDBの接続先に Cloud SQL の MySQL を使用しています。
なかなかハードル高めですが、以下の手順でまいりましょう！

## 0. Firebase を使用します。

今回は大人の事情により Firebase 経由の functions デプロイをいたします。環境変数の設定やデプロイコマンドなどが通常の Cloud Functions とは異なりますのでご注意ください。

とりあえず以下のコマンドで Firebase tools をインストールします。

```shell
$ npm i -g firebase-tools
```

引き続き firebase プロジェクトの初期設定は以下のコマンドで行います。

```shell
$ firebase init
```

`firebase init`を行うとカレントフォルダ以下に `functions`が作られます。
前回の記事で作成したプロジェクトはこの `functions` 以下にコピーし、以下では`functions`内の作業といたします。

## 1. パッケージの追加

Apollo Server を Cloud Functionsで動かくすために、apollo-server の Cloud Functions 版である `apollo-server-cloud-functions` を追加します。~~(まんまですね！)~~

```shell
$ npm i apollo-server-cloud-functions
```

ついでにデプロイ時のコンパイルで怒られたので `apollo-server-core` を package.json の `depedencies` に追加します。

最終的に package.json の dependecies は以下のようになっておます。

```json:packages.json
...
  "dependencies": {
    "@types/bluebird": "^3.5.28",
    "apollo-server": "^2.9.7",
    "apollo-server-cloud-functions": "^2.9.7",
    "apollo-server-core": "^2.9.7",
    "dataloader-sequelize": "^2.0.1",
    "firebase-admin": "^8.0.0",
    "firebase-functions": "^3.0.0",
    "graphql": "^14.5.8",
    "merge-graphql-schemas": "^1.7.0",
    "mysql2": "^1.7.0",
    "reflect-metadata": "^0.1.13",
    "sequelize": "^5.21.1",
    "sequelize-graphql-schema": "^0.1.70",
    "type-graphql": "^0.17.5"
  },
  "devDependencies": {
    "@types/node": "^12.11.6",
    "tslint": "^5.12.0",
    "typescript": "^3.2.2"
  },
...
```

昨日のプロジェクトよりfirebase関連のパッケージが増えてます。

## 2. Cloud SQL のインスタンス作成など

以降しばらくは Cloud Functions の公式ガイドに従って手順を進めます。

- [Connecting to Cloud SQL from Cloud Functions](https://cloud.google.com/sql/docs/mysql/connect-functions?hl=ja)

まずは Cloud SQL で MySQL のインスタンスを作成します。
GCP のコンパネ画面から "SQL" を選択し、MySQLインスタンスの作成メニューへ進んでください。
![MySQL 第 2 世代インスタンスの作成 Google Cloud Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/836d8509-78b1-2a75-facb-c0e70bd39db7.png)

今回のインスタンス名は `graphql-sample-xxxx` といたしました。後でも触れますがこのインスタンス名はなるべく短い方がよいでしょう。

作成ボタンを押すとインスタンスの作成が始まります。終了までしばらくお待ちください。
また、インスタンスの詳細画面で表示される**インスタンス接続名**は控えておいてください。後ほどSequelizeから接続するときに、この値を使用します。

インスタンスが出来上がったら、続いてユーザーの追加です。

![graphql ユーザーの作成 Google Cloud Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e993ca4d-e0b6-d9d6-050b-b244f7b86f11.png)

ここでも例によって "example", "example" です。

作成ができたらデータベースの作成を行います。

![graphql データベースの作成 - Google Cloud Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/3ec3b877-64b2-6cd8-6a2b-8812ba09f477.png)

ここも "example" といたします。・・・手抜きですいません。

## 3. API の有効化と権限の追加

ここは上記のご案内のままなんですが、Cloud SQL Admin API と Cloud SQL API は有効化しておいてね～ということと、ちょっとややこしいですが Cloud functions の実行アカウントに権限の追加が必要です。

GCP の管理画面の "IAM と管理"から、`xxxxxx@gcf-admin-robot.iam.gserviceaccount.com` のアカウントを探し、権限の変更を行います。

![IAM – IAM と管理 – Google Cloud Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/06567fd1-4791-d296-5ba6-96e8ddf8353a.png)

上記のように "Cloud SQL クライアント"の役割を追加します。

GCP 側の設定による準備は以上です。

## 4. Sequelize の接続先を Cloud SQL へ

続いて、Sequelize の接続先を Cloud SQLのMySQLへ繋ぎ変えます。

またここでは接続設定を直書きせずに、Cloud functions の環境変数を経由します。

まずはコードを以下のように変更しましょう。

```ts:/db/conf.ts
import { Sequelize } from 'sequelize';
import * as functions from 'firebase-functions';

export const sequelize = new Sequelize(
  functions.config().db.name,
  functions.config().db.user,
  functions.config().db.pw,
  {
    dialect: 'mysql',
    host: "localhost",
    dialectOptions: {
      socketPath: `/cloudsql/${functions.config().db.conn}`,
    }
  }
);
```

ここでのキモは設定の値は firebase の functionsの設定値経由で取得するために、`functions.config()`経由で値を取得している箇所と、SQLの接続先にUNIXソケットで`/cloudsql/[Cloud SQLのインスタンス接続名]`としているところですね。

Cloud Functions のヘルプには赤字で書いてありますが、この`/cloudsql/[Cloud SQLのインスタンス接続名]`のUNIXソケット、文字列長が最大 `107` 文字なのですね！

> Linux based operating systems have a max socket path length of 107 characters.If the total length of the path exceeds this length, you will not be able to connect with a socket from Cloud Functions.

実は手元の環境でもインスタンス接続名は70文字を超えています。プロジェクトが入り組んできたり、インスタンスの命名規則などでちょっと長い名前を付けると、107文字はあっさり超えると思います。また、リージョン名も`/asia-northeast1/` (東京)で17文字持ってかれます。。。これはもう、**気を付けましょう**としか言えないですが、出来るだけ簡潔な短い名前を付けるように心がけましょう・・・

さて、先ほどの**インスタンス接続名** と合わせて、以下のコマンドでCloud SQLの接続設定を functions の設定に追加します。

```shell
$ firebase functions:config:set db.conn="[インスタンス接続名]" db.name="example" db.user="example" db.pw="example"
```
このコマンドで設定した値が、`functions.config()`メソッド以下でアクセス可能になる、ということです。

Firebase の Functions の設定値の使い方に関しては以下のFirebaseの公式ガイドでしっかりと説明があります。

- [Firebase | Environment configuration](https://firebase.google.com/docs/functions/config-env)

## 5. Cloud Functions のエンドポイント実装

さて、いよいよ大詰めの Functions で呼び出すエンドポイントを `index.ts` に実装します。~~(昨日の記事であえて `server.ts` を使用していたのはこのためだったのですよ。。。)~~

もう、どーんと晒してしまいます！こちらです。

```ts:index.ts
import "reflect-metadata";

import * as functions from 'firebase-functions';
import { buildSchemaSync } from "type-graphql";
import { ApolloServer } from 'apollo-server-cloud-functions';
import * as db from "./database";

db.sequelize
  .sync({ force: false, alter: true })
  .catch(console.log);

const schema = buildSchemaSync({
  resolvers: [__dirname + "/resolvers/**/*.js"],
});

const server = new ApolloServer({
  schema,
  playground: true,
  introspection: true
});

exports.gqltest = functions.https.onRequest(server.createHandler({
  cors: {
    origin: true,
    credentials: true,
  },
}));
```

まず、大きく変わった点は、Cloud Functions でのエンドポイントをexportするために、この初期化処理全体を"同期"方式で書かなければ、ちゃんと認識してくれない、ということです。
そのため、前回は非同期としていたtype-graphqlのbuildSchemaを、同期方式の`buildSchemaSync` に差し替え、全体を async 関数の中から外に出しています。

続いて `ApolloServer` の import 元は `apollo-server` から `apollo-server-cloud-functions` へと切り替え、ついでに CORS の設定を追加してます。

また `playground: true`の設定に `introspection: true` を追加しました。
このあたりの`apollo-server-cloud-functions`の使い方は本家の Github Pages に上がってますので、ご覧ください。

- [https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-cloud-functions](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-cloud-functions)

上記の設定項目で仕立てた handler を`gqltest`という名前で公開します。

細かい点ですが、念のため Cloud Functions のインスタンスを "node.js 10" に切り替えました。TypeGraphQL は割と新しめの環境がいいのかな～？という配慮です。(TypeScriptが宜しくコンパイルしてくれれば大丈夫なんだろうけど。。。)
以下のように package.json で設定を変更します。

```json:package.json
...
  "engines": {
    "node": "10"
  },
...
```

## 6. デプロイ

コード上の変更は上記の手順でOKです。それでは早速、**Apollo、行きまーす！**

```shell
$ firebase deploy --only functions:gqltest 
```

で、Functionsの方でもログを監視しておいて下さい。起動時に Sequelize のマイグレーション処理が入りますが、ここで MySQL との接続がうまくいかない場合が(多々)あります。
個人的な感触では**打率7割くらい**です。やはり Functions 起動直後は他サービスとの接続が不安定ですね。。。
Sequelize のコネクションエラーなど発生したら、再度、デプロイをおこなってください。

無事に `CREATE TABLE` などのログが流れたら、接続完了です！

## 7. Playground での確認

Firebaseの管理画面、もしくはCloud Functionsの管理画面で表示されているエンドポイントをブラウザで開いてみましょう。

GraphQLのPlayground画面が立ち上がっていると思います。
例によってユーザーを一件、追加してみましょう。

左上のクエリのペインに・・・

```graphql
mutation CreateUser($userInput: AddUserInput!){
  addUser(data: $userInput) {
    id
    name
  }
}
```
と記述し、渡す引数を左下の'QUERY VARIABLES'のペインに記載します。

```graphql
{
  "userInput":{"name": "user1"}
}
```

これで画面中央の再生ボタンクリックでuser1という名前のユーザーが登録されるはずですが・・・？
![Playground -cloudfunctions gqltest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/cc15462d-a3f1-40f8-4115-2d27f02939b6.png)
はい、無事にUUID "e3530830-f6f9-11e9-8f97-b7dbf5fbb0f4" が自動発番されたユーザーが一件、返却されてまいりました！

今回はタブを追加してこのユーザを取得するクエリも流してみましょう。

新規タブを追加後、左上のペインに以下のようにクエリを記述します。

```graphql
{
  user(id: "e3530830-f6f9-11e9-8f97-b7dbf5fbb0f4") {
    id
    name
  }
}
```

画面中央の再生ボタンクリックすると・・・

![Playground - cloudfunctions net gqltest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/99f38c5b-15bb-b762-5787-39bb6cec6684.png)

はい、取得できましたっ！！Apollo+Sequelize、ちゃんと動いているようです！

## 8. Cloud Shell から MySQL を確認。

MySQL でお問合せをするために、Cloud Shellを使用してみます。

Cloud SQL のインスタンス詳細から "Cloud Shell を使用して接続" をクリックしてください。

![graphql-sGoogle Cloud Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/4bae4600-bccf-6a4a-e937-55edc3c1a0f5.png)

Shellが動くウィンドウが開くので、そこでrootユーザーのパスワードを入力します。

```shell
$ gcloud sql connect graphql-sample-xxxx --user=root --quiet
Whitelisting your IP for incoming connection for 5 minutes...done.
Connecting to database with SQL user [root].Enter password:
```

ログイン成功すると、MySQLのクライアントを使えるようになります。

```shell
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1159
Server version: 5.7.14-google-log (Google)

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```

それでは、さっそく、`example`データベースの `users` テーブルのレコードを取得してみましょう！

```shell
mysql> use example;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql> select * from users;
+--------------------------------------+-------+---------------------+---------------------+
| id                                   | name  | createdAt           | updatedAt           |
+--------------------------------------+-------+---------------------+---------------------+
| e3530830-f6f9-11e9-8f97-b7dbf5fbb0f4 | user1 | 2019-10-25 07:34:33 | 2019-10-25 07:34:33 |
+--------------------------------------+-------+---------------------+---------------------+
1 row in set (0.15 sec)

mysql>
```

`USE` でデータベースを切り替えて、`select` 文の発行です。
無事に ID:"e3530830-f6f9-11e9-8f97-b7dbf5fbb0f4"のレコードが追加されていますね！


上記の手順で、TypeGraphQLのアノテーションで定義した Resolver がしっかり動いて、Sequelize の書き込みがちゃんと Cloud SQL上の MySQLに出来ていることが確認できました。

TypeScript での TypeGraphQL + Sequelize 悪くはないんではないでしょうか？
TypeGraphQL は今日時点でまだバージョンが**0.17.5**ですが、今後も楽しみに注視していきたいと思います！

本日は以上です。