背景
最近、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-nginx
とdbt-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の転送もできてこれはこれですごい便利。
- Using IAP for TCP forwarding | Identity-Aware Proxy | Google Cloud
- GCEちょい技 – Cloud IAP TCP Forwarding で踏み台サーバ要らずなメンテナンス | apps-gcp.com
まず、どのユーザーに対して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を全公開すると、何でもやりたい放題になる...