コンテナ型仮想化技術 Study08 / Statefulset, Ingress

# はじめに
今回は、ステートフルセット、イングレス辺りをやってみます。

## 関連記事
[コンテナ型仮想化技術 Study01 / Docker基礎](https://qiita.com/tomotagwork/items/34b4505b90b71dcfa8ca)
[コンテナ型仮想化技術 Study02 / Docker レジストリ](https://qiita.com/tomotagwork/items/bb1df5232dec5ea7adb7)
[コンテナ型仮想化技術 Study03 / Docker Compose](https://qiita.com/tomotagwork/items/af802c3e25f594cb4a8a)
[コンテナ型仮想化技術 Study04 / Minikube & kubectl簡易操作](https://qiita.com/tomotagwork/items/79527a132d343ab8d534)
[コンテナ型仮想化技術 Study05 / Pod操作](https://qiita.com/tomotagwork/items/7b16bde0529c052a9540)
[コンテナ型仮想化技術 Study06 / ReplicaSet, Deployment, Service](https://qiita.com/tomotagwork/items/2eb36f84c7f396f51e56)
[コンテナ型仮想化技術 Study06' / Kubernetesネットワーク問題判別](https://qiita.com/tomotagwork/items/f1145f3f5656a5cd51c3)
[コンテナ型仮想化技術 Study07 / ストレージ](https://qiita.com/tomotagwork/items/eacf8629bd4889f80672)
[コンテナ型仮想化技術 Study08 / Statefulset, Ingress](https://qiita.com/tomotagwork/items/3a5589031854c702fcec)
[コンテナ型仮想化技術 Study09 / Helm](https://qiita.com/tomotagwork/items/f46a26e3437c716881dc)


# 参考情報
[レプリカを持つステートフルアプリケーションを実行する](https://kubernetes.io/ja/docs/tasks/run-application/run-replicated-stateful-application/)
[Exposing TCP and UDP Services via ingress on Minikube](https://skryvets.com/blog/2019/04/09/exposing-tcp-and-udp-services-via-ingress-on-minikube/)
[Accessing Kubernetes Pods from Outside of the Cluster](http://alesnosek.com/blog/2017/02/14/accessing-kubernetes-pods-from-outside-of-the-cluster/)
[NGINX Ingress Controller - Exposing TCP and UDP services](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/)

# ステートフルセットの操作
ステートフルセットはデプロイメントと似たリソースで複数Podの管理を行うことができますが、ステートフルという名前の通り、各Podの"ステート"を維持するための仕組みが組み込まれたリソースです。デプロイメントと違い、個々のPodはIDにより識別され、それぞれ専用の永続ボリュームがアサインされることになります。

Minikube環境で、以下のチュートリアルをやってみます。
[レプリカを持つステートフルアプリケーションを実行する](https://kubernetes.io/ja/docs/tasks/run-application/run-replicated-stateful-application/)

ただ、このチュートリアルで提供されているマニフェストファイルのパラメータ値は、mysqlというネーミングがいたるところに出てきて、実際のところどの値とどの値を合わせないといけないかが分かりにくい。
ネーミング変更や余分と思われる箇所を削除するなど、若干カスタマイズを行って試してみます。

## ConfigMap作成

[mysql-configmap.yml](https://raw.githubusercontent.com/kubernetes/website/master/content/ja/examples/application/mysql/mysql-configmap.yaml)適用 (これはそのまま)

```
vagrant@minikube:~/statefulset$ kubectl apply -f mysql-configmap.yaml
configmap/mysql created
```

確認

```
vagrant@minikube:~/statefulset$ kubectl get configmap
NAME    DATA   AGE
mysql   2      2m47s

vagrant@minikube:~/statefulset$ kubectl get configmap mysql -o yaml
apiVersion: v1
data:
  master.cnf: |
    # Apply this config only on the master.
    [mysqld]
    log-bin
  slave.cnf: |
    # Apply this config only on slaves.
    [mysqld]
    super-read-only
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"master.cnf":"# Apply this config only on the master.\n[mysqld]\nlog-bin\n","slave.cnf":"# Apply this config only on slaves.\n[mysqld]\nsuper-read-only\n"},"kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app":"mysql"},"name":"mysql","namespace":"default"}}
  creationTimestamp: "2019-12-11T02:04:42Z"
  labels:
    app: mysql
  name: mysql
  namespace: default
  resourceVersion: "263977"
  selfLink: /api/v1/namespaces/default/configmaps/mysql
  uid: 6ced75a7-61af-4cbc-a468-149fc606ab08
```


## Service作成

[mysql-services.yaml](https://raw.githubusercontent.com/kubernetes/website/master/content/ja/examples/application/mysql/mysql-services.yaml)をカスタマイズ

```yaml:mysql-services.yaml
# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
  name: mysql-service
spec:
  ports:
  - name: mysql-port
    port: 3306
  type: ClusterIP
  clusterIP: None
  selector:
    app: mysql-sts
---
# Client service for connecting to any MySQL instance for reads.
# For writes, you must instead connect to the master: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
spec:
  ports:
  - name: mysql-port
    port: 3306
  selector:
    app: mysql-sts
```

※2つ目のClusterIPのサービスもここでは使わないので削除しておけばよかった....

適用

```
vagrant@minikube:~/statefulset$ kubectl apply -f mysql-services.yaml
service/mysql-service created
service/mysql-read created
```

確認

```
vagrant@minikube:~/statefulset$ kubectl get service -o wide
NAME            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE     SELECTOR
kubernetes      ClusterIP   10.96.0.1     <none>        443/TCP    41d     <none>
mysql-read      ClusterIP   10.98.33.98   <none>        3306/TCP   6m51s   app=mysql-sts
mysql-service   ClusterIP   None          <none>        3306/TCP   6m51s   app=mysql-sts
```

## Statefulset作成

[mysql-statefulset.yaml](https://raw.githubusercontent.com/kubernetes/website/master/content/ja/examples/application/mysql/mysql-statefulset.yaml)をカスタマイズ

```yaml:mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql-sts
  serviceName: mysql-service
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql-sts
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:5.7
        command:
        - bash
        - "-c"
        - |
          set -ex
          # Generate mysql server-id from pod ordinal index.
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          # Add an offset to avoid reserved server-id=0 value.
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
          # Copy appropriate conf.d files from config-map to emptyDir.
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/master.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/slave.cnf /mnt/conf.d/
          fi
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map
      - name: clone-mysql
        image: gcr.io/google-samples/xtrabackup:1.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          # Skip the clone if data already exists.
          [[ -d /var/lib/mysql/mysql ]] && exit 0
          # Skip the clone on master (ordinal index 0).
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          [[ $ordinal -eq 0 ]] && exit 0
          # Clone data from previous peer.
          ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
          # Prepare the backup.
          xtrabackup --prepare --target-dir=/var/lib/mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      containers:
      - name: mysql-container
        image: mysql:5.7
        env:
        - name: MYSQL_ALLOW_EMPTY_PASSWORD
          value: "1"
        ports:
        - name: mysql-port
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping"]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            # Check we can execute queries over TCP (skip-networking is off).
            command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
      - name: xtrabackup
        image: gcr.io/google-samples/xtrabackup:1.0
        ports:
        - name: xtrabackup
          containerPort: 3307
        command:
        - bash
        - "-c"
        - |
          set -ex
          cd /var/lib/mysql

          # Determine binlog position of cloned data, if any.
          if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
            # XtraBackup already generated a partial "CHANGE MASTER TO" query
            # because we're cloning from an existing slave. (Need to remove the tailing semicolon!)
            cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
            # Ignore xtrabackup_binlog_info in this case (it's useless).
            rm -f xtrabackup_slave_info xtrabackup_binlog_info
          elif [[ -f xtrabackup_binlog_info ]]; then
            # We're cloning directly from master. Parse binlog position.
            [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
            rm -f xtrabackup_binlog_info xtrabackup_slave_info
            echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
                  MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
          fi

          # Check if we need to complete a clone by starting replication.
          if [[ -f change_master_to.sql.in ]]; then
            echo "Waiting for mysqld to be ready (accepting connections)"
            until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done

            echo "Initializing replication from clone position"
            mysql -h 127.0.0.1 \
                  -e "$(<change_master_to.sql.in), \
                          MASTER_HOST='mysql-0.mysql', \
                          MASTER_USER='root', \
                          MASTER_PASSWORD='', \
                          MASTER_CONNECT_RETRY=10; \
                        START SLAVE;" || exit 1
            # In case of container restart, attempt this at-most-once.
            mv change_master_to.sql.in change_master_to.sql.orig
          fi

          # Start a server to send backups when requested by peers.
          exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
            "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi
```

※Statefulset自体のLabel指定をはずす、Pod TemplateのLabel名、コンテナーの名前、ポートの名前辺りを変更しています。

適用

```
vagrant@minikube:~/statefulset$ kubectl apply -f mysql-statefulset.yaml
statefulset.apps/mysql created
```

確認

```
vagrant@minikube:~/statefulset$ kubectl get service,pod,storageclass,pvc,pv -o wide
NAME                    TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE   SELECTOR
service/kubernetes      ClusterIP   10.96.0.1     <none>        443/TCP    41d   <none>
service/mysql-read      ClusterIP   10.98.33.98   <none>        3306/TCP   10m   app=mysql-sts
service/mysql-service   ClusterIP   None          <none>        3306/TCP   10m   app=mysql-sts

NAME          READY   STATUS    RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
pod/mysql-0   2/2     Running   0          9m44s   172.17.0.10   minikube   <none>           <none>
pod/mysql-1   2/2     Running   0          9m36s   172.17.0.11   minikube   <none>           <none>
pod/mysql-2   2/2     Running   0          9m28s   172.17.0.12   minikube   <none>           <none>

NAME                                             PROVISIONER                AGE
storageclass.storage.k8s.io/standard (default)   k8s.io/minikube-hostpath   41d

NAME                                                                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE     VOLUMEMODE
persistentvolumeclaim/data-mysql-0                                  Bound    pvc-4570a5b6-6e12-48da-bb16-b9524e0a9dfd   10Gi       RWO            standard       3h35m   Filesystem
persistentvolumeclaim/data-mysql-1                                  Bound    pvc-b2fdc30d-ea7a-4b2b-9f07-016db25f03aa   10Gi       RWO            standard       3h35m   Filesystem
persistentvolumeclaim/data-mysql-2                                  Bound    pvc-c991093e-96b5-4844-9ed9-897824b3c029   10Gi       RWO            standard       3h35m   Filesystem

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                 STORAGECLASS   REASON   AGE     VOLUMEMODE
persistentvolume/pvc-4570a5b6-6e12-48da-bb16-b9524e0a9dfd   10Gi       RWO            Delete           Bound    default/data-mysql-0                                  standard                3h35m   Filesystem
persistentvolume/pvc-b2fdc30d-ea7a-4b2b-9f07-016db25f03aa   10Gi       RWO            Delete           Bound    default/data-mysql-1                                  standard                3h35m   Filesystem
persistentvolume/pvc-c991093e-96b5-4844-9ed9-897824b3c029   10Gi       RWO            Delete           Bound    default/data-mysql-2                                  standard                3h35m   Filesystem
```

mysql-0,1,2というPodが3つ起動しました！それぞれ、172.17.0.10, 172.17.0.11, 172.17.0.12 というIPアドレスが割り当てられています。

さて、busyboxのpod作成して、名前解決を試してみます。

```
vagrant@minikube:~$ kubectl run -it busybox --restart=Never --rm --image=busybox sh
If you don't see a command prompt, try pressing enter.

/ # ping -c 3 mysql-service
PING mysql-service (172.17.0.11): 56 data bytes
64 bytes from 172.17.0.11: seq=0 ttl=64 time=0.366 ms
64 bytes from 172.17.0.11: seq=1 ttl=64 time=0.122 ms
64 bytes from 172.17.0.11: seq=2 ttl=64 time=0.049 ms

--- mysql-service ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.049/0.179/0.366 ms


/ # ping -c 3 mysql-service
PING mysql-service (172.17.0.10): 56 data bytes
64 bytes from 172.17.0.10: seq=0 ttl=64 time=0.049 ms
64 bytes from 172.17.0.10: seq=1 ttl=64 time=0.051 ms
64 bytes from 172.17.0.10: seq=2 ttl=64 time=0.054 ms

--- mysql-service ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.049/0.051/0.054 ms


/ # ping -c 3 mysql-service
PING mysql-service (172.17.0.12): 56 data bytes
64 bytes from 172.17.0.12: seq=0 ttl=64 time=0.040 ms
64 bytes from 172.17.0.12: seq=1 ttl=64 time=0.042 ms
64 bytes from 172.17.0.12: seq=2 ttl=64 time=0.080 ms

--- mysql-service ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.040/0.054/0.080 ms
```

mysql-serviceにpingを打ってみると、その都度ランダムにmysql-xのアドレスにアクセスしているのが分かります。


## 補足

### 各リソースの関連図
上で出来上がった各リソースの関連を図示してみると、こんなイメージでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/04cb2a65-0126-c53a-2b51-9674614003dd.png)


ステートフルセットでは、PodやVolumeが動的に作成されますが、各Podには固有の名前が付けられてVolumeもそれぞれのPod専用に割り当てられます。また、ステートフルセットを作成する際はHeadlessのサービス(clusterIP: Noneを指定したサービス)と関連付ける必要があります。
この例では、Headlessサービス以外に、もう一つClusterIPのサービスを定義しています。
また、上で使用したmysql-statefulset.yamlではvolumeClaimTemplates.spec.storageClassNameを明示指定していませんが、その場合、デフォルトのStorageClassが使われます。minikube環境の場合standardというStorageClassがデフォルトとして定義されており、ここから動的にVolumeがプロビジョニングされることになります。

```
vagrant@minikube:~/statefulset$ kubectl get storageclass
NAME                 PROVISIONER                AGE
standard (default)   k8s.io/minikube-hostpath   41d

vagrant@minikube:~/statefulset$ kubectl describe storageclass standard
Name:                  standard
IsDefaultClass:        Yes
Annotations:           storageclass.kubernetes.io/is-default-class=true
Provisioner:           k8s.io/minikube-hostpath
Parameters:            <none>
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Delete
VolumeBindingMode:     Immediate
Events:                <none>
```

### Serviceについての整理

DeploymentやStatefulsetなど、複数Podへのアクセスを考えた場合、手前にServiceを配置してそれを利用してアクセスすることになると思われますが、ClusterIP, NodePort, Headless辺り混乱してきたので理解を一旦整理します。

参考: [コンセプト - Service](https://kubernetes.io/ja/docs/concepts/services-networking/service/)

#### ClusterIP

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/f956ef81-857a-d68a-affb-228e497005a6.png)

ClusterIPのServiceを定義すると、代表IPアドレスが動的にアサインされて、サービスの名前でIPアドレスが解決できるよう内部DNSに登録される。
サービスの定義では、ターゲットのPodがListenしているポート番号と、公開用のポート番号の組を指定する(複数ポート指定可)。上の例だと、クライアントとなるPodからは、my-service:80にアクセスすると、ターゲットのPodの9376ポートにリクエストが割り振られることになる。
ClusterIPはk8s Cluster内で利用できるものなので、クラスター内のPodから利用することが前提(外部からのアクセスは不可)。
type指定を省略した場合デフォルトでClusterIPと認識される。

#### NodePort

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/b9887caa-92ce-b73b-0be0-a790d238987d.png)
NodePortのServiceを定義すると、各ノードのIPアドレス上の特定のポートを外部に公開することができる。
「nodePort: 30000」というように公開用のポートを明示指定しなければ、30000-32767の範囲で使用されていないポートが動的に割り当てられる。
k8s Cluster外のクライアントから、<各ノードのIP>:<公開用ポート番号>でアクセス可能。
各ノードで受け付けられたリクエストはkube-proxyによりMyAppのPodに負荷分散される(受け付けたノード以外のノードのPodにも分散される)。
※Podへのルーティングを行うために前述のClusterIPが内部的に作成される。

#### Statefulset と Headless Service
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/5c02f183-81ef-1004-b43e-b774746e4553.png)


Serviceの定義で"type: ClusterIP"、および、"clusterIP: None"を指定するとHeadless Serviceとして認識される。代表IPアドレスの付与や負荷分散は行われない。
サービス名で名前解決すると、各ターゲットのPodのアドレスがランダムに返される。
k8s Cluster内のクライアントとなるPodからは、my-service:3306で、ターゲットのいずれかのPodにアクセスすることになる。
Statefulsetを作成する場合、Headless Serviceの定義は必須。

参考: [Statefulset - Limitations](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#limitations)
>StatefulSets currently <font color="red">require a Headless Service</font> to be responsible for the network identity of the Pods. You are responsible for creating this Service.

## MySQLクライアントからの操作

MySQLクライアントのコンテナが稼働するPodを一時的に立ち上げて、上で作成したSQLサーバーにアクセスしてみます。

```
vagrant@minikube:~$ kubectl run mysql-client --image=mysql:5.7 -it --rm --restart=Never bash
If you don't see a command prompt, try pressing enter.

root@mysql-client:/# mysql -h mysql-service -e 'SELECT @@server_id,NOW()'
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2019-12-11 08:17:07 |
+-------------+---------------------+

root@mysql-client:/# mysql -h mysql-service -e 'SELECT @@server_id,NOW()'
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         102 | 2019-12-11 08:17:18 |
+-------------+---------------------+

root@mysql-client:/# mysql -h mysql-service -e 'SELECT @@server_id,NOW()'
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         101 | 2019-12-11 08:17:19 |
+-------------+---------------------+
```

MySQLのクライアントからmysqlコマンドで"mysql-service"というホスト名のMySQLサーバーにアクセスしてサーバーの情報を取得していますが、同じホスト名を指定してもサーバーIDが異なるものが返される、つまり、その都度別のPodのMySQLサーバーにアクセスしている様子が確認できます。

# イングレスの操作(non-HTTP/HTTPS)
Ingressは、Kubernetesクラスタにデプロイされたサービスを、クラスタの外部へ公開するためのリソースです。
TCP/IPベースの通信であれば、Ingressを用いて外部にサービスを公開できると思われますが、ほとんどのドキュメントが、HTTP/HTTPSの通信を前提に記述されているようです。恐らく、Kubernetesで管理するアプリケーションとしては、主としてWebアプリケーションやRESTfulなサービスを想定しているからだと思われます。
公式ドキュメントでも以下のような記述となっています。

[Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress)

>What is Ingress?
Ingress exposes HTTP and HTTPS routes from outside the cluster to services within the cluster. Traffic routing is controlled by rules defined on the Ingress resource.

また、Ingressの実体としてIngress Controllerという実装が必要になりますが、nginxを利用した[NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/)というものがOSSとして無料で提供されており、HTTP/HTTPSの公開はこれを利用することで実現できるようです。

ただ、訳あってHTTP/HTTPS以外の通信を扱いたいので(ゆくゆくは独自プロトコルを使用したミドルウェアをKubernetes環境で動かしたいので)、TCP/UDPの通信の公開方法を探ります。
ここでは、上でステートフルセットとして作成したMySQLのサービスを外部公開してKubernetesクラスター外のMySQLクライアントからKubernetes上のMySQLサーバーにアクセスする、というのを目指します。

ということで色々調べていると、ドンピシャで以下のような記事がありました！
[Exposing TCP and UDP Services via ingress on Minikube](https://skryvets.com/blog/2019/04/09/exposing-tcp-and-udp-services-via-ingress-on-minikube/)
先達あらまほしき事なり。
これをベースに進めます。

その他、参考
[Accessing Kubernetes Pods from Outside of the Cluster](http://alesnosek.com/blog/2017/02/14/accessing-kubernetes-pods-from-outside-of-the-cluster/)
[NGINX Ingress Controller - Exposing TCP and UDP services](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/)

## 全体像
やろうとしてることのイメージを図示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132331/5ede8a9e-ba63-1f4b-d85c-0fc05504b6ce.png)


※今回は、minikube環境で1ノード構成上で簡易的に試すために、NGINX Ingress Controller実装のPodの設定でhostNetwork:trueにすることで、このノードのアドレス上にポートを公開するようにしていますが、実際のKubernetesクラスター環境では、当然このPodがどのノードで動くか分からないし、シングルポイントにならないように構成しないといけません。
[NGINX Ingress Controller - Exposing TCP and UDP services](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/)にあるように、LoadBlancerタイプのService作って公開する、といったようなことを考える必要があると思われます。


## K8s Cluster側
### NGINX Ingress Controller準備

Ingress Controller実装のマニフェストファイルを入手 (Deploymentとして実装されているっぽい)。
https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml

```
vagrant@minikube:~/ingress$ wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
--2019-12-11 10:07:24--  https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.196.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.196.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6364 (6.2K) [text/plain]
Saving to: ‘mandatory.yaml’

mandatory.yaml                                              100%[===========================================================================================================================================>]   6.21K  --.-KB/s    in 0s

2019-12-11 10:07:25 (104 MB/s) - ‘mandatory.yaml’ saved [6364/6364]
```

```hostNetwork: true```を追記します。

```yaml:mandatory.yaml抜粋
...
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/part-of: ingress-nginx
      annotations:
        prometheus.io/port: "10254"
        prometheus.io/scrape: "true"
    spec:
      # wait up to five minutes for the drain of connections
      terminationGracePeriodSeconds: 300
      serviceAccountName: nginx-ingress-serviceaccount
      nodeSelector:
        kubernetes.io/os: linux
      hostNetwork: true
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1
          args:
            - /nginx-ingress-controller
            - --configmap=$(POD_NAMESPACE)/nginx-configuration
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx
            - --annotations-prefix=nginx.ingress.kubernetes.io
 ...
```

適用します。

```
vagrant@minikube:~/ingress$ kubectl apply -f mandatory.yaml
namespace/ingress-nginx created
configmap/nginx-configuration created
configmap/tcp-services created
configmap/udp-services created
serviceaccount/nginx-ingress-serviceaccount created
clusterrole.rbac.authorization.k8s.io/nginx-ingress-clusterrole created
role.rbac.authorization.k8s.io/nginx-ingress-role created
rolebinding.rbac.authorization.k8s.io/nginx-ingress-role-nisa-binding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created
deployment.apps/nginx-ingress-controller created
```

確認

```
vagrant@minikube:~/ingress$ kubectl get all -n ingress-nginx -o wide
NAME                                            READY   STATUS    RESTARTS   AGE   IP          NODE       NOMINATED NODE   READINESS GATES
pod/nginx-ingress-controller-5bbd46cd86-pk4ft   1/1     Running   0          79s   10.0.2.15   minikube   <none>           <none>

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS                 IMAGES                                                                  SELECTOR
deployment.apps/nginx-ingress-controller   1/1     1            1           79s   nginx-ingress-controller   quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1   app.kubernetes.io/name=ingress-nginx,app.kubernetes.io/part-of=ingress-nginx

NAME                                                  DESIRED   CURRENT   READY   AGE   CONTAINERS                 IMAGES                                                                  SELECTOR
replicaset.apps/nginx-ingress-controller-5bbd46cd86   1         1         1       79s   nginx-ingress-controller   quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1   app.kubernetes.io/name=ingress-nginx,app.kubernetes.io/part-of=ingress-nginx,pod-template-hash=5bbd46cd86
```

### MySQL用ポート公開用ConfigMap

マニフェストファイル作成

```yaml:configmap-tcp-services.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  3306: "default/mysql-service:3306"
```

適用

```
vagrant@minikube:~/ingress$ kubectl apply -f configmap-tcp-services.yaml
configmap/tcp-services configured
```

確認

```
vagrant@minikube:~/ingress$ netstat -an | grep 3306
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN
tcp6       0      0 :::3306                 :::*                    LISTEN
```

minikubeのノード上でnetstat見ると、3306ポートがListenされました！！！

## MySQLクライアント側

別のノードにMySQLクライアント環境を作ってアクセスしてみます。NFSサーバー用に用意したVirtualBox上のノード使うことにします。

### Ubuntu上にDockerCEインストール

以下のガイドに従ってDockerCEインストール
https://docs.docker.com/install/linux/docker-ce/ubuntu/

以下のコマンド順番に実行

```
vagrant@nfsserver:~$ sudo apt-get update

vagrant@nfsserver:~$ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

vagrant@nfsserver:~$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

vagrant@nfsserver:~$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

vagrant@nfsserver:~$ sudo apt-get update

vagrant@nfsserver:~$ sudo apt-get install docker-ce docker-ce-cli containerd.io

```

バージョン確認

```
vagrant@nfsserver:~$ docker version
Client: Docker Engine - Community
 Version:           19.03.5
 API version:       1.40
 Go version:        go1.12.12
 Git commit:        633a0ea838
 Built:             Wed Nov 13 07:50:12 2019
 OS/Arch:           linux/amd64
 Experimental:      false
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/version: dial unix /var/run/docker.sock: connect: permission denied
```

### MySQLクライアント用コンテナ実行

実行！(クライアントと言いつつサーバー上げちゃってますが、これのクライアントコマンドを使います。)

```
vagrant@nfsserver:~$ sudo docker container run -it --rm -e MYSQL_ROOT_PASSWORD=password mysql:5.7
```

確認

```
vagrant@nfsserver:~$ sudo docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                 NAMES
0523e710e118        mysql:5.7           "docker-entrypoint.s…"   About a minute ago   Up About a minute   3306/tcp, 33060/tcp   zealous_galileo
```

コンテナに接続してbashからMySQLサーバーにアクセスしてみます。
が、pingコマンド入ってなかったのでまずインストール

```
vagrant@nfsserver:~$ sudo docker exec -it 0523e710e118 bash

root@0523e710e118:/# apt-get update
...
root@0523e710e118:/# apt-get install iputils-ping net-tools
...
```

一応minikubeのノードのIPアドレスにpingが通るのを確認。

```
root@0523e710e118:/# ping -c 3 172.16.10.10
PING 172.16.10.10 (172.16.10.10) 56(84) bytes of data.
64 bytes from 172.16.10.10: icmp_seq=1 ttl=62 time=0.705 ms
64 bytes from 172.16.10.10: icmp_seq=2 ttl=62 time=0.785 ms
64 bytes from 172.16.10.10: icmp_seq=3 ttl=62 time=0.836 ms

--- 172.16.10.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.705/0.775/0.836/0.058 ms
```

では、いよいよMySQLサーバーにアクセス。

```
root@0523e710e118:/# mysql -h 172.16.10.10 -e 'SELECT @@server_id,NOW()'
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         102 | 2019-12-11 11:39:33 |
+-------------+---------------------+
root@0523e710e118:/# mysql -h 172.16.10.10 -e 'SELECT @@server_id,NOW()'
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         101 | 2019-12-11 11:39:36 |
+-------------+---------------------+
root@0523e710e118:/# mysql -h 172.16.10.10 -e 'SELECT @@server_id,NOW()'
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2019-12-11 11:39:39 |
+-------------+---------------------+
```

来ました！きちんと、リクエスト毎に違うサーバー(Pod)にアクセスしているのが確認できました！