landscape

landscape

Deep think, Easy go.

27 Aug 2023

自宅k8sクラスター内で稼働するcode-serverにtailscaleで外から繋ぐ

自宅k8sクラスターでcode-serverを起動しており、もっぱら現在のメイン開発環境となっている。

このcode-serverを外出先から接続するために、tailscaleをクラスターに導入した。

構成

arch

code-serverにはcert-managerで払い出した所有するパブリックドメインの証明書でhttpsで接続している。 TLS終端は[ingress-nginx]で、DNSはパブリックドメインのDNSサーバにプライベートIPを登録して自宅LAN内で利用していた。

tailscaleで接続するには以下の2つのエンドポイントが必要となる。

  1. Proxyサーバ: ingress-nginxへHTTPS通信をプロキシするProxyサーバ
  2. DNSサーバ: tailscaleネットワーク内でのみ所有するパブリックドメインに対する名前解決で上記のProxyサーバのtailscale IPを解決する必要がある。

tailscaleの公式でKubernetesへのデプロイ方法が公開されている。 https://tailscale.com/kb/1185/kubernetes/

ドキュメントによると以下のような方式が選択可能とのこと

方式 説明
Sidecar 公開するPodのサイドカーとして起動することで、Podをtailscaleネットワークに参加させる方法
Service Proxy ServiceのClusterIPへプロキシする単独で稼働するtailscale Podを、tailscaleネットワークに参加させる方法
Subnet Router PodやServiceのCIDRをtailscaleネットワークから直接接続可能にする方法

今回は以下の方式とした

  1. Proxyサーバ: Service Proxy方式で、ingress-nginxのServiceのClusterIPへプロキシさせる
  2. DNSサーバ: 所有するドメインに対する名前解決でProxyサーバのtailscale IPを返すDNSサーバPodを起動する。またService Proxy方式で、そのDNS ServiceのClusterIPへプロキシさせる

YAML構成

ドキュメントでは単一Podで起動する手順となっているが、tailscale上のノードはホスト名が利用される仕様からStatefulSetで起動するように修正した。

最終的なYAMLは次のようにした。

Auth Key

tailscaleネットワークに参加するためにAuth Keyを発行し、Tailscale Podに読み込ませる必要がある。 ドキュメントには以下の設定が推奨されていたが、StatefulSetで通常のサーバと同じように扱えることから、推奨設定は行わないことにした。

  • Reusable: 同じAuth Keyを複数サーバで利用できる(Pod名が異なる場合に有効だが、StatefulSetで固定化したので不要と判断した)
  • Ephemeral: サーバの接続がしばらく切れたらTailscaleから削除する設定(Podが一時的なものであれば有効だが、StatefulSetで…)

tailscale-proxy

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tailscale
  namespace: tailscale-proxy
rules:
- apiGroups: [""]
  resources: ["secrets"]
  # Create can not be restricted to a resource name.
  verbs: ["create"]
- apiGroups: [""]
  resourceNames: ["tailscale-auth"]
  resources: ["secrets"]
  verbs: ["get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tailscale
  namespace: tailscale-proxy
subjects:
- kind: ServiceAccount
  name: "tailscale"
roleRef:
  kind: Role
  name: tailscale
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tailscale
  namespace: tailscale-proxy
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: tailscale-proxy
  namespace: tailscale-proxy
spec:
  serviceName: tailscale-proxy
  selector:
    matchLabels:
      app: tailscale-proxy
  template:
    metadata:
      labels:
        app: tailscale-proxy
    spec:
      serviceAccountName: tailscale
      initContainers:
        # In order to run as a proxy we need to enable IP Forwarding inside
        # the container. The `net.ipv4.ip_forward` sysctl is not allowlisted
        # in Kubelet by default.
      - name: sysctler
        image: busybox
        securityContext:
          privileged: true
        command: ["/bin/sh"]
        args:
          - -c
          - sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
        resources:
          requests:
            cpu: 1m
            memory: 1Mi
      containers:
        - name: tailscale
          image: "ghcr.io/tailscale/tailscale:latest"
          env:
          # Store the state in a k8s secret
          - name: TS_KUBE_SECRET
            value: "tailscale-auth"
          - name: TS_USERSPACE
            value: "false"
          - name: TS_AUTHKEY
            valueFrom:
              secretKeyRef:
                name: tailscale-auth
                key: TS_AUTHKEY
                optional: true
          - name: TS_DEST_IP
            value: "10.96.91.132" # ingress-nginxのServiceのClusterIPを設定
          - name: TS_AUTH_ONCE
            value: "true"
          securityContext:
            capabilities:
              add:
              - NET_ADMIN
          resources:
            limits:
              cpu: 300m
              memory: 512Mi

tailscale-dns

DNSサーバ(CoreDNS)

大体はkube-dnsのCoreDNSから流用したもの。

CoreDNSの設定はドキュメントを見た。 プラグイン中心のドキュメントとなっており、単独のDNSサーバとして動かす設定までたどり着くのに時間がかかった。

fileプラグインで特定のドメインに対するDNSサーバを設定するのが正解のようだが、

とりあえずサクッと動かしてみたく、今はcode-serverやArog CD Dashboardなどをhostsプラグインで設定した。

対象がない場合は、kube-dnsへフォワードするようにした。

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: tailscale-dns
  name: coredns
  namespace: tailscale-dns
data:
  Corefile: |
    .:53 {
        errors
        health {
            lameduck 5s
        }
        ready
        hosts "ドメイン" {
            "tailscale-proxyのTailscale IPアドレス" code-server."ドメイン" nas."ドメイン" proxmox."ドメイン" gitbucket."ドメイン"
            fallthrough
        }
        forward . 10.96.0.10 <=== 対象がなければkube-dnsへフォワード
        reload
        log
    }    
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: tailscale-dns
  name: coredns
  namespace: tailscale-dns
spec:
  clusterIP: 10.96.0.11 # kube-dnsと被らないようにした
  clusterIPs:
  - 10.96.0.11
  ports:
  - name: dns
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53
  - name: metrics
    port: 9153
    protocol: TCP
    targetPort: 9153
  selector:
    app: tailscale-dns
  type: ClusterIP
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app: tailscale-dns
  name: coredns
  namespace: tailscale-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: tailscale-dns
  name: coredns
  namespace: tailscale-dns
spec:
  selector:
    matchLabels:
      app: tailscale-dns
  replicas: 1
  template:
    metadata:
      labels:
        app: tailscale-dns
    spec:
      containers:
      - args:
        - -conf
        - /etc/coredns/Corefile
        image: registry.k8s.io/coredns/coredns:v1.9.3
        livenessProbe:
          failureThreshold: 5
          httpGet:
            path: /health
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 60
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 5
        name: coredns
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
        - containerPort: 9153
          name: metrics
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /ready
            port: 8181
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        resources:
          limits:
            memory: 170Mi
          requests:
            cpu: 100m
            memory: 70Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            add:
            - NET_BIND_SERVICE
            drop:
            - all
          readOnlyRootFilesystem: true
        volumeMounts:
        - mountPath: /etc/coredns
          name: config-volume
          readOnly: true
      serviceAccountName: coredns
      volumes:
      - configMap:
          defaultMode: 420
          name: coredns
        name: config-volume

tailscale

こちらはtailscale-proxyと同様(IPをDNSサーバのServiceのClusterIPに設定) ホスト名(StatefulSet名)が被らないようにした。

Tailscaleの設定

Machines

このようになった。

machines

DNS設定

tailscaleではTailscaleネットワーク上でDNSサーバの設定ができる

nameservers

Add Nameservers => Custom… => “tailscale-dns-0のIP"の設定と"Split DNS"に✅

最後に

これで外出先でTailscaleに参加したPCでcode-serverのURLに接続すると

  1. code-serverのドメインの名前解決としてtailscale-dnsとして自宅k8sクラスター内のCoreDNSがtailscale-proxyのIPを返す。
  2. tailscale-proxyのIPに対してHTTPS接続することでingress-nginxへプロキシされ、目的のcode-server Podに到達する。
  3. ドメインは同一であるため、証明書のドメイン検証が問題なくクリアする!