TypeGraphSQL + Apollo Server の GraphQL エンドポイントを Cloud Functions で動かしてみる。
昨日の記事(【2019年10月版】Apollo + Sequelize で GraphQL やるなら TypeScript バッチリの TypeGraphQL がおすすめ!) で構築した Apollo Server を Cloud Functioinsに上げてしまおう、というのが今回のテーマです。
というか、こちらが本題で昨日のは準備編だったのですよ・・・
さらに今回は Sequelize のRDBの接続先に Cloud SQL の MySQL を使用しています。
なかなかハードル高めですが、以下の手順でまいりましょう!
0. Firebase を使用します。
今回は大人の事情により Firebase 経由の functions デプロイをいたします。環境変数の設定やデプロイコマンドなどが通常の Cloud Functions とは異なりますのでご注意ください。
とりあえず以下のコマンドで Firebase tools をインストールします。
$ npm i -g firebase-tools
引き続き firebase プロジェクトの初期設定は以下のコマンドで行います。
$ firebase init
firebase init
を行うとカレントフォルダ以下に functions
が作られます。
前回の記事で作成したプロジェクトはこの functions
以下にコピーし、以下ではfunctions
内の作業といたします。
1. パッケージの追加
Apollo Server を Cloud Functionsで動かくすために、apollo-server の Cloud Functions 版である apollo-server-cloud-functions
を追加します。(まんまですね!)
$ npm i apollo-server-cloud-functions
ついでにデプロイ時のコンパイルで怒られたので apollo-server-core
を package.json の depedencies
に追加します。
最終的に package.json の dependecies は以下のようになっておます。
...
"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 の公式ガイドに従って手順を進めます。
まずは Cloud SQL で MySQL のインスタンスを作成します。
GCP のコンパネ画面から "SQL" を選択し、MySQLインスタンスの作成メニューへ進んでください。
今回のインスタンス名は graphql-sample-xxxx
といたしました。後でも触れますがこのインスタンス名はなるべく短い方がよいでしょう。
作成ボタンを押すとインスタンスの作成が始まります。終了までしばらくお待ちください。
また、インスタンスの詳細画面で表示されるインスタンス接続名は控えておいてください。後ほどSequelizeから接続するときに、この値を使用します。
インスタンスが出来上がったら、続いてユーザーの追加です。
ここでも例によって "example", "example" です。
作成ができたらデータベースの作成を行います。
ここも "example" といたします。・・・手抜きですいません。
3. API の有効化と権限の追加
ここは上記のご案内のままなんですが、Cloud SQL Admin API と Cloud SQL API は有効化しておいてね~ということと、ちょっとややこしいですが Cloud functions の実行アカウントに権限の追加が必要です。
GCP の管理画面の "IAM と管理"から、xxxxxx@gcf-admin-robot.iam.gserviceaccount.com
のアカウントを探し、権限の変更を行います。
上記のように "Cloud SQL クライアント"の役割を追加します。
GCP 側の設定による準備は以上です。
4. Sequelize の接続先を Cloud SQL へ
続いて、Sequelize の接続先を Cloud SQLのMySQLへ繋ぎ変えます。
またここでは接続設定を直書きせずに、Cloud functions の環境変数を経由します。
まずはコードを以下のように変更しましょう。
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 の設定に追加します。
$ firebase functions:config:set db.conn="[インスタンス接続名]" db.name="example" db.user="example" db.pw="example"
このコマンドで設定した値が、functions.config()
メソッド以下でアクセス可能になる、ということです。
Firebase の Functions の設定値の使い方に関しては以下のFirebaseの公式ガイドでしっかりと説明があります。
5. Cloud Functions のエンドポイント実装
さて、いよいよ大詰めの Functions で呼び出すエンドポイントを index.ts
に実装します。(昨日の記事であえて server.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 に上がってますので、ご覧ください。
上記の設定項目で仕立てた handler をgqltest
という名前で公開します。
細かい点ですが、念のため Cloud Functions のインスタンスを "node.js 10" に切り替えました。TypeGraphQL は割と新しめの環境がいいのかな~?という配慮です。(TypeScriptが宜しくコンパイルしてくれれば大丈夫なんだろうけど。。。)
以下のように package.json で設定を変更します。
...
"engines": {
"node": "10"
},
...
6. デプロイ
コード上の変更は上記の手順でOKです。それでは早速、Apollo、行きまーす!
$ firebase deploy --only functions:gqltest
で、Functionsの方でもログを監視しておいて下さい。起動時に Sequelize のマイグレーション処理が入りますが、ここで MySQL との接続がうまくいかない場合が(多々)あります。
個人的な感触では打率7割くらいです。やはり Functions 起動直後は他サービスとの接続が不安定ですね。。。
Sequelize のコネクションエラーなど発生したら、再度、デプロイをおこなってください。
無事に CREATE TABLE
などのログが流れたら、接続完了です!
7. Playground での確認
Firebaseの管理画面、もしくはCloud Functionsの管理画面で表示されているエンドポイントをブラウザで開いてみましょう。
GraphQLのPlayground画面が立ち上がっていると思います。
例によってユーザーを一件、追加してみましょう。
左上のクエリのペインに・・・
mutation CreateUser($userInput: AddUserInput!){
addUser(data: $userInput) {
id
name
}
}
と記述し、渡す引数を左下の'QUERY VARIABLES'のペインに記載します。
{
"userInput":{"name": "user1"}
}
これで画面中央の再生ボタンクリックでuser1という名前のユーザーが登録されるはずですが・・・?
はい、無事にUUID "e3530830-f6f9-11e9-8f97-b7dbf5fbb0f4" が自動発番されたユーザーが一件、返却されてまいりました!
今回はタブを追加してこのユーザを取得するクエリも流してみましょう。
新規タブを追加後、左上のペインに以下のようにクエリを記述します。
{
user(id: "e3530830-f6f9-11e9-8f97-b7dbf5fbb0f4") {
id
name
}
}
画面中央の再生ボタンクリックすると・・・
はい、取得できましたっ!!Apollo+Sequelize、ちゃんと動いているようです!
8. Cloud Shell から MySQL を確認。
MySQL でお問合せをするために、Cloud Shellを使用してみます。
Cloud SQL のインスタンス詳細から "Cloud Shell を使用して接続" をクリックしてください。
Shellが動くウィンドウが開くので、そこでrootユーザーのパスワードを入力します。
$ 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のクライアントを使えるようになります。
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
テーブルのレコードを取得してみましょう!
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ですが、今後も楽しみに注視していきたいと思います!
本日は以上です。