概要
Kubernetesを利用した開発において、クラスタ外のDNS名(ドメイン名)を使ったアクセスが必要となる時があります。実際にそのDNS名でアクセスできる(しても良い)場合は問題ないのですが、例えば下記のようなケースが考えられます。
- プログラムに本番用URLがハードコーディングされているが、何とかして開発用のMockにリクエストをねじ曲げたい
- Cloud上のクラスタからしかアクセスできないManagedなDBに対してローカルのMacから接続したい
KubernetesにはDNS名でのアクセスを操作するための機能がいくつか用意されているため、これらのユースケースを実現できないかと検討してみたところ、標準機能に一工夫すれば実現できることが分かりました。
そこで検討したKubernetesの標準機能と、それを利用して上記のユースケースを実現する方法についてまとめてみました。
KubernetesのDNS関連機能
KubernetesのDNSに関する機能のうち、今回のユースケースに関係するもの1を記載しています。
(前提)ServiceへのDNS名でのアクセス
説明の前提として、ServiceへのDNS名でのアクセス方法について簡単に記載しておきます。
下記のようなServiceがあったとします。
apiVersion: v1
kind: Service
metadata:
name: svc1
sepc:
...
クラスタ内からは、このServiceに対して svc1
という名前でアクセスすることができます。
$ curl http://svc1/
正式には svc1.[namespace].svc.cluster.local
という名前なのですが、クラスタ内では resolv.conf
に下記のような search
行が自動設定されているため svc1
が補完されてアクセス可能になっています。
search [namespace].svc.cluster.local svc.cluster.local cluster.local
hostAliases
Podの /etc/hosts
を上書きする機能です。
下記のように hostAlaises
の設定をPodに対して行います。
apiVersion: v1
kind: Pod
metadata:
name: alias
spec:
containers:
- name: nginx
image: nginx
hostAliases:
- ip: "127.0.0.1"
hostnames:
- "hoge.example.com"
- "fuga.example.com"
設定されたPodから hoge.example.com
や fuga.example.com
という名前でアクセスすると、実際には127.0.0.1(localhost)
に(名前解決されて)リクエストが送られます。
root@alias:/# curl -s http://hoge.example.com/ | head
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
これは最初に示したように Pod の /etc/hosts
に下記のような追記が行われることにより実現されています。
root@alias:/# cat /etc/hosts | tail -n 2
# Entries added by HostAliases.
127.0.0.1 hoge.example.com fuga.example.com
ExternalName
Service名に対して CNAME
を設定できる機能です。
この機能を利用するには spec.type
が ExternalName
な Service を作成します。
apiVersion: v1
kind: Service
metadata:
name: external-svc1
spec:
type: ExternalName
externalName: example.com
クラスタ内で external-svc1
という名前でアクセスすると、実際には example.com
にリクエストが送られます2。
$ curl -s -H 'Host: example.com' http://external-svc1/ | head
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
(おまけ)Headless Service (subdomain)
今回のユースケースでは使っていない3のですがDNS関連の機能なようなので記載しておきます。Headless Service自体にはいくつかの使い方があるようですが、ここではDNS名(サブドメイン)に関連した部分のみ触れています。
まずはServiceの接続先となるPodを作成します。
apiVersion: v1
kind: Pod
metadata:
name: sub-pod
labels:
name: sub
spec:
hostname: sub1
subdomain: example
containers:
- name: nginx
image: nginx
Podのポイントは下記の点です。
-
hostname
とsubdomain
を設定する - Serviceのselectorに使うため
metadata.labels.name=sub
を設定する
このPodに対応するServiceを作成します。
apiVersion: v1
kind: Service
metadata:
name: example
spec:
selector:
name: sub
clusterIP: None
Serviceのポイントは下記の点です。
- Podの
subdomain
とServiceのmetadata.name
を同じ名称にする。 -
clusterIP
にNone
を指定する(このような Service を Headless Service と呼ぶらしいです) -
spec.selector
にPodのmetadata.labels
と同じ値を指定(これは普通のServiceと同じ)
このようなServiceを作成することでクラスタ内から sub1.example
という名称で対象Podへのアクセスが可能となります。
root@alias:/# curl -s sub1.example | head
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
ちなみに(こちらが普通の使い方だとは思いますが) example
というServiceの名称でも同様にPodへのアクセスが可能です。
ユースケースごとの実現方法
これまでに説明した標準機能の応用として、最初に示したユースケースなどの実現方法を示していきます。
Podからの外部DNS名でのアクセスを別の場所に向けたい
プログラム内で接続先に hoge.example.com
がハードコーディングされている状況で、これを透過的に開発用のService(Pod)にアクセスさせたい等のケースです(プログラムを直した方が良い気はとてもしますが・・・)。
一見すると hostAliases
だけで実現できそうなのですが、接続先のService(Pod)のIPアドレスは動的に変わるため、固定のIPアドレスを設定することは現実的ではないと思います。
この問題を解決する方法はいくつかあるようなのですが socat
を併用する方法が一番簡単そうです。
具体的には下記のような構成で実現します。
まず接続元Podで hostAliases
を使って hoge.example.com
を 127.0.0.1
にaliasします。
hostAliases:
- ip: "127.0.0.1"
hostnames:
- "hoge.example.com"
次に接続元PodのSidecarとして socat
を追加します。
containers:
- name: socat
image: alpine/socat
command: ["socat"]
args:
- "tcp-listen:8080,fork,reuseaddr"
- "tcp-connect:mock-service:8080"
ports:
- containerPort: 8080
ここでのポイントは下記の点となります。
- tcp-listenで接続先のポート番号を指定(ここでは
8080
) - tcp-connectで実際に接続させたいアドレスを指定(ここでは
mock-service:8080
)
これで接続元Podから hoge.example.com:8080
という名称でアクセスすると開発用の mock-service:8080
に接続が向かう状態となります。
外部DNS名のリソースにPort Forwardingしたい
Kubernetesクラスタ内からしかアクセスできないリソースに対して、ローカルの端末からPort Forwardingして接続するようなケースです。例えばAWSのEKSクラスタと同じVPC内にあるDBに対して、ローカルMac上のDBツールを使って接続する場合などが該当します。
Serviceに対するPort Forwardingが可能なため、一見すると ExternalName
で実現できそうなのですが、残念ながら ExternalName に対しては PortForwarding をすることはできないようです。
物理サーバで良くあるSSHの踏み台サーバ(Pod)を用意する方法も考えられるのですが、鍵の管理など色々と面倒な部分が出てきます。そこでこちらのケースでも socat
を利用した下記のような構成で実現してみます。
まずPort Forwardingの対象となるServiceを用意します。
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
selector:
app: mysql-proxy
ports:
- protocol: TCP
port: 3306
targetPort: 3306
次にServiceに対応する Deployment を作成します。(Podでも良いのですが死んだ場合に自動で再起動することを期待してDeploymentにしてみます)。このDeploymentではポート 3306
を managed-mysql:3306
に転送するsocatが動いています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-proxy
spec:
selector:
matchLabels:
app: mysql-proxy
template:
metadata:
labels:
app: mysql-proxy
spec:
containers:
- name: socat
image: alpine/socat
command: ["socat"]
args:
- "tcp-listen:3306,fork,reuseaddr"
- "tcp-connect:managed-mysql:3306"
ports:
- containerPort: 3306
最後に managed-mysql
に対応する ExternalName
を作成します。socatに直接RDBのアドレスを書いても良いのですが、実用的にはクラスタ上の他のPodからも利用することが多いと思われるのでExternalNameを介しています。
apiVersion: v1
kind: Service
metadata:
name: managed-mysql
spec:
type: ExternalName
externalName: [実際のRDBのアドレス]
あとは接続前に svc/mysql
への Port Forwarding を実施すれば準備は完了です。
$ kubectl port-forward svc/mysql 3306:3306
Forwarding from 127.0.0.1:3306 -> 3306
Forwarding from [::1]:3306 -> 3306
127.0.0.1:3306
を経由してCloud上のDBへのアクセスが可能となります。
$ mysql -h 127.0.0.1 -P 3306 ...
Webブラウザから外部DNS名でクラスタ内のServiceに接続したい
Ingressを使わずに hoge.example.com
という名称でWebブラウザからKubernetes上のService(Pod)にアクセスしたい場合が該当します。
主にProxy(Reverse Proxy)の設定方法になってしまうので詳細は省略しますが、下記のようにクラスタ上に Proxy(ForwardProxy + hostAliases + ReverseProxy) を用意して Port Forwarding した上でWebブラウザのProxy設定をそちらに向ければ実現できます。