カスタムしたistio-ingressgatewayを作ってGCPのLBの裏に置く

自分用メモ。

istioがinjectされたアプリを外部公開する場合は、istio-ingressgatewayを使って公開するか、NodePortで公開した普通のServiceにhttpフロントエンドなんかをおいて、それをLBの裏に置くというのがよくやられることだけれど、独自のingressgatewayを作って、それをNodePortで公開してLBの裏に置く方法をまとめておく。lbの裏にいるやつもカナリアリリースとかしたいよね。

istioを自前でインストールしている場合は、インストール時に`--set gateways.istio-ingressgateway.type=NodePort`にやればよいのだけど、gkeでistioをonにして自動で入るやつを使う場合は、k8sとistioの自動アップデートで設定が戻ってしまうので既存のingressgatewayの設定を弄らず独自に作れということらしい。

- https://cloud.google.com/istio/docs/istio-on-gke/installing?hl=ja#adding_gateways

istio-ingressgateway + cert-managerでも別にええんやないのという話もあるけれど、cert-managerのバージョンアップでハマったりしたので、gcpのmanaged certificateを使いってやりたかったということで勉強がてらやってみた。やってみてistio-ingressgatewayがだいたいわかった。

ベースのドキュメントはこれ

- https://istio.io/blog/2019/custom-ingress-gateway/


## custom ingressgatewayを作る

istio-systemのnamespaceにingressgatewayとして動作させるserviceを作成する。通常のingressgatewayは`type: LoadBalancer`で構築されているのをNodePortに変える。

```
apiVersion: v1
kind: Service
metadata:
  name: custom-ingressgateway
  namespace: istio-system
  labels:
    app: custom-ingressgateway
    istio: custom-ingressgateway
spec:
  type: NodePort
  selector:
    app: custom-ingressgateway
    istio: custom-ingressgateway
  ports:
    - name: http
      port: 80
      targetPort: 80
```

実体のproxyのDeploymentを作成する。設定については以下をベースに、 `kubectl describe pod istio-ingressgateway -n istio-system`を見てパクってきた。

- https://istio.io/blog/2019/custom-ingress-gateway/deployment-custom-ingress.yaml

```
apiVersion: v1
kind: ServiceAccount
metadata:
  name: custom-ingressgateway-service-account
  namespace: istio-system
  labels:
    app: custom-ingressgateway
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: custom-ingressgateway
  namespace: istio-system
  labels:
    app: custom-ingressgateway
    istio: custom-ingressgateway
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: custom-ingressgateway
        istio: custom-ingressgateway
      annotations:
        sidecar.istio.io/inject: "false"
        seccomp.security.alpha.kubernetes.io/pod: docker/default
    spec:
      serviceAccountName: custom-ingressgateway-service-account
      containers:
        - name: istio-proxy
          image:  gke.gcr.io/istio/proxyv2:1.1.7-gke.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
            - containerPort: 443
            - containerPort: 31400
            - containerPort: 15011
            - containerPort: 8060
            - containerPort: 15030
            - containerPort: 15031
          args:
          - proxy
          - router
          - --domain
          - $(POD_NAMESPACE).svc.cluster.local
          - --log_output_level=default:info
          - --drainDuration
          - '45s' #drainDuration
          - --parentShutdownDuration
          - '1m0s' #parentShutdownDuration
          - --connectTimeout
          - '10s' #connectTimeout
          - --serviceCluster
          - custom-ingressgateway
          - --zipkinAddress
          - zipkin:9411
          - --proxyAdminPort
          - "15000"
          - --controlPlaneAuthPolicy
          - NONE
          - --discoveryAddress
          - istio-pilot:15010
          resources:
            requests:
              cpu: 10m
          volumeMounts:
          - mountPath: /etc/certs
            name: istio-certs
            readOnly: true
          - mountPath: /etc/istio/ingressgateway-certs
            name: ingressgateway-certs
            readOnly: true
          - mountPath: /etc/istio/ingressgateway-ca-certs
            name: ingressgateway-ca-certs
            readOnly: true
          env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                apiVersion: v1
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                apiVersion: v1
                fieldPath: metadata.namespace
          - name: INSTANCE_IP
            valueFrom:
              fieldRef:
                apiVersion: v1
                fieldPath: status.podIP
          - name: HOST_IP
            valueFrom:
              fieldRef:
                apiVersion: v1
                fieldPath: status.hostIP
          - name: ISTIO_META_POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: ISTIO_META_CONFIG_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
          - name: ISTIO_META_ROUTER_MODE
            value: sni-dnat
      volumes:
      - name: istio-certs
        secret:
          defaultMode: 420
          optional: true
          secretName: istio.istio-ingressgateway-service-account
      - name: ingressgateway-certs
        secret:
          defaultMode: 420
          optional: true
          secretName: istio-ingressgateway-certs
      - name: ingressgateway-ca-certs
        secret:
          defaultMode: 420
          optional: true
          secretName: istio-ingressgateway-ca-certs
```

autoscaleの設定も入れておく。これはgkeでistioをonにした場合に設定されている物と同様。

```
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: custom-ingressgateway
  namespace: istio-system
spec:
  maxReplicas: 5
  minReplicas: 1
  scaleTargetRef:
    apiVersion: apps/v1beta1
    kind: Deployment
    name: custom-ingressgateway
  targetCPUUtilizationPercentage: 80
status:
  currentCPUUtilizationPercentage: 0
  currentReplicas: 1
  desiredReplicas: 1
```

istio-system内のgatewayとpodの設定はここまで。この後は自分のアプリ向けの設定を適当なnamespace下で示指する。


## アプリ側のGateway, VirtualService等の設定

Gatewayは以下の通り。hostsに`*`があるのはlbのhealth check用。

ingressgatewayの後ろのserviceのhealth checkのpathがdefaultの`/`でない場合にhealth checkがこけるので面倒なワークアラウンドを実施している。
`gke.gcr.io/istio/proxyv2`のlivenessProbeの設定ができればそこでpathを変えたりできるんだろうけれど……

```
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  annotations:
  name: custom-gateway
  namespace: tekitou-na-namespace
spec:
  selector:
    istio: custom-ingressgateway
  servers:
  - hosts:
    - 'ore.no.domain'
    - '*'
    port:
      name: http
      number: 80
      protocol: HTTP
```


ヘルスチェック用のserviceを作る。ただhealth checkを通すためだけの糞コンテナをデプロイ。`ore.no.domain`向けのVirtualServiceはすでにあるものとする。

```
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: healthcheck
  namespace: tekitou-na-namespace
  labels:
    app: healthcheck
    tier: web
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: healthcheck
  template:
    metadata:
      labels:
        app: healthcheck
        version: v1
        tier: web
    spec:
      containers:
      - name: healthcheck
        image: aihara/healthcheck:latest
        args: 
          - -port
          - "80"
          - -path
          - /
        ports:
          - name: http
            containerPort: 80
            protocol: TCP
        livenessProbe:
          httpGet:
            path: /
            port: 80
        readinessProbe:
          httpGet:
            path: /
            port: 80
---
apiVersion: v1
kind: Service
metadata:
  name: healthcheck
  namespace: tekitou-na-namespace
  labels:
    app: healthcheck
    tier: web
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: healthcheck
    tier: web
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: healthcheck-virtualservice
  namespace: tekitou-na-namespace
spec:
  hosts:
  - "*"
  gateways:
  - bandit-gateway
  http:
  - route:
    - destination:
        host: healthcheck
```

## Ingress

namespaceはistio-systemにしているが、ExternalServiceを使って自分のアプリのnamespaceに入れてもよいかもしれない。

ingressgatewayを通ると、`X_FORWARDED_PROTO`が問答無用でhttpになってしまうので、nginxで`X_FORWARDED_PROTO`がhttpのときにhttpsにリダイレクトするみたいなことをすると無限リダイレクトが発生するので、EnvoyFilterで頑張るか、`kubernetes.io/ingress.allow-http: "false"`でそもそもlbレベルでhttpsを無効化するなどをする。podの中のnginxで証明書を使ってるとかだと、下のissueのように`appendHeaders`で無理やりhttpsをつけるなどする。

- https://github.com/istio/istio/issues/7964

EnvoyFilterは試してないけど`X_FORWARDED_PORT`が443のときに`X_FORWARDED_PROTO`をhttpsにするとか書いて試して置きたい。組み込みluaのデバックめんどいからだるいけど。

```
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: lb
  namespace: istio-system
  annotations:
    kubernetes.io/ingress.global-static-ip-name: tekitou-na-static-ip
    kubernetes.io/ingress.class: gce
    kubernetes.io/ingress.allow-http: "false"
    ingress.gcp.kubernetes.io/pre-shared-cert: tekitouna-cert
spec:
  rules:
    - host: ore.no.domain
      http:
        paths:
          - backend:
              serviceName: custom-ingressgateway
              servicePort: 80
```

これでmanaged certificateを設定したlbの裏にingressgatewayを置くことができたが、あんまりうれしみが無いかもしれない。