【2019年8月版】Cloud FunctionsからGKE上のサービスに内部IPでHTTP GET

**※ 現時点では接続先に制限があるようです。us-central1 から asia-northeast1 への接続はタイムアウトしてしまいました。us-central1の中でないと接続できない模様です。。。**

# Cloud Functions から GKE 上のサービスにアクセスするのに外部IPアドレスは使いたくない。

GKE 上のロードバランサで外部IP割り当てるとHTTPSの対応がマストであったりするし、何より内向けのみを前提としたサービスの場合はそもそも外部に曝したくないのであります。

そのためにひと手間加える必要がありますが、キーポイントとしては以下の2つになります。

1. GKEの "[Internal TCP/UDP Load Balancing](https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balancing)"
2. Functionsなどから内部IPアドレスでGCPにアクセスするための "[Serverless VPC Access](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access)"、それをFunctionsで使用するための"[VPC ネットワークの内部リソースに接続する](https://cloud.google.com/functions/docs/connecting-vpc)"設定

の2点です。

それでは以下で、GoogleのHello Worldチュートリアルにちょっと手を加えて、FunctionsからGKE上のサービスに内部IPでアクセスする手順を示します。

# 1. GKE とHello Worldコンテナアプリのデプロイ

始めにKubernetesのクラスタとHello Worldを出力するだけのサンプルアプリをデプロイします。

## 1-1. GKE クラスタの作成

GCP の GKEコンパネからクラスタを作成します。今回は "[最初のクラスタ](https://console.cloud.google.com/kubernetes/add?template=your-first-cluster)" をテンプレートとして作成します。

クラスタでの設定のキモは、
- ネットワークの"VPCネイティブ"が`有効`であること
- ネットワークが `default` であること

です。 後ほどServerless VPC Accessで、`default`ネットワークに接続するように設定しますので、ここでも `default`のネットワークに参加させておきます。
また、`default`のネットワーク上にアクセスポイントを限定的に公開させるために、`VPCネイティブ`として`default`ネットワーク内のIPアドレスが割り当てられるように指定します。

ノード数のスペックなどはこのままでクラスタを作成しておきましょう。

## 1-2. Cloud Shell の準備

これ以降の実行は GCP の管理画面から"Cloud Shell"を起動してそちらのターミナルから実施します。

以下のコマンドで対象のクラスタを指定しておきます。

```
gcloud container clusters get-credentials your-first-cluster-1 --zone us-central1-a --project $DEVSHELL_PROJECT_ID
```
クラスタ名、ゾーン、プロジェクト名などは適宜、変更してください。

## 1-3. Hello World アプリのコンテナ準備

今回は[こちらのページのチュートリアル "Containerizing an app with Cloud Build"](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app)に従って Python でいってみます。
まずプログラムを格納するフォルダとプログラムをさくっと作成してしまいます。

```
$ mkdir helloworld-gke
$ cd helloworld-gke
$ cat <<EOF >> app.py
import os

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    target = os.environ.get('TARGET', 'World')
    return 'Hello {}!\n'.format(target)

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))
EOF
```

続いて Dockerfile の準備です。

```
$ cat <<EOF >> dockerfile
# Use the official Python image.
# https://hub.docker.com/_/python
FROM python:3.7

# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . .

# Install production dependencies.
RUN pip install Flask gunicorn

# Run the web service on container startup. Here we use the gunicorn
# webserver, with one worker process and 8 threads.
# For environments with multiple CPU cores, increase the number of workers
# to be equal to the cores available.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app
EOF

$ cat <<EOF >> .dockerignore
Dockerfile
README.md
*.pyc
*.pyo
*.pyd
__pycache__
EOF
```
続いて以下のコマンドで docker イメージの作成と Googleコンテナリポジトリへのアップロードを行います。

```
$ gcloud builds submit --tag gcr.io/$DEVSHELL_PROJECT_ID/helloworld-gke .
```

## 1-4. クラスタへアプリコンテナをデプロイ

続いて Hello Worldアプリを格納したコンテナをクラスタへデプロイしましょう。

まずは deployment 用の yaml を作成します。以下で `{{プロジェクトID}}` の箇所は実際のプロジェクトIDに置き換えてください。

```yaml:deployment.yaml
# This file configures the hello-world app which serves public web traffic.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: helloworld-gke
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - name: hello-app
        image: gcr.io/{{プロジェクトID}}/helloworld-gke:latest
        # This app listens on port 8080 for web traffic by default.
        ports:
        - containerPort: 8080
        env:
          - name: PORT
            value: "8080"
```

これを以下のコマンドでクラスタに適応します。

```
$ kubectl apply -f deployment.yaml
```

## 1-5. 内部ロードバランサのデプロイ

肝心の要素その1である"内部ロードバランサ"をデプロイします。

以下の yaml を作成します。

```service.yaml
apiVersion: v1
kind: Service
metadata:
  name: hello
  annotations:
    cloud.google.com/load-balancer-type: "Internal"
spec:
  type: LoadBalancer
  selector:
    app: hello
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
```

`cloud.google.com/load-balancer-type: "Internal"`がキモですね。

これをクラスタに適応します。

```
$ kubectl apply -f service.yaml
```

しばらくすると、hello ロードバランサの作成され、通常であれば外部IPアドレスが割り当てられますが、内部IPアドレスが割り当てられた service が作成されます。

```
$ kubectl get services
NAME         TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
hello        LoadBalancer   10.0.0.xxx    10.xxx.xxx.xxx   80:31648/TCP   102m
```

ClusterのIPとEXTERNALのIPが10.x系に当てられているので、GCEなどのインスタンスから内部のIPでアクセスできるようになったのが分かります。
Cloud Shell 自体は10.x 系は割り当てられていないはずですので上記のIPアドレスでアクセスできませんが、利用できるGCEのインスタンスなどがあれば、`curl`で確認できます。

```
curl http://10.xxxx.xxx.xxx/
Hello World!
```

# 2. Cloud Functionsのデプロイ

続いて、こちらのページ "[VPC ネットワークに接続する](https://cloud.google.com/functions/docs/connecting-vpc)" を参考に、Serverless VPC Access の設定と Cloud Functions からのアクセスを確認します。

## 2-1. Serverless VPC Access の準備

まず、初回のみ[こちらの手順を参考](https://cloud.google.com/functions/docs/connecting-vpc#creating_a_connector)に Serverless VPC Accessの設定を行います。

大まかな手順としては以下の通りです。

1. Serverless VPC Access API を有効にする。
2. "コネクタの作成" をクリック
3. 名前は適当でOKです。ここでは `internal-k8s`としておきます。
4. 地域はGKEクラスタを作成した地域を指定します。(デフォルトでは `us-cental1`)
5. ネットワークは `default`
6. IP範囲はGKEが10.x系であれば10.x系のほうが良いかもしれません。ここでは `10.10.0.0` としておきます。`/28`はテキストボックスの右側にうっすらとありますが勝手に補完されるので `10.10.0.0/28`と入力するとエラーになります。

## 2-2. Cloud Functions サービスエージェントの権限設定

続いて[こちらの手順を参考](https://cloud.google.com/functions/docs/connecting-vpc#setting_up_permissions)に、初回のみ権限の設定を行います。

大まかな手順としては以下の通りです。

1. IAM のページを開く
2. "Google Cloud Functions Service Agent"の名前のメンバーを探す。
3. 鉛筆ボタンクリックで編集ペインを開く
4. "編集者"の役割を追加

## 2-3. functions プログラムの作成

先ほどの GKE のロードバランサにアクセスするだけのプログラムを node.js ベースで作成してみましょう。

再び、Cloud Shell から[こちらのチュートリアル](https://cloud.google.com/functions/docs/tutorials/http?hl=ja)を参考に、以下のようにプロジェクトを作成します。

```
$ mkdir ~/gcf_http
$ cd ~/gcf_http
```
今回は http クライアントに `node-rest-client` を使用します。

```
$ npm init
...
$ npm i node-rest-client
```
続いて、以下のように `index.js` を作成します。`CLUSTER_IP` は上記の`kubectl get services`で得られた EXTERNAL IP のIPアドレスを指定します。

```javascript:index.json
var Client = require('node-rest-client').Client
var client = new Client();
const CLUSTER_IP="10.xxx.xxx.xxx";

exports.helloGET = (req, res) => {
  client.get(`http://${CLUSTER_IP}`, (data, response) => {
    res.send(data ? data.toString('utf8') : "ERROR");
  });
};
```

## 2-4. functionsのデプロイ

それでは、先の手順で作成した`internal-k8s`コネクタを指定して functionsをデプロイしましょう。
以下のコマンドとなります。

```
$ gcloud beta functions deploy helloGET --trigger-http --vpc-connector projects/$DEVSHELL_PROJECT_ID/locations/us-central1/connectors/internal-k8s --runtime=nodejs8
...
httpsTrigger:
  url: https://us-central1-xxxxxxx.cloudfunctions.net/helloGET
...
```

`us-central1`や`internal-k8s`は実際に作成したコネクタに合わせて変更してください。

デプロイが完了後、ブラウザや`curl`などで上記の`httpsTrigger`で表示されたURLを叩いてみましょう。

```
$ curl https://us-central1-xxxxxxx.cloudfunctions.net/helloGET
Hello World!
```

無事に `Hello World!` というメッセージが表示され、GKE上のコンテナで実行された結果が Cloud Functions から内部IP経由で取得できました。

お疲れさまでした！