KubernetesでDNS名でのアクセスを操る方法

# 概要

Kubernetesを利用した開発において、クラスタ外のDNS名（ドメイン名）を使ったアクセスが必要となる時があります。実際にそのDNS名でアクセスできる（しても良い）場合は問題ないのですが、例えば下記のようなケースが考えられます。

- プログラムに本番用URLがハードコーディングされているが、何とかして開発用のMockにリクエストをねじ曲げたい
- Cloud上のクラスタからしかアクセスできないManagedなDBに対してローカルのMacから接続したい

KubernetesにはDNS名でのアクセスを操作するための機能がいくつか用意されているため、これらのユースケースを実現できないかと検討してみたところ、標準機能に一工夫すれば実現できることが分かりました。

そこで検討したKubernetesの標準機能と、それを利用して上記のユースケースを実現する方法についてまとめてみました。

# KubernetesのDNS関連機能

KubernetesのDNSに関する機能のうち、今回のユースケースに関係するもの[^1]を記載しています。

[^1]: ここで触れているもの以外では /etc/resolv.conf の nameserver を変更して自前のDNSサーバを利用する機能などがあるようです。

## （前提）ServiceへのDNS名でのアクセス

説明の前提として、ServiceへのDNS名でのアクセス方法について簡単に記載しておきます。

下記のようなServiceがあったとします。

```yaml:svc1.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc1
sepc:
  ...
```

クラスタ内からは、このServiceに対して `svc1` という名前でアクセスすることができます。

```console
$ 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に対して行います。

```alias.yaml
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)` に（名前解決されて）リクエストが送られます。

```console
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 を作成します。

```yaml:external-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: external-svc1
spec:
  type: ExternalName
  externalName: example.com
```

クラスタ内で `external-svc1` という名前でアクセスすると、実際には `example.com` にリクエストが送られます[^2]。

```console
$ 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 {
```

[^2]: そのままだとリクエストのHostヘッダが `Host: external-svc1` になり意図したページが表示されないため `-H` オプションでHostヘッダを上書きしています。

## （おまけ）Headless Service (subdomain)

今回のユースケースでは使っていない[^3]のですがDNS関連の機能なようなので記載しておきます。Headless Service自体にはいくつかの使い方があるようですが、ここではDNS名（サブドメイン）に関連した部分のみ触れています。

まずはServiceの接続先となるPodを作成します。

```yaml:sub-pod.yaml
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を作成します。

```yaml:sub-svc.yaml
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へのアクセスが可能となります。

```console
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へのアクセスが可能です。

参考: [ServiceとPodに対するDNS](https://kubernetes.io/ja/docs/concepts/services-networking/dns-pod-service/)

[^3]: 使いどころが今ひとつ分からなかったのですが、どのような場面で使う機能なんでしょうね？

# ユースケースごとの実現方法

これまでに説明した標準機能の応用として、最初に示したユースケースなどの実現方法を示していきます。

## Podからの外部DNS名でのアクセスを別の場所に向けたい

プログラム内で接続先に `hoge.example.com` がハードコーディングされている状況で、これを透過的に開発用のService(Pod)にアクセスさせたい等のケースです（プログラムを直した方が良い気はとてもしますが･･･）。

一見すると `hostAliases` だけで実現できそうなのですが、接続先のService(Pod)のIPアドレスは動的に変わるため、固定のIPアドレスを設定することは現実的ではないと思います。

この問題を解決する方法はいくつかあるようなのですが `socat` を併用する方法が一番簡単そうです。
具体的には下記のような構成で実現します。

![](https://www.planttext.com/api/plantuml/img/PL71IWCn4BtdAuQUzB35zY8KBFK7HEXHUeZPQRjqDob9QXNf8L6XM1KlWdWKjA2WDq7yd53z3PFTjjHA833lpRmtUMaeJQKUP2aXgfVmFfKq0oQolk38TLEVf0YUhHB2HOnG-hvv-lbuc5sSxOku1AS4u809hcd2KRPybN2DiaqPGbTqi8p7DEkdMFQAG5MEXH4b6DMmqKnYP5HkjWANOyIpN6Y4Gw6rdn5jM7M4cfTGUZTDg49L---nKuTATRkyvKzb8PLYMmUXqFnNm9bNPyVENYvd0c9Ud9si3nSRrs1sU-NClL_5cUbiSkVCkJFFuROJPzuyk28rTVRHsNTdlvpz3AMv3ieZGWXBnI26FTH38Ni-nj5wlkDvk9bWlKYXF4eOvZ458BGlKuSe5aCUrOjmyqSka2BIOlL89fskzk3Sb2pCbqIkJ1h8O__Xlm00)

まず接続元Podで `hostAliases` を使って `hoge.example.com` を `127.0.0.1` にaliasします。

```yaml
  hostAliases:
    - ip: "127.0.0.1"
      hostnames:
        - "hoge.example.com"
```

次に接続元PodのSidecarとして `socat` を追加します。

```yaml
  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` を利用した下記のような構成で実現してみます。

![](https://www.planttext.com/api/plantuml/img/LL5BJiCm4Dtx5AEiw28R3e0gMf1GgG1bYRemzj1QE7Pm7De8kY7Nu0OHuWBS9X9S0t-IXcucupTlthoPJ4jBZQroIKZv95H13Ss1wRpG2fNDR2qHNFMK4AKvGj8rdrtptRstNTF-jby_7sq2hmJWFhkxXfaKhca5j0GMKbSGog9vf0pXLXjxgSsM6YxKsfCAXv0z8KpgYaDovQaAhLVS3mCNrKC0iUm7tTWD6YV6Lr627mr9CZGlWk59NfVFSf9uJaXzQwaPjV06SooahdDdCDBImkXTFOc2FZsMlDXvNrGUfAcYQ-IzU7oCXcT-cTxhidPd2RBBI9kVnttzSI0rOhsnwLcyWmyZDDo23CbOYkk422dNM-KLWkiG3zZWp9teNtCFzj-nC1eaKrJS_VC_)

まずPort Forwardingの対象となるServiceを用意します。

```yaml:mysql.yaml
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が動いています。

```yaml:mysql-proxy.yaml
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を介しています。

```yaml:managed-mysql.yaml
apiVersion: v1
kind: Service
metadata:
  name: managed-mysql
spec:
  type: ExternalName
  externalName: [実際のRDBのアドレス]
```

あとは接続前に `svc/mysql` への Port Forwarding を実施すれば準備は完了です。

```console
$ 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へのアクセスが可能となります。

```console
$ 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設定をそちらに向ければ実現できます。

![](https://www.planttext.com/api/plantuml/img/RPFFIiD04CRl-nH3JWfjq5u4GL4HKRmK13r8aMqoRHUJdNLt-m_rOb_3DoZY2_Ws1NqD9zqqIQ7aCfblvZUPRpTdpalhntacX7jMsaWhSqWeDwHH-pi_pn1OxGYXAKM8LeklrU9dzR5SBPP_o-_VpsK4hmBWyG7x5vQc3cqFf8D-Y5bHsgCTo0IXIzPVaPrAcoezBAe6rPju5oB9Q9n2T5CqQFG5-7qpztRSNoVHXNarDRf3Er49xXbBi_b-L73NuPCB0bS7HfVIggXWy5Uxbx5AMrol9fnl0VeaZcDuWqiq6Srpzg1iwLNSqAc7IizQ88rCHhYxqU84hSDs0s03OOtGv14y6Q110CCnH2Do_ZnJqg4xXX4DCSQPp4s6CHyAd9p2OUSeFk3dCAeXVVAULURO2dIl-5XbTisydHnfJ45WWxVyuNJbpWwFWyoBSD3mb_rex5qKzSgYzGWUKju7Q5ir7FdsQVFewx2MDvT0R89QsZxShJTefpJLcvgWLRdI-zAY2jVSf15NRSKIuWnroh_1Fm00)