最近の砂場活動その25: 複数のnamespaceにまたがるデータ管理ツールをIAPで守る

背景

最近、Argo Workflowsやdbtなどデータ管理用のアプリケーションをGKE上で動かしています。これらのツールは管理用のWebの画面が存在していて便利ですが、何も考えずにやると全世界に公開されてしまって危険です*1。手元からportforwardすれば自分だけが見る環境を作れますが、毎回portforwardするのも面倒です。portforwardせずに自分だけ管理用の画面を見れるようにIAP(Identity-Aware Proxy)を入れてみたので、それについてのメモを書きました。

また、Ingressについても自分で手を動かしたのはほぼ初めてだったので、悪戦苦闘したところもメモを残しておきます。

複数のnamespaceにまたがるツールを1つにまとめる

IAPで守るのは後からやるとして、ひとまず管理用のツールを一箇所にまとめることを考えます。argo / dbt / ..., といくつか管理ツールがあったときに

といった画面をKubernetesのIngressでまとめることを考えます。Ingressを別々で持ってもいいんですが、裏側ではCloud Load Balancingが立ち上がったり、別々になる分の静的IPの確保など、趣味プロジェクトのお財布には無視できない出費となるため、まとめます。管理用のツールはargo-ns / dbt-ns / ..., といった具合にnamespace毎に分かれていることを前提とします(RBACをnamespace毎に管理したいため)。

問題点: 素朴にはIngress内でnamespaceを分けることができない

Ingressでまとめていくことを考えたときに問題点が出てきます。Googleで検索しながら「こういう感じで書けばいいかなー」と最初は思っていました。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: my-ip
    networking.gke.io/managed-certificates: my-certificate
    kubernetes.io/ingress.class: "gce"
    kubernetes.io/ingress.allow-http: "false"
spec:
  rules:
    - host: "argo.yasuhisay.info"
      http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: argo
                port:
                  number: 80
    - host: "dbt.yasuhisay.info"
      http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: dbt
                port:
                  number: 80

しかし、この書き方で動かすためにはargoやdbtは「同じnamespace内」で管理されている必要があります。そんな...。

さらに検索してみると、同じことで困っていた人がいました。この人はk8sのServiceの一つであるExternalNameを使って問題を回避していました。

「ふう、これで解決か...」と思ったのですが、世の中は厳しい。GKEのIngressの場合、Cloud Load Balancingが立ち上がるわけですが、ExternalNameを使っている場合だとロードバランサーのヘルスチェックが通らないという問題があることが分かりました。ううう、辛い...。

解決策: Nginxでwrapする

ExternalNameでは問題の解決は難しかったのですが、何かしらwrapするしか解決方法はないと思い、最終的にはNginxでwrapすることにしました。

異なるnamespaceであっても、service-name.namespace.svc.cluster.local:2746といったFQDNを指定してpodは通信できるため、これを利用しています。以下がwrapした内容の例です。割と素朴にやっているし、あまりかっこよくはない...。Ingressのbackendにnamespaceを指定できればこんなことには...。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argo-nginx-config
data:
  nginx.conf: |-
    upstream local-argo {
      server argo-argo-workflows-server.argo.svc.cluster.local:2746;
    }
    # EventSourceがnginx越しで動かないので、対処
    # https://stackoverflow.com/questions/13672743/eventsource-server-sent-events-through-nginx/13673298#13673298
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
    proxy_buffering off;
    proxy_cache off;
    
    server {
      listen     80;
      location / {
        proxy_pass http://local-argo/;
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: argo-nginx-deployment
spec:
  selector:
    matchLabels:
      app: argo-nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: argo-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.14.2
          ports:
            - containerPort: 80
          volumeMounts:
            - name: conf
              mountPath: /etc/nginx/conf.d/
      volumes:
        - name: conf
          configMap:
            name: argo-nginx-config
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: argo-nginx
  name: argo-nginx
  annotations:
    beta.cloud.google.com/backend-config: '{"default": "backend-config-default"}'
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  selector:
    app: argo-nginx
  type:
    NodePort
status:
  loadBalancer: {}

これでargo-nginxdbt-nginxを同じnamespaceに配置できたので、Ingressがようやく動きます。設定はこんな感じ。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: my-ip
    networking.gke.io/managed-certificates: my-certificate
    kubernetes.io/ingress.class: "gce"
    kubernetes.io/ingress.allow-http: "false"
spec:
  rules:
    - host: "argo.yasuhisay.info"
      http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: argo-nginx
                port:
                  number: 80
    - host: "dbt.yasuhisay.info"
      http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: dbt-nginx
                port:
                  number: 80

Gatewayってやつを使うともっといい感じにできるかなと思ったけど、全然枯れてなさそうだったので今回は様子見で諦めました。

Ingressを設定するためのその他の設定

静的IPの確保と設定

GKE上のIngressを全世界に公開するだけなら、静的IPは不要。ただし、毎回割り振られるIPアドレスはエフェメラル外部 IP アドレスであり、途中で変化する場合がある。サブドメインを切ることを考えると、静的IPを確保しておくと便利(多少のお金はかかる)。

静的IPの確保はTerraformでやっている。

resource "google_compute_global_address" "my_ip_address" {
  name = "my-ip"
}

ドメインの確保 & CDNの設定 & 証明書の設定

ドメインはCloud Domainsで取得した。どこで取ってもよかったのだが、Cloud CDNとセットになっていると使いやすいかなと思ったので。サブドメインのAレコードに先ほど取得した静的IPのアドレスを設定する。マネージドのSSL証明書を簡単に設定できるのもよかった。k8sのリソースとして宣言すると、GCP側のSSL証明書を勝手に作ってくれる。

apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: my-certificate
spec:
  domains:
    - argo.yasuhisay.info
    - dbt.yasuhisay.info

IAPの設定

IAPは最近まで全然知らなかったのだけど、適切なIAMロールを持つユーザーのみアプリケーションやリソースにアクセスできるようにする仕組み。

今回のようにGKE上のアプリケーションに対して使うこともできるし、TCPの転送もできてこれはこれですごい便利。

まず、どのユーザーに対してiapを有効にするかを設定する。今回はTerraformを使って設定する。自分だけ設定できればいいのでuserを指定しているが、例によってGoogleグループを指定できたり、ドメインを設定できるので仕事でも使いやすい。

resource "google_iap_web_iam_member" "iap" {
  role   = "roles/iap.httpsResourceAccessor"
  member = "user:me@gmail.com"
}

iapの設定はBackendConfigというやつの設定の一部になっているので、yamlで指定する。

apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: backend-config-default
spec:
  iap:
    enabled: true
    oauthclientCredentials:
      secretName: iap-oauth

あとはサービス毎にbackend-configの設定をmetadataに書いていく。サービス毎に設定すればいいので、同一のIngressを使っていたとしても、argoはiapで守るけど、dbtはiapを設定しない、といったこともできる。

apiVersion: v1
kind: Service
metadata:
  name: argo-nginx
  annotations:
    beta.cloud.google.com/backend-config: '{"default": "backend-config-default"}'
spec:
  ...

今回は特にやってないけど、署名付きヘッダーを使って「このリクエストはIAPを通ってきたものだよ!」というのを確認することもできる。

所感

  • GKEの場合、複数のnamespaceをIngressにまとめるのがこんなに面倒だと思わなくて疲れた。大分時間を溶かした...
    • あとで聞いた話だけど、割とあるあるっぽい
  • k8sの設定をするとGCP側の設定が生えてくるの、慣れないとちょっと気持ち悪い
    • yamlを書くと証明書が生えてくるとか
    • GCPの画面から設定するのかコードで設定するといいのかが初手だと判断が難しい
      • 終わってみると全部コードで書けるものだなって分かる
  • Cloud Load Balancingの設定が完了するまでに結構時間がかかったので、試行錯誤はちょっとダルかった。知ってる人とやるとよさそう
  • IAPは便利。GKEに限らず機会があれば使っていきたい

*1:特にワークフローエンジンンであるargoを全公開すると、何でもやりたい放題になる...