Kubernetes Service カテゴリのハンズオンを輪読会でやってみた

## はじめに
皆さんこんにちはasmgです。普段社内の新卒有志でKubernetes完全ガイド輪読会（勉強会）をおこなっています。
その中で、Kubernetes Serviceカテゴリのハンズオンを行なったのでその内容をまとめていきたいと思います。

## 本記事の対象者
* 本記事では、Kubernetes完全ガイド第2版の5章までを読み進めている人を対象としています。
  * 完全ガイドを読んでいなくとも、Deploymentリソースがわかっていれば基本的に問題はありません。

※ 本記事は、KubernetesのService カテゴリーの挙動を確認する記事になります。なので、Serviceカテゴリについては説明を書きますが、他のリソースについては説明しません。

## 環境
* 検証機
  * Mac Book pro 14inch M1 Pro
* 検証環境
  * kind（Kubernetes in Docker）

## サービスカテゴリとは？
:::note info
Podの集合で実行されているアプリケーションをネットワークサービスとして公開する抽象的な方法です。
Kubernetesでは、なじみのないサービスディスカバリーのメカニズムを使用するためにユーザーがアプリケーションの修正をする必要はありません。 KubernetesはPodにそれぞれのIPアドレス割り振りや、Podのセットに対する単一のDNS名を提供したり、それらのPodのセットに対する負荷分散が可能です。
ref: [Service | Kubernetes](https://kubernetes.io/ja/docs/concepts/services-networking/service/)
:::
## 今回ハンズオンで扱うもの
今回は、以下の2つのサービスカテゴリをハンズオンで扱います。
* ClusterIP
* NodePort
### 今回のKubernetes環境の構成図と環境の構築方法

```yaml:cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
```

```terminal:terminal
➜ kind create cluster --config cluster.yaml -n serivce-test
```

### 受け元のKubernetesマニフェストファイル
```terminal:ディレクトリ構造
├── employer
│   ├── configmap.yaml
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── namespace.yaml
```

```yaml:namespace.yaml
kind: Namespace
apiVersion: v1
metadata:
  name: employer
```

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: employer-deployment
  namespace: employer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: employer-app
  template:
    metadata:
      labels:
        app: employer-app
    spec:
      containers:
      - name: employer-deployment
        image: nginx
        volumeMounts:
          - {name: curl-sh-cluster, mountPath: /curl-cluster.sh ,subPath: curl-cluster.sh}
          - {name: curl-sh-node, mountPath: /curl-node.sh ,subPath: curl-node.sh}
      volumes:
        - {name: curl-sh-cluster, configMap: {name: curl-config, items: [{key: curl-bash-cluster, path: curl-cluster.sh}]}}
        - {name: curl-sh-node, configMap: {name: curl-config, items: [{key: curl-bash-node, path: curl-node.sh}]}}

```

```yaml:configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: curl-config
  namespace: employer
data:
  curl-bash-cluster: |
    #!/bin/sh
    for ((i=0; i < 10; i++)); do
      curl -w "\n" http://cluster-service.clusterip.svc.cluster.local:5000
    done
  curl-bash-node: |
    #!/bin/sh
    for((i=0; i < 10; i++)); do
      curl -w "\n" http://serivce-test-worker:30000
    done

```

```yaml:kustomization.yaml
resources:
  - deployment.yaml
  - namespace.yaml
  - configmap.yaml
```

受け元のKubernetesリソースのconfigmapでスクリプトを2つ作っていますが、これは今後ClusterIPとNodePortの動作確認をする際に使います。
`curl-cluster.sh`と`curl-node.sh`というスクリプトを作成していますが、スクリプトの具体的な処理の内容は、特定のエンドポイントにcurlを10回叩くという処理になります。


#### 導入

```
➜ kubectl apply -k employer/
```

### 今回検証で使うアプリケーションのサンプルコード
```terminal:アプリケーションのディレクトリ構造
├── app
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt
```
```python:app.py
from flask import Flask
import socket

app = Flask(__name__)

@app.route("/")
def index_page():
    return socket.gethostbyname(socket.gethostname())

if __name__ == "__main__":
    app.run(host="0.0.0.0")
```

```:requirements.txt
click==8.1.3
Flask==2.2.2
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
Werkzeug==2.2.2
```

```Dockerfile:Dockerfile
FROM python:3.10
WORKDIR /
COPY app.py requirements.txt ./
RUN pip install -r requirements.txt
CMD ["python","app.py"]
```
今回は検証用のアプリケーションをFlask(Pythonのウェブアプリケーションフレームワーク)で作成しました。
アプリケーションの処理は、クライアントがアプリケーションにアクセスするとサーバーのIPをレスポンスするようになっています。

#### コンテナをkindに登録する

```
➜ kind load docker-image test-python:latest -n serivce-test
```

## Kubernetes Service カテゴリーのハンズオンをやってみる


### ClusterIPのハンズオン

#### ClusterIPとは？
:::note info
<b>ClusterIPとは？</b>

クラスター内部のIPでServiceを公開する。このタイプではServiceはクラスター内部からのみ疎通性があります。
ref: [Service ClusterIP](https://kubernetes.io/ja/docs/concepts/services-networking/service/#publishing-services-service-types)
* 一般的なClusterIPのイメージ(App Aにする場合)
![monorepo-EKS-testing - 2ページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567360/d38a957f-da0f-987f-b4f4-c4083eeca064.png)

* ClusterIPのFQDN
  * [service name].[namespace].svc.cluster.local:[port] 

:::

本記事で行うハンズオンでは、以下2つのパターンを確認したいと思います。
1. パラメータを設定しない場合(default)
2. sessionAffinityを有効にした場合

:::note info
<b>sessionAffinityパラメータをClusterIPで検証する理由</b>
基本的にKubernetesクラスターでPodにアクセスする際、ClusterIPを経由してPodにアクセスします。なので、`sessionAffinity`パラメータはClusterIPで検証するのが良いと考え検証しています。
:::

#### manifestの作成

本manifestは、ClusterIP検証namespace clusteripに作成します。
※ディレクトリ構造に関しては以下に示します。

```terminal:ディレクトリ構造
├── cluster_ip
│   ├── service.yaml
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── namespace.yaml
```


##### 1 . パラメータを設定しない場合(default)

```yaml:namespace.yaml
kind: Namespace
apiVersion: v1
metadata:
  name: clusterip
```

```yaml:kustomization.yaml
resources:
  - deployment.yaml
  - namespace.yaml
  - service.yaml
```

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cluster-deployment
  namespace: clusterip
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cluster-app
  template:
    metadata:
      labels:
        app: cluster-app
    spec:
      containers:
      - name: cluster-deployment
        image: test-python
        imagePullPolicy: IfNotPresent
```

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: cluster-service
  namespace: clusterip
spec:
  type: ClusterIP
  ports:
  - name: cluster-service
    protocol: "TCP"
    port: 5000
    targetPort: 5000
  selector:
    app: cluster-app
```


##### 2 . sessionAffinityを有効にした場合

今回は、ClusterIPのパラメーター`sessionAffinity`を入れてみようと思います。
1 . パラメータを設定した場合のmanifestをベースにservice.yamlにだけ修正を入れます。

```diff_yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: cluster-clusterip
  namespace: clusterip
spec:
  type: ClusterIP
  ports:
  - name: cluster-service
    protocol: "TCP"
    port: 5000
    targetPort: 5000
  selector:
    app: cluster-app
+  sessionAffinity: ClientIP
+  sessionAffinityConfig:
+    clientIP:
+      timeoutSeconds: 1
```

<b>sessionAffinityとは？</b>
sessionAffinityとは、`timeoutSeconds`で指定した秒数間同じPodsにリクエストを流すことができます。
ref:[IPVSプロキシーモード](https://kubernetes.io/ja/docs/concepts/services-networking/service/#proxy-mode-ipvs)

#### 動作の確認

##### 1 . パラメータを設定しない場合

動作確認は動作確認用に作ったPodから`パラメータを設定しない場合`のパターンで作ったClusterIPにアクセスをしてみます。
今回は事前にスクリプトを作成しているのでそちらを使ってアクセスしてみます。

`パラメータを設定しない場合`のパターンで作ったmanifestをapplyし、動作を確認します。

```terminal:パラメータを設定しない場合のパターンで作ったmanifestをapply
➜ kubectl apply -k cluster_ip/
```

```terminal:ClusterIPの検証
❯ kubectl get pods -n clusterip -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP           NODE                   NOMINATED NODE   READINESS GATES
cluster-deployment-6785dbc954-2wf64   1/1     Running   0          13h   10.244.2.5   serivce-test-worker2   <none>           <none>
cluster-deployment-6785dbc954-wxfn7   1/1     Running   0          13h   10.244.1.5   serivce-test-worker    <none>           <none>

➜ kubectl get pods -n employer
NAME                                   READY   STATUS    RESTARTS   AGE
employer-deployment-54bf65f757-rtmrn   1/1     Running   0          13h

# employer-deployment-54bf65f757-rtmrn のpodsにアクセスしてスクリプトを叩きます。
➜ kubectl exec -it employer-deployment-54bf65f757-rtmrn -n employer bash
root@employer-deployment-54bf65f757-rtmrn:/# bash curl-cluster.sh
10.244.2.5
10.244.1.5
10.244.2.5
10.244.2.5
10.244.1.5
10.244.1.5
10.244.1.5
10.244.1.5
10.244.2.5
10.244.1.5
```
実際に、ClusterIPのエンドポイントを叩くと各nodeにpodsに分散されてアクセスがされていることがわかります。

##### 2 . sessionAffinityを有効にした場合
動作確認は動作確認用に作ったPodから`sessionAffinityを有効にした場合`のパターンで作ったClusterIPにアクセスをしてみます。
今回も同様事前にbashを作成しているのでそちらを使ってアクセスしてみます。

`sessionAffinityを有効にした場合`のパターンで作ったmanifestをapplyし、動作を確認します。

```terminal:sessionAffinityを有効した場合のパターンで作ったmanifestをapply
➜ kubectl apply -k cluster_ip/
```

```terminal:terminal
❯ kubectl get pods -n clusterip -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP           NODE                   NOMINATED NODE   READINESS GATES
cluster-deployment-6785dbc954-2wf64   1/1     Running   0          13h   10.244.2.5   serivce-test-worker2   <none>           <none>
cluster-deployment-6785dbc954-wxfn7   1/1     Running   0          13h   10.244.1.5   serivce-test-worker    <none>           <none>

➜ kubectl get pods -n employer
NAME                                   READY   STATUS    RESTARTS   AGE
employer-deployment-54bf65f757-rtmrn   1/1     Running   0          13h

# employer-deployment-76d7784fc7-nv8gg のpodsにアクセスしてスクリプトを叩きます。
➜ kubectl exec -it employer-deployment-54bf65f757-rtmrn -n employer bash
root@employer-deployment-54bf65f757-rtmrn:/# bash curl-cluster.sh
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
10.244.2.5
```
`sessionAffinity: ClientIP`を有効にするとsessionAffinityで設定した秒数間(`timeoutSeconds`)同じPodに振り分けられていることがわかります。

:::note info
<b>sessionAffinityが有効なパターン</b>
sessionAffinityを有効にすると、timeoutSecondsに設定した時間の間同じPodにアクセスが流れます。
セッションを意識したい通信を設計したい場合には有効です。
:::

### NodePortのハンズオン
#### NodePortとは？
:::note info
<b>Nodeportとは？</b>
各NodeのIPにて、静的なポート(NodePort)上でServiceを公開します。そのNodePort のServiceが転送する先のClusterIP Serviceが自動的に作成されます。NodeIP:NodePortにアクセスすることによってNodePort Serviceにアクセスできるようになります。
ref: [Service NodePort](https://kubernetes.io/ja/docs/concepts/services-networking/service/#publishing-services-service-types)

* 一般的なNodePortのイメージ
![monorepo-EKS-testing - 2ページ (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567360/47fc722f-ddc3-f30f-0b69-c35b69ee3c18.png)




* NodePortのFQDN
  * [node-name] : [port]

* ※このFQDNはKubernetes内部からからNodeにアクセスしたい場合に使うエンドポイントになります。
  * 外部からアクセスしたい場合には、[node ip] : [port]になります。
:::


前章でClusterIP、本章でNodePortをまとめているので一応ここでざっくりとClusterIPとNodePortの違いをまとめたいと思います。

:::note info
<b>ClusterIPとNodePortの違い</b>
* ClusterIPはKubernetes内部からアクセスされるServiceカテゴリ
* NodePortはKubernetes外部からアクセスされるServiceカテゴリ

:::


#### manifestの作成

本manifestは、NodeIP検証namespace nodeportに作成します。

```terminal:ディレクトリ構造
├── node_port
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   ├── namespace.yaml
│   └── service.yaml
```


```yaml:namespace.yaml
kind: Namespace
apiVersion: v1
metadata:
  name: nodeport
```

```yaml:kustomization.yaml
resources:
  - deployment.yaml
  - namespace.yaml
  - service.yaml
```

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodeport-deployment
  namespace: nodeport
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nodeport-app
  template:
    metadata:
      labels:
        app: nodeport-app
    spec:
      containers:
      - name: recipient-deployment
        image: test-python
        imagePullPolicy: IfNotPresent
```

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nodeport-service
  namespace: nodeport
spec:
  type: NodePort
  ports:
  - name: nodeport-service
    protocol: "TCP"
    port: 5000
    targetPort: 5000
    nodePort: 30000
  selector:
    app: nodeport-app
```

#### 動作の確認


```terminal:terminal
➜ kubectl get pods -n nodeport -o wide
NAME                                   READY   STATUS    RESTARTS   AGE   IP           NODE                   NOMINATED NODE   READINESS GATES
nodeport-deployment-6879847b7d-4fcwm   1/1     Running   0          13h   10.244.1.3   serivce-test-worker    <none>           <none>
nodeport-deployment-6879847b7d-k4pc9   1/1     Running   0          13h   10.244.2.2   serivce-test-worker2   <none>           <none>

➜ kubectl get pods -n employer
NAME                                   READY   STATUS    RESTARTS   AGE
employer-deployment-54bf65f757-rtmrn   1/1     Running   0          13h

# employer-deployment-54bf65f757-rtmrn のpodsにアクセスしてスクリプトを叩きます。
➜ kubectl exec -it employer-deployment-54bf65f757-rtmrn -n employer bash
root@employer-deployment-54bf65f757-rtmrn:/# bash curl-node.sh
10.244.2.2
10.244.1.3
10.244.2.2
10.244.2.2
10.244.2.2
10.244.2.2
10.244.2.2
10.244.1.3
10.244.2.2
10.244.2.2
```

nodeに向けてcurlを叩くとNodeIPを介してclusterにアクセスしCluster IPを通じて各NodeのPodsにアクセスが流れていることがわかります。

## まとめ
今回は、Kubernetes Serviceカテゴリのハンズオンで行った内容をまとめてみました。
やはり手を動かして動作を確認してみると理解が深まるなという気がしました。
本記事を読むことでServiceカテゴリを学習されている皆さんの解像度が少しでも上がると嬉しいです。