GKEでgRPCのサーバをenvoy使って外部公開したよ


GKEでマイクロサービスとかやってて、たまーーーにgRPCのサービスを外部に公開したくなることってありますよね?
でもGKEのL7LB(HTTPロードバランサ)はgRPCに対応してません(2018/03/18)。もうすぐ出るかも?
さらにはgRPCの通信は永続化されるので高負荷時にスケールさせられない問題があります。
そこで使えるのがenvoyproxyというわけです。
envoyはTLSのターミネーションにも対応しています。外部に公開したいgRPCサービスの前段にenvoyを噛ませればTLSのターミネーションからスケールアウト時のバランシングまで対応できちゃうというわけでとても便利です。

構成

L4LB -> envoy -> grpcService

grpcServiceはport:50051でGKE内にNodePortとして存在するとします。

この状態にenvoyを導入します。

TLSの準備

まず、TLSの鍵と証明書を用意します。何かしらの方法で用意して下さい。
用意された鍵と証明書をGKEのsecretとして登録します。

tls_secret.yaml
apiVersion: v1
data:
  tls.crt: base64 encoded cert
  tls.key: base64 encoded key
kind: Secret
metadata:
  name: tlssecret
  namespace: default
type: Opaque

証明書と鍵をbase64に変換する必要があります。
base64への変換は下記のコマンドなんかを使うと良いでしょう。

$ base64 -i path/to/wildcard.example.com.key

envoyの設定

TLSの準備ができたらenvoyの設定を作ります。設定はconfigmapで管理すると良い感じです。

envoy_configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: "envoy-config"
data:
  envoy.json: |
    {
      "listeners": [
        {
          "address": "tcp://0.0.0.0:15001",
          "ssl_context" : {
            "cipher_suites" : "[ECDHE-RSA-AES256-GCM-SHA384|ECDHE-RSA-AES128-GCM-SHA256]",
            "cert_chain_file" : "/etc/tlssecret/tls.crt",
            "private_key_file" : "/etc/tlssecret/tls.key"
          },
          "filters": [
            {
              "type": "read",
              "name": "http_connection_manager",
              "config": {
                "codec_type": "auto",
                "stat_prefix": "ingress_http",
                "route_config": {
                  "virtual_hosts": [
                    {
                      "name": "service",
                      "domains": ["*"],
                      "routes": [
                        {
                          "timeout_ms": 0,
                          "prefix": "/",
                          "cluster": "grpc"
                        }
                      ]
                    }
                  ]
                },
                "filters": [
                  {
                    "type": "decoder",
                    "name": "router",
                    "config": {}
                  }
                ]
              }
            }
          ]
        }
      ],
      "admin": {
        "access_log_path": "/dev/stdout",
        "address": "tcp://127.0.0.1:8001"
      },
      "cluster_manager": {
        "clusters": [
          {
            "name": "grpc",
            "features": "http2",
            "connect_timeout_ms": 250,
            "type": "strict_dns",
            "lb_type": "round_robin",
            "hosts": [{"url": "tcp://grpc-service:50051"}]
          }
        ]
      }
    }

このように/etc/tlssecretにTLSの設定を読みに行くような記述にします。(パスは別のところでもかまいません)

grpc-serviceのheadlessService化

上述のenvoyの設定で"hosts": [{"url": "tcp://grpc-service:50051"}]このように記述しています。
これはkubernetesのheadlessServiceを利用してサービスディスカバリーするためです。
kubernetesのheadlessServiceは問い合わせるとPodのIPアドレス一覧を返す仕組みです。
つまり既存で用意していたgrpc-serviceをheadlessService化する必要があります。

grpc_service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    name: "grpc"
  name: "grpc-service"
spec:
  clusterIP: None
  selector:
    app: "grpc"
  ports:
    - name: grpc
      port: 50051
      targetPort: 50051
      protocol: TCP

このようにclusterIP: NoneとしてあげるとheadlessService化します。

envoyのDeployment

configmapとheadlessServiceができたらdeploymentを用意します。

envoy_deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: "envoy"
spec:
  replicas: "2"
  template:
    metadata:
      labels:
        app: "envoy"
    spec:
      volumes:
        - name: envoy
          configMap:
            name: "envoy-config"
        - name: tls
          secret:
            secretName: tlssecret

      containers:
        - name: envoy
          image: envoyproxy/envoy:latest
          command:
            - "/usr/local/bin/envoy"
          args:
            - "--config-path /etc/envoy/envoy.json"
          resources:
            limits:
              memory: 512Mi
          ports:
            - containerPort: 15001
              name: app
            - containerPort: 8001
              name: envoy-admin
          volumeMounts:
            - name: envoy
              mountPath: /etc/envoy
            - name: tls
              mountPath: /etc/tlssecret
              readOnly: true

ここではenvoyのDockerImageにTLSの情報とenvoyの設定をマウントしています。
/etc/envoyにenvoyの設定を、/etc/tlssecretにTLSの証明書等をマウントしているのがvolumeMounts等でわかるかと思います。

envoyのservice

次に用意したenvoyを外の世界と繋げるためにserviceを用意します。

envoy_service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    name: "envoy"
  name: "envoy-service"
spec:
  type: LoadBalancer
  loadBalancerIP: "192.0.2.3"
  selector:
    app: "envoy"
  ports:
    - name: tcp
      port: 15001
      targetPort: 15001

type:LoadBalancerでserviceを作ります。これでGCPのL4LBが立ち上がります。
外部に公開することを考えるとStaticIPAdressを用意したほうが良いでしょう。これはGCPの画面から(terraform等のが良いかも)で準備します。
準備したIPアドレスはloadBalancerIPに記述してあげます。
DNSの設定も別途行っておいて下さい。

これで完成です。無事TLSで暗号化されたgRPCの通信ができるはず!!