学習用Kubernetes環境を作る2 動的PVが使えない環境でKubernetes管理WebUIのPortainer環境を作る/データ永続化/NodePortについて

## はじめに

**注意:今回作る環境は,初心者が学習用に作成したものです.**
**決して本番環境等,外部に公開するような重要な環境ではそのままの手順通りで行わないで下さい.**
**また,insecureな設定やベストプラクティスから外れた運用をしている箇所があります.**

学習用Kubernetes環境を作るの第2回です.

Part 1 [学習用Kubernetes環境を作る1 Ubuntu 20.04 LTS+kubeadm+containerd+CalicoなKubernetes環境をHyper-V上にTailscaleの固定IPで構築](https://qiita.com/Hyper-W/items/3b1dd5523eed42786fe8)

Part 2 [学習用Kubernetes環境を作る2 動的PVが使えない環境でKubernetes管理WebUIのPortainer環境を作る/データ永続化/NodePortについて](https://qiita.com/Hyper-W/items/e3b81da2bfc1a82c3c3f)

Part 3 [学習用Kubernetes環境を作る3 Kubernetes上にKubernetes用プライベートDockerレジストリHarbor環境を作る/httpなDockerレジストリをDocker・containerdで使う](https://qiita.com/Hyper-W/items/5b36a03eae172e53109f)

Kubernetes管理WebUIのPortainerをNodePortで公開,PersistentVolume(PV)のlocalでデータ永続化を行いHelmでデプロイします.

前提となるNodePortやデータ永続化の話は雑にざっくりとしますが,詳しくは他の記事を調べて下さい.

Kubernetesの環境は,[前回](https://qiita.com/Hyper-W/items/3b1dd5523eed42786fe8)構築した環境を前提としています.

## NodePortについて

Kubernetesで,ホストの外部にサービスを公開する方法は,NodePort/LoadBalancer/Ingressの3つがあります.

この内,NodePortとLoadBalancerはサービスタイプであり,Ingressはサービスの外側で動きます.

### NodePort

![NodePort.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/59fec6c6-1abf-70c4-1b6c-d636c20d922b.png)

NodePortは,30000～32767番号を割り当てることができ,デプロイされたPodがあるノードで指定されたポート番号がリッスンされます.

![NodePort-No-Access.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/c6ef1e3a-eb58-64b3-0148-8a71a94800da.png)


マルチノードでスケジューラー等によりPodが別のノードにデプロイされた場合,アクセスするためのIPアドレスが変更しなければならない,負荷分散し辛い等のデメリットがあります.

接続方法は**デプロイされたPodがあるノードのIP(またはDNS名):ポート番号**です.おそらく最も,簡単な公開方法であり,Dockerのように利用出来ます.

### LoadBalancer

![LoadBalancer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/c8b327b1-f56e-8014-7f3b-ba6daf65ec73.png)

LoadBalancerは,Kubernetesで最も標準的な公開方法です.クラウド環境では簡単に利用でき,ハンズオン等でも出てきます.オンプレな環境では,MetalLBがよく利用されます.サービスに対してExternal IPが割り当てられ,そのIPアドレスを利用して外部から接続が可能になります.

### Ingress

![Ingress.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/af2282ec-857f-c3a8-4717-7f690dca5dd0.png)


Ingressはサービスタイプではなく,全サービスの外側で動きます.様々なコントローラーがあり,コントローラーにより動作が異なります.

Ingressには,公開する複数のサービスを,パスでマッピングする事で公開可能になります.

公開するサービスのPod内も,アクセスのために指定されたパスと同じパスで転送されるので,Podもそれに合わせて変更する必要があるというデメリットがあります.

コントローラー自体にも,サービスが存在し,このサービスの公開方法をNodePort/LoadBalancer等にすることによってアクセス方法が異なります.

### NodePortのサンプルを動かす

サンプルのサービスをNodePortでデプロイします.

以下のファイルをtest.ymlとして保存します.

```yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
        - name: nginx
          image: nginx
---
apiVersion: v1
kind: Service
metadata:
  name: test-service
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 32767
      protocol: TCP
  selector:
    app: test
```

以下のコマンドでファイルの内容をデプロイします.

```bash
kubectl apply -f test.yml
```

以下のコマンドで,deploymentによりデプロイされたPodが正常に動作をしているか確認します.

```bash
kubectl get pod
```

以下のように,test-deploymentから始まるPodのSTATUSがRunningであれば,正常にPodが稼働しています.

```bash
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-5f6778868d-4zw82   1/1     Running   0          3m32s
```

次に,サービスを確認します.

```bash
kubectl get svc test-service 
```

以下のような,結果になります.

```bash
NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
test-service   NodePort   10.106.223.45   <none>        80:32767/TCP   14m
```

NodePortで設定したポート番号が割り当てられていることがわかります.

最後に実際に,ノードのIP:32767へ実際にブラウザでアクセスして確かめます.

![nginx-result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/68284fa0-0119-8c19-15bb-993d726e14d0.png)


アクセスして,上記のようになればOKです.

curlで確認する場合は,以下のようなコマンドになります.

```bash
curl ノードのIP:32767
```

以下のような結果になれば,OKです.

```html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

## データ永続化について

Kubernetesでは様々なデータ永続化方法がありますが,今回はPersistentVolume(PV)のlocalを利用したデータ永続化を行います.

PVを利用するには,PV以外にもStorage Class(SC),Persistent Volume Claim(PVC)が必要です.

それぞれ何かを説明するよりもサンプルを出した方が早いと思うのでサンプルを出して説明します.

以下のファイルをtest-pv.ymlとして保存します.

```yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: test-sc
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: test-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: test-sc
  local:
    path: /mnt/kubernetes/test
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - ノード名
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: test-sc
  resources:
    requests:
      storage: 1Gi
```

先程作成したtest.ymlのdeploymentを以下のように変えます.

```yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
        - name: nginx
          image: nginx
          volumeMounts:
            - mountPath: "/usr/share/nginx/html"
              name: test-data
      volumes:
        - name: test-data
          persistentVolumeClaim:
            claimName: test-pvc
```

PVCは本来どちらかというとtest.yml側に書くべきですが,都合上こうしています.

サンプルを見ればわかりますが,構造としては,アプリケーション(Deployment)側が,PVCを割り当て,PVCはSCにPVを要求し,SCは要求(PVC)に対して最適なPVを与え,PVはデータを保存する実体を指すものです.

PVは,動的プロビジョニングにより自動生成する方が一般的なようですが,今回採用したlocalでは動的プロビジョニングをサポートしていないため,手動で作成する必要があります.

オンプレ環境では,local以外だと動的プロビジョニングが利用可能なNFS等を使うことになると思いますが,面倒なので一番楽なlocalを使用します.

また,localの制限として他には,1つのPVに対して1つのPodにしか割り当て出来ず,アクセスモードは常にReadWriteOnce固定であるということです.

このため,kindをPodにして使うか,DeploymentとStatefulSetではreplicasを1にして使わないといけません.

尚,DeploymentでPodを削除して,Podが再作成された場合は,特に問題なく動きます.

なぜ,このような回りくどい割り当て方をしているのかは,Kubernetes管理者とアプリケーション開発者を考えるとわかりやすいです.

![PV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/cd4730db-9b16-5b03-9dab-8abc46cc34ed.png)

5GBのPVをPodに割り当てることを考えるとこうなります.

![ex-PV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743928/e7d1f059-d262-496e-b673-dca7108a3dfe.png)


このように抽象化していれば,管理者はSCと複数のPV(動的プロビジョニングが使えない環境等)を用意するだけでよく,開発者はデータ永続化をしたいアプリケーションがあれば,それをSCに要求するPVCとアプリケーションに永続化するPVCの設定をするだけでいいのです.

そうすれば,要求された容量以上で一番近い容量のPVをSCが自動的に割り当ててくれます.

では,applyにして実際にデプロイします.

まずは,実体のディレクトリを作成します.

```bash
sudo mkdir -p /mnt/kubernetes/test

sudo chmod 777 /mnt/kubernetes/test
```

test-pv.ymlをapplyします.

```bash
kubectl apply -f test-pv.yml
```

デプロイ出来たか確認します.

SCを確認します.

```bash
kubectl get sc test-sc
```

以下のようになっていれば,問題ありません.

```bash
NAME                 PROVISIONER                    RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
test-sc              kubernetes.io/no-provisioner   Delete          WaitForFirstConsumer   false                  2m19s
```

PVを確認します.

```bash
kubectl get pv test-pv
```

以下のようになっていれば,問題ありません.

STATUSはAvailableです.

```bash
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS       CLAIM              STORAGECLASS   REASON   AGE
test-pv   1Gi        RWO            Retain           Available    default/test-pvc   test-sc                 24m
```

PVCを確認します.

```bash
kubectl get pvc test-pvc
```

STATUSはPendingです.

```bash
NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Pending                                      test-sc        5m7s
```

test.ymlをapplyして更新します.

```bash
kubectl apply -f test.yml
```

Podが正常に再作成され動いているか確認します.

```bash
kubectl get pod
```

Pod名が変更されているか,AGEがリセットされているか確認します.

```bash
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-56bddb69d5-rc7c9   1/1     Running   0          53s
```

PVCの状態を確認します.

```bash
kubectl get pvc test-pvc
```

PendingからBoundに変わりました.

```bash
NAME       STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Bound    test-pv   1Gi        RWO            test-sc        20m
```

PVの状態を確認します.

```bash
kubectl get pv test-pv
```

AvailableからBoundに変わりました.

```bash
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
test-pv   1Gi        RWO            Retain           Bound    default/test-pvc   test-sc                 24m
```

マウントされているか確認するために,実体のディレクトリ(/mnt/kubernetes/test)にindex.htmlを作成し,変化を確認します.

htmlファイルは以下のようにします.

```html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello</title>
</head>

<body>
    Hello PV!!
</body>

</html> 
```

実際にブラウザでアクセスして,Hello PV!!と表示されるか,curlコマンドで上記htmlと同じ結果が帰って来れば成功です.

テストが完了すれば,これらは不要なので,削除します.

```bash
kubectl delete -f test.yml

kubectl delete -f test-pv.yml

sudo rm -rf /mnt/kubernetes/test
```

## Portainerをデプロイ

これらを理解したところで,本題のPortainerをデプロイします.

https://documentation.portainer.io/v2.0/deploy/ceinstallk8s/

基本的に,公式の手順通りですが,公式の手順通りでは,この環境ではデプロイ出来ません.

公式では,デフォルトのSCと諸アクセス権限があれば良いというように書かれていますが,これは正確ではなく,動的プロビジョニングが有効なSCが必要です(指定すれば,デフォルトである必要もない).

動的プロビジョニングが有効でない場合は,10GB以上ののPVが必要です.

### データの保存先を作成

上記を踏まえて,まずPortainerのデータを保存ディレクトリを作成します.

```bash
mkdir /mnt/kubernetes/portainer

chmod 777 /mnt/kubernetes/portainer
```

### SC・PVのymlを作成

次に,SCとPVを作成します.

```yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: portainer-sc
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: portainer-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: portainer-sc
  local:
    path: /mnt/kubernetes/portainer
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - ノード名
```

今回このSCは,Portainer専用としています.

必要であれば,名前を変えたり,PVと別ファイルに分けて下さい.

### デフォルトSCを利用する場合

ここで,デフォルトのSCを利用したい場合,SCの部分を以下のようにするか,

```yml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: portainer-sc
  annotations: { "storageclass.kubernetes.io/is-default-class": "true" }
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
```

公式順のように,以下のようなコマンド等で,デフォルトSCにして下さい.

```bash
kubectl patch storageclass portainer-sc -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
```

以下のようなコマンドで,

```bash
kubectl get sc portainer-sc
```

以下のように,defaultとなっていれば問題ありません.

```bash
NAME                      PROVISIONER                    RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
portainer-sc (default)   kubernetes.io/no-provisioner   Delete          WaitForFirstConsumer   false                  15s
```

### SC・PVのymlをapply

先程作成したymlをデプロイします.

```bash
kubectl apply -f portainer-vol.yml
```

正常にデプロイできたか確認します.

SCを確認します.

```bash
kubectl get sc portainer-sc
```

以下のようになっていれば,問題ありません.

```bash
NAME                 PROVISIONER                    RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
portainer-sc              kubernetes.io/no-provisioner   Delete          WaitForFirstConsumer   false                  2m19s
```

PVを確認します.

```bash
kubectl get pv portainer-pv
```

以下のようになっていれば,問題ありません.

STATUSはAvailableです.

```bash
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS       CLAIM                 STORAGECLASS   REASON   AGE
portainer-pv   10Gi       RWO            Retain           Available    portainer/portainer   portainer-sc            3m7s
```

### Helmを利用してインストール

Helmリポジトリを追加してインストールします.

```bash
helm repo add portainer https://portainer.github.io/k8s/

helm install --create-namespace -n portainer portainer portainer/portainer --set persistence.storageClass=portainer-sc
```

デフォルトSCを使用する場合,--set persistence.storageClass=portainer-scは不要です.

また,service.typeでサービスタイプを指定できます.

Portainerはportainerのnamespaceにデプロイされます.

Podが正常に稼働しているか確認します.

```bash
kubectl -n portainer get pod
```

以下のように,READYが1/1で,STATUSがRunningであれば,問題ありません.

```bash
NAME                         READY   STATUS    RESTARTS       AGE
portainer-59f798c579-pdhm9   1/1     Running   0              2m19s
```

一応,PVも確認します.

```bash
kubectl get pv portainer-pv
```

Availableが,Boundに変わりました.

```bash
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE
portainer-pv   10Gi       RWO            Retain           Bound    portainer/portainer   portainer-sc            4m7s
```

後は,

```
http://ノードIP(orDNS):30777
```

にブラウザでアクセスできれば,成功です.

アクセスすれば,アカウント作成->接続方法の選択(今回はKubernetes)->設定になると思います.

設定でmetrics-serverと連携すれば,リソースの監視等も強化出来ます.

Dockerも管理出来るので,Dockerを使っている人はTLSでDockerソケットを公開するなどして,エンドポイントに追加するといいと思います. ~~(Dockerの管理UIとしての方が優秀)~~

## Next

[次](https://qiita.com/Hyper-W/items/5b36a03eae172e53109f)