GCP: Cloud FunctionsとCloud SQLをVPC経由で接続する方法

この投稿では、Google Cloud Platform(GCP)にて、Cloud FunctionsをCloud SQLに接続する手順をチュートリアル形式で説明します。

構築するインフラ構成は、Cloud FunctionsとCloud SQLをVPCに繋いで、両者が内部ネットワークだけで通信する構成にします:

![cloudfunctions-sql - New frame.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/47eba762-3f17-e0ce-2b80-07b7378f27e6.jpeg)

## 想定読者

この投稿では様々な技術スタックを使いますが、それぞれの詳しい説明は割愛します。読者としては次のような方を想定しています。

* なんとなく以下が分かる
    * Node.jsでサーバサイドプログラミング
    * MySQLへのSQLの発行のしかた
* 2週間くらいGCPに触れているGCP初心者
    * VPC、Cloud Functions、Cloud SQLの概要はなんとなく把握している
    * Cloud FunctionsでHello Worldはしたことがある
* 「サーバレスでSQLを扱いたい！」と考えている人

## Cloud FunctionsとCloud SQLをVPC経由で接続する手順

同じVPCにCloud FunctionsとCloud SQLを入れて、接続させるには次の手順を踏んで設定していくことになります:

1. VPCネットワークを作る
1. VPCに「プライベートサービス接続」のIP範囲を割り当てる
1. Cloud SQLのインスタンスを作る
1. サーバレスVPCアクセスコネクタを作る
1. Cloud Functionsのサービスアカウントに権限を付与する
1. Cloud Functionsをデプロイする

手順は、再現性を重視して、スクリーンショット多めで説明します。なので、特にこだわりがなければ、設定値はこの投稿と同じものを使うことをおすすめします。また、既存のプロジェクトではなく、新規に作成したまっさらなプロジェクトでやってみるのがおすすめです。

手順は多いですが、ひとつひとつやっていきましょう。30分ほどあればすべてこなせると思います。

### VPCネットワークを作る

まず、VPCネットワークを作ります。

メニューから「VPCネットワーク」を開きます:

![20200820115620@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/bd61d633-fa9c-87f8-8858-670fe303fb40.png)

> :bulb: VPCネットワークの利用が初めての場合、「Compute Engine API」の画面にリダイレクトされます。その場合は「有効にする」ボタンを押してAPIを有効化してください。
> ![20200820115835@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/0b3a68ec-150b-a6f5-f3b1-c5a09df03664.png)

「VPCネットワークを作成」を開きます:

![20200820120123@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/25bcfa0a-aeb1-6445-4791-f712fdbe6c5f.png)

作成画面では、次のように入力します:

1. 名前: `my-vpc-1`。好きな名前をつけてOK。
2. Subnet creation mode: `カスタム`
3. New subnet: ゴミ箱アイコンをクリックして消す。今回はサブネットは不要なので、消して構いません。

![20200820213740@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/d26a1f2c-fef6-b25f-cbad-09b6a55fc305.png)


### VPCに「プライベートサービス接続」のIP範囲を割り当てる

VPCネットワークを作ったら、次は「プライベートサービス接続」のIP範囲を割り当てます。このステップは、上で作成したVPCからCloud SQLに接続するために行います。

Cloud SQLのインスタンスは、[サービスプロバイダーネットワーク](https://cloud.google.com/vpc/docs/private-access-options#service_producer_network)という別のVPCにいるため、ユーザが作ったVPCに直接Cloud SQLを配置することができません。「プライベートサービス接続」には、ユーザが作ったVPCとCloud SQLがいるVPCを橋渡しする役割があります。

プライベートサービス接続を有効化するために、上で作ったVPCネットワークの詳細を開きます:

![20200820122720@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/4a9832d3-0d2f-391f-1f7f-8ddf87edee08.png)

「プライベートサービス接続」のタブを開きます。すると、「Service Networking APIの有効化」について聞かれるので「APIを有効にする」をクリックして有効化します:

![20200820122638@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/7e208693-0d5f-3ff9-cda8-1079a9ffdaf1.png)

続いて「IP範囲の割り当て」を開きます:

![20200820123023@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/f0f94d49-4309-994e-2707-bc7e332543db.png)

入力画面が出るので、次のとおり埋めます:

1. 名前: `cloud-sql`<br>好きな名前で構いませんが、用途がわかるような名前がいいでしょう。
2. IP範囲: `カスタム`を選択し、`192.168.1.0/24`を入力します。<br>好きなIP範囲で構いませんが、VPCの他のサブネットの範囲と重複するのはNGです。この投稿ではサブネットは作ってませんので気にしなくて構いません。なお、この`192.168.1.0/24`は「`192.168.1.0`から`192.168.1.255`の範囲をCloud SQLに割り当ててあげる」という意味になります。

![20200820123204@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/2cb5e93b-0ce5-7d61-5664-c5b1a73adbec.png)

### Cloud SQLのインスタンスを作る

ここではCloud SQLのセットアップをしていきます。

メニューから「SQL」を探して開きます:

![20200820123845@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/9c79076e-284f-9606-a26f-ee0ea9540b8e.png)

開いたら「インスタンスを作成」をクリックします:

![20200820124003@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/c079e962-4f34-c39c-1565-0cdc2134473a.png)

DBエンジンの選択画面になります。好きなDBを選んでいいですが、ここでは「MySQL」を選ぶことにします:

![20200820124049@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/1721d774-2108-12bf-c14d-fdb593662293.png)

インスタンスの設定画面が出るので、必要な設定をしていきます。

![20200820124328@2x 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/2db3cce7-7588-209c-9f29-2c837bf5b4c5.png)

- ① インスタンスID: `my-database` (好きなIDで構いません)
- ② rootパスワード: 好きなパスワードを設定します。今回は「生成」を押して自動生成しました。生成した場合は、それをメモして控えておきます。パスワードはCloud Functionsの関数で使うことになります。
- ③ ロケーションのリージョン: `asia-northeast1 (東京)`

![20200820124328@2x 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/167eee00-9cd2-5230-7b20-3f5c1474700c.png)

- ④ 「プライベートIP」にチェックを入れます。
- ⑤ 「関連付けられたネットワーキング」は、作ったVPCネットワークを選択します。
- ⑥⑦ 「マネージド・サービスネットワーク接続」の「IP範囲の選択」は、作っておいたプライベートサービス接続を選択します。
- ⑧ 上を入力し終わったら「接続」を押します。
- ⑨ 「パブリックIP」に入っていたチェックは外します。

![20200820124328@2x 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/da08f04b-2b14-5551-8d93-c153aa609cf1.png)

今回はお試しなのでマシンスペックなどを最低にして、不必要な費用が発生しないようにします。

- ⑩ 「マシンタイプ」は、「db-f1-micro」を選びます。
- ⑪ 「ストレージの種類」は、「HDD」を選択します。
- ⑫ 「ストレージの自動増量を有効化」のチェックを外しておきます。

![20200820124328@2x 5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/de9f5209-1fe6-d0cd-0db7-f89fc8f12b9e.png)

また、お試しには不要なバックアップもオフにして、費用が発生しないようにします。

- ⑬ 「バックアップを自動化する」のチェックを外しておきます。
- ⑭ 以上すべてを入力し終えたら、「作成」を押します。

Cloud SQLのインスタンスの作成され、インスタンスの詳細画面にて、そのインスタンスに割り当てられたプライベートIPが確認できます:

![20200820162915@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/846d85b9-6c6d-e97a-e52f-f18e5fb0fa5d.png)

今回は、`192.168.1.3`が割り当てられました。これはCloud FunctionsからDBに接続するときに使うのでメモしておきます。

Cloud SQL側の設定は以上で完了です。

### サーバレスVPCアクセスコネクタを作る

ここからはCloud Functionsのための作業になります。

Cloud FunctionsがVPCで通信できるようにするために、サーバレスVPCアクセスコネクタを作っていきます。

メニューの「VPCネットワーク」→「サーバレスVPCアクセス」を開きます:

![20200820163414@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/8e4dc685-cfd3-b553-8c08-88ea81270f83.png)


> :bulb: サーバレスVPCアクセスを使ったことが無い場合、Serverless VPC Access APIを有効にするか聞かれます。「有効にする」をクリックしてこのAPIを有効化してください。
> ![20200820163626@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/add7dc60-2b8f-5716-06f6-892d61584411.png)

「コネクタを作成」をクリックします:

![20200820163731@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/df18b2d7-a692-6b26-8d2d-c5d1e72e0e13.png)

作成画面が表示されるので、次のとおりに入力していきます:

- ① 名前: `cloudfunctions-connector`。 好きな名前で構いませんが、Cloud Functionsをデプロイするときに使うのでメモしておきます。
- ② リージョン: `asia-northeast1`を選択します。
- ③ ネットワーク: `my-vpc-1`。本稿の手順で作成したVPCを選びます。
- ④ IP範囲: `192.168.2.0`。好きな範囲で構いませんが、VPCのサブネットとかぶっていない範囲にする必要があります。この投稿では、`my-subnet-1`の`192.168.0.0/24`と、Cloud SQL用の`192.168.1.0/24`の範囲は使えませんので、それとかぶらない`192.168.2.0/28`にします。ちなみに、`192.168.2.0/28`は`192.168.2.0`から`192.168.2.15`の範囲になり、これをCloud Functionsのために割り当てるという意味になります。

![20200820163849@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/ca4f2479-494f-e38b-25ef-cb690a3a1ab9.png)

### Cloud Functionsのサービスアカウントに権限を付与する

メニューの「IAMと管理」を開きます:

![20200820171533@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/a9a03ee7-5b3d-61b7-0223-b8733f2db469.png)

「メンバー」一覧のフィルタに「Cloud Function」などと入力して、「Cloud Functions サービスエージェント」を絞り込みます:

![20200820171647@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/692d57e5-b6ed-3a11-8fb0-efbebd632869.png)

> :bulb: ここでCloud Functionのサービスエージェントが見つからない場合は、Cloud Functions APIを有効にしてから再度試して下さい。
> https://console.cloud.google.com/apis/library/cloudfunctions.googleapis.com を開き、ヘッダでプロジェクトを選択し、「APIを有効にする」を押して下さい。

絞り込んだら、「Cloud Functionsサービスエージェント」の鉛筆アイコンをクリックします:

![20200820171831@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/ed85fb9d-ccf5-0e01-45c1-3fb0833760e7.png)

権限の設定画面が出るので、「別のロールを追加」をクリックして次の2つのロールを付与します:

1. 「Project」→「閲覧者」
2. 「Compute Engine」→「Computeネットワークユーザー」

![20200820172008@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/18c5a8aa-14eb-a123-6350-db528ba24e67.png)

権限の付与は以上です。

### Cloud Functionsをデプロイする

続いて、Cloud Functionsに関数を作ってデプロイします。

メニューの「Cloud Functions」を開きます:

![20200820164756@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/97536cb9-316a-3fc2-ae7c-ef11a2b8eb54.png)

> :bulb: 関数を作ったことがない場合、Cloud Functionsの一言紹介画面が出るので、そこの「関数を作成」をクリックします。
> ![20200820164919@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/3a51297e-4f3a-c9b8-dfba-4b3352bcccd0.png)


関数作成画面では、次のように入力します:

- ① Function name: `function-1`。好きな名前でかいまいません。
- 「トリガー」
    - ② リージョン: `asia-northeast1`
    - ③ 認証: 「Allow unauthenticated invocations」を選択します。ここでは、後で`curl`で叩いてみたいので、誰でも関数を実行できるようにします。
    - ④ 「保存」ボタンを押します。
- 「VARIABLES, NETWORKING AND ADVANCED SETTINGS」
    - ⑤ 「接続」タブを開き、
    - ⑥ 「下り設定」に先ほど作成したサーバレスVPCアクセスコネクタの名前を入力します。本稿の手順どおりに名付けていれば`cloudfunctions-connector`です。
![20200820165535@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/64e3f05b-4b95-ba4b-fcb4-d99e1af813a8.png)

「次へ」を押して、コードの入力画面を開きます。

エディタが開くので、まずランタイムとエントリポイントを設定します。

- ① ランタイム: Node.js 12
- ② エントリポイント: `queryDatabases`

![20200820170606@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/fbaece01-0c5c-be53-3133-ec9988c5dfd5.png)

- ③④ 次に、エントリポイントと同じ関数を`index.js`に実装します。

```index.js
const mysql = require("mysql");

/**
 * @param {import("express").Request} req
 * @param {import("express").Response} res
 * @return {Promise<void>}
 */
exports.queryDatabases = async (req, res) => {
  const connection = mysql.createConnection({
    // hostはCloud SQL作成時に割り当てられたプライベートIP
    host: "192.168.1.3",
    port: 3306,
    user: "root",
    // passwordはCloud SQL作成時に設定したrootパスワード
    password: "LMgbiKCAnk1jL0FI",
  });

  try {
    // DBに接続する
    await new Promise((resolve, reject) =>
      connection.connect((err) => (err ? reject(err) : resolve()))
    );
    // データベース一覧をクエリする
    const results = await new Promise((resolve, reject) =>
      connection.query(`SHOW DATABASES`, (err, results) =>
        err ? reject(err) : resolve(results)
      )
    );
    // ログにクエリ結果を出す
    console.log(JSON.stringify({ results }));

    res.send("OK");
  } catch (e) {
    console.error(e);
    res.send("ERROR");
  }
};
```

次に、package.jsonも変更して、`mysql`パッケージへの依存を追記しておきます:

```package.json
{
  "name": "cloudfunctions-sql",
  "version": "1.0.0",
  "dependencies": {
    "mysql": "^2.18.1"
  }
}
```

以上、2つのファイルを書き換えたら「デプロイ」を押します。

![20200820171100@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/092bea45-a37d-66fe-0fee-239b7f268c98.png)

以上で関数のデプロイ作業は完了です。

関数のデプロイが完了するまでしばらく待ちます。数分かかります。

### デプロイした関数を実行してみる

関数のデプロイが完了したら、関数の詳細ページを開き、「トリガー」タブで関数のURLを調べます:

![20200820212458@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/e0cae57f-1b8a-2b68-0119-103b26fc8163.png)

URLをコピーして、`curl`で叩いてみます:

![20200820212723@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/8f053f13-fe35-78a5-87e1-02a6a87e9f43.png)

「OK」というレスポンスが返ってくれば、関数が正常に実行されています。

最後に、関数のログを確認してクエリ結果が取れているか見てみましょう。ログは関数詳細ページの「ログを表示」で見ることができます。

![20200820212859@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/1d63b0a8-e9b8-6422-0245-e16288f6c518.png)

ログを見てみると、データベース名一覧を参照するSQLがうまく動作したことが分かります:

![20200820212318@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/ee2db4ee-5437-bf50-25fd-9925d29b4b77.png)

以上で、Cloud FunctionsとCloud SQLをVPC経由で接続する方法の説明は終わりです。お疲れさまでした。

### あとかたづけ

この投稿で作成したリソースのうち、Cloud SQLは利用料金が時間単位で発生するので削除しておきましょう。

この投稿のために新規プロジェクトを作った場合は、プロジェクトを削除してしまうのが無駄な出費を抑えるには確実です。

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)