自宅k8sクラスター内で稼働するcode-serverにtailscaleで外から繋ぐ
自宅k8sクラスターでcode-serverを起動しており、もっぱら現在のメイン開発環境となっている。
このcode-serverを外出先から接続するために、tailscaleをクラスターに導入した。
構成
code-serverにはcert-managerで払い出した所有するパブリックドメインの証明書でhttpsで接続している。 TLS終端は[ingress-nginx]で、DNSはパブリックドメインのDNSサーバにプライベートIPを登録して自宅LAN内で利用していた。
tailscaleで接続するには以下の2つのエンドポイントが必要となる。
- Proxyサーバ: ingress-nginxへHTTPS通信をプロキシするProxyサーバ
- 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ネットワークから直接接続可能にする方法 |
今回は以下の方式とした
- Proxyサーバ: Service Proxy方式で、ingress-nginxのServiceのClusterIPへプロキシさせる
- 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
このようになった。
DNS設定
tailscaleではTailscaleネットワーク上でDNSサーバの設定ができる
Add Nameservers => Custom… => “tailscale-dns-0のIP"の設定と"Split DNS"に✅
最後に
これで外出先でTailscaleに参加したPCでcode-serverのURLに接続すると
- code-serverのドメインの名前解決としてtailscale-dnsとして自宅k8sクラスター内のCoreDNSがtailscale-proxyのIPを返す。
- tailscale-proxyのIPに対してHTTPS接続することでingress-nginxへプロキシされ、目的のcode-server Podに到達する。
- ドメインは同一であるため、証明書のドメイン検証が問題なくクリアする!