こんにちは。KOUKIです。
とある企業でWeb系のシステム開発業務に従事しています。
今回は、前回学習したKubernetesの学習の続きを記事にしています。今回は、Volumeについて触れていきたいと思います。
ワークスペースの準備
1 2 3 4 5 6 7 8 9 |
touch Dockerfile touch docker-compose.yml touch deployment.yml touch service.yml mkdir story touch story/text.txt touch main.go go mod init k8s go get github.com/labstack/echo/v4 |
アプリケーションの準備
検証で使うアプリケーションは、Go言語で実装します。
簡単に説明すると、GET/POSTリクエストの受付を可能にしたEchoサーバーを作成しています。このEchoサーバーは/storyパスからのリクエストを裁くことが可能であり、GETリクエストの場合はローカルに保存されたデータを返却し、POSTリクエストの場合はローカルに新しいデータを追加保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
package main import ( "io/ioutil" "net/http" "os" "strings" "github.com/labstack/echo" ) const ( filePath = "./story/text.txt" ) // GET用の送信データ type getSendMsg struct { Story string `json:"story" query:"story"` } // POST用の受信データ type postReciveMsg struct { Text string `json:"text" query:"text"` } // POST用の送信データ type postSendMsg struct { Message string `json:"message" query:"message"` } func main() { // https://github.com/labstack/echo e := echo.New() // Routes e.GET("/story", getHandler) e.POST("/story", postHandler) // Start Server e.Logger.Fatal(e.Start(":8080")) } func getHandler(c echo.Context) error { bytes, err := ioutil.ReadFile(filePath) if err != nil { return c.JSON(http.StatusInternalServerError, "Failed to open file") } gs := getSendMsg{ Story: string(bytes), } return c.JSON(http.StatusOK, gs) } func postHandler(c echo.Context) error { t := new(postReciveMsg) // bindに失敗or入力文字が0の場合はエラーを返す if err := c.Bind(t); err != nil || strings.TrimSpace(t.Text) == "" { return c.JSON(http.StatusUnprocessableEntity, "Text must not be empty!") } // ファイルに追加書き込み f, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { return c.JSON(http.StatusInternalServerError, "Storing the text failed.") } defer f.Close() f.WriteString(t.Text + "\n") ps := postSendMsg{ Message: "Text was stored!", } return c.JSON(http.StatusCreated, ps) } |
Dockerの準備
Dockerfileとdocker-compose.ymlファイルを用意しましょう。
1 2 3 4 5 6 7 8 9 10 |
# Dockerfile FROM golang:1.15-alpine WORKDIR /app COPY . . EXPOSE 8080 CMD ["go", "run", "main.go"] |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# docker-compose.yml version: "3" services: stories: build: . volumes: - stories:/app/story ports: - 80:8080 volumes: stories: |
volumesを作成することで、データの永続化を可能にしています。
アプリケーションの挙動
以下のコマンドにて、アプリケーションを起動しましょう。
1 |
docker-compose up |
アプリケーションが立ち上がったら「http://localhost/story」に対してGETリクエストを送ってみましょう。筆者は、ChromeのTalend API Testerを使ってテストしてます。

リクエストが成功(200)して、空文字が返却されました。
続いて、「http://localhost/story」に対して、POSTリクエストでJSON文字列を送ってみましょう。
1 2 3 4 |
// JSON文字列 { "text": "Hello World" } |

OKですね。もう一度、GETリクエストを送信します。

今度は、返り値にHello Worldが表示されています。
書き込んだデータは、ローカルPCに永続保存できるようにしたので、コンテナが削除されても消えないはずです。確かめてみましょう。
1 2 3 4 5 |
# Ctrl + c でコンテナを停止する # コンテナを削除する docker container prune # コンテナを立ち上げる docker-compose up |
先ほどと同じように「http://localhost/story」に対してGETリクエストを送ってみましょう。
以下のように「Hello World」が表示されたら成功です。

Kubernetes(k8s)環境構築
DeploymentとServiceを作成
先ほど作成したアプリケーションを元にDeploymentとServiceを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# deployment.yml apiVersion: apps/v1 kind: Deployment metadata: name: story-deployment # deploymentの名前 spec: replicas: 1 selector: matchLabels: app: story # deploymentが管理するPodの名前 template: metadata: labels: app: story # podの名前 spec: containers: - name: story # podがDockerhubにあるimageタグ↓に指定したイメージで動くことを指定 image: アカウント名/kub-data-demo resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 8080 |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# service.yml apiVersion: v1 kind: Service metadata: name: story-service spec: selector: app: story # podの名前を指定 ports: - port: 80 protocol: "TCP" targetPort: 8080 type: LoadBalancer |
Docker Hub上でレポジトリ作成
次に、先ほど作成したコンテナイメージをDocker HubにPushします。
そのためには、最初にDocker Hub上で新しいレポジトリを作成しましょう。

筆者の場合は「kub-data-demo」レポジトリを作成しました。これはdeployment.ymlに指定した「image: アカウント名/kub-data-demo」と同じものです。※アカウントには、ご自身のアカウント名を入れてください
Docker HubにイメージをPush
レポジトリ作成が完了したら下記のコマンドで、Docker HubにイメージをPushします。
1 2 3 4 |
# イメージをビルド docker build -t アカウント名/kub-data-demo . # docker hubにpush docker push アカウント名/kub-data-demo |
デプロイ
色々事前準備が多くて大変でしたが、いよいよDocker HubにPushしたイメージをminikube(k8s クラスタ)上にデプロイします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# minikubeをスタート $ minikube start # minikubeを構築していない場合は、以下のコマンド # $ minikube start --driver=virtualbox # status確認 $ minikube status minikube type: Control Plane host: Running kubelet: Running apiserver: Running kubeconfig: Configured # 念のため、関係ないdeploymentsが動いていないか確認する $ kubectl get deployments No resources found in default namespace. # serviceとdeploymentをapplyする $ kubectl apply -f=service.yml -f=deployment.yml service/story-service created deployment.apps/story-deployment created $ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE story-deployment 1/1 1 1 82s # アプリを起動 $ minikube service story-service |-----------|---------------|-------------|-----------------------------| | NAMESPACE | NAME | TARGET PORT | URL | |-----------|---------------|-------------|-----------------------------| | default | story-service | 80 | http://192.168.99.100:31674 | |-----------|---------------|-------------|-----------------------------| 🎉 Opening service default/story-service in default browser... |
「minikube service story-service」コマンドを実行するとアプリケーションが起動します。※Podを作るまで多少時間がかかるかもしれません

アプリケーションの挙動
筆者の環境では、「http://192.168.99.100:30800/」にてアプリケーションが立ち上がったので、Dockerコンテナ時に確認した時と同じ方法で、アプリケーションが動作するか確認します。



アプリケーションは、問題なく動いていますね。しかし、これには一つ欠点があります。
以下のコマンドで、deploymentとserviceを一度削除してから再度立ち上げてください。
1 2 3 4 5 6 7 8 9 10 |
$ kubectl delete -f=deployment.yml -f=service.yml deployment.apps "story-deployment" deleted service "story-service" deleted $ kubectl apply -f=deployment.yml -f=service.yml deployment.apps/story-deployment created service/story-service created アプリにアクセスする $ minikube service story-service |
アプリケーションが立ち上がったら、「http://192.168.99.100:32170/story」にGETリクエストを送信します。

すると「story: “”」がかえってきました。データが永続化されていないのです。
Kubernetes Volumes
docker コンテナの時は、docker-compose.ymlにvolumesを指定してデータを永続化しました。
k8sにもvolumesの概念があります。
今回もvolumeを使ってデータの永続化をしましょう。
errorエンドポイントを追加
データの永続化の前にやることがあります。
現状、Podを削除しないと動作(データの存在有無)確認ができないので、アプリケーションにerrorエンドポイントを追加して、GetリクエストでPodを破壊できるようにしましょう。
破壊しても大丈夫なの?と思われるかもしれませんが、k8sが自動で再作成されるのでOKです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func main() { ... // Routes e.GET("/story", getHandler) e.POST("/story", postHandler) e.GET("/error", errHandler) // 追加 ... } // 追加 func errHandler(c echo.Context) error { log.Fatal("destruction app") return nil } |
ファイルを修正したのでイメージを作り直して、Docker HubにPushする必要があります。
1 2 |
docker build -t アカウント名/kub-data-demo:1 . docker push アカウント名/kub-data-demo:1 |
次にdeployment.ymlのimageの指定を変更します。
1 2 3 4 |
spec: containers: - name: story image: アカウント名/kub-data-demo:1 <<< 先ほど作成したイメージに変更 |
最後に以下のコマンドでアプリケーションを更新します。※既存のPodは削除しなくて大丈夫です。更新してくれます。
1 2 3 4 |
kubectl apply -f=service.yml -f=deployment.yml # アプリを立ち上げる minikube service story-service |
アプリが立ち上がったら「/error」にアクセスして、Podが破壊されるか確認しましょう。

1 2 3 |
$ kubectl get pods NAME READY STATUS RESTARTS AGE story-deployment-75c6c4f5d8-bjvh2 0/1 Error 2 118s |
OKですね。しばらくすると自動修復されます。
1 2 3 |
$ kubectl get pods NAME READY STATUS RESTARTS AGE story-deployment-75c6c4f5d8-bjvh2 1/1 Running 3 2m25s |
emptyDir Volume
k8sのvolumesには色々なTypeがありますが、最初にemptyDirを試してみましょう。
このvolumeは、WorkerNodeにPodが割り当てられた時に最初に作成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
spec: containers: - name: story # podがDockerhubにあるimageタグ↓に指定したイメージで動くことを指定 image: アカウント名/kub-data-demo:1 imagePullPolicy: Always # 常に新しいイメージを使うように設定 volumeMounts: # 追加 - mountPath: /app/story # アプリ内のデータ保存場所を指定 name: story-volume # volumesの名前を指定 resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 8080 volumes: # 追加 - name: story-volume emptyDir: {} |
emptyDirは空volumeを作成するので、/storyにアクセスした時は、「”Failed to open file”」が表示されますが、データを格納すると回避できるので問題ありません。



それでは、/errorにアクセスしてPodを破壊後、データが永続化されるか確認しましょう。

podが立ち上がったら/storyにアクセスします。
1 2 3 4 5 6 7 8 9 |
# crashしているが... $ kubectl get pods NAME READY STATUS RESTARTS AGE story-deployment-777754d46-rc6tb 0/1 CrashLoopBackOff 1 4m26s # そのうちrunningに戻る $ kubectl get pods NAME READY STATUS RESTARTS AGE story-deployment-777754d46-rc6tb 1/1 Running 2 4m36s |

OKですね。よかった!
emptyDirは、Pod内に指定したマウント先に空のvolumeを作成します。その際に、マウント先に格納されたファイル類は削除されるようです。
このアプリケーションの例で言えば、storyディレクトリ配下のtext.txtは一旦削除されます。
しかし、emptyDirは、Pod数を増やすと期待した動きをしてくれません。例えばPodを2つにしてみましょう。
1 2 3 |
# deployment.yml spec: replicas: 2 |
1 2 3 4 5 6 7 |
$ kubectl apply -f=deployment.yml deployment.apps/story-deployment configured $ kubectl get pods NAME READY STATUS RESTARTS AGE story-deployment-777754d46-7kv5j 1/1 Running 0 11s story-deployment-777754d46-rc6tb 1/1 Running 6 34h |
コンテナが立ち上がったら/errorにアクセスします。この操作でもともと立ち上がっていたPodがエラー落ちします。

このあと、再度/storyにアクセスします。

はい、「Failed to open file」と表示されました。
この動作で2つのことがわかります。
1つは、replicasで増やしたPodに対してリクエストが飛んだこと。
もう一つは、EmptyDirはPod毎に作られる->新しく立ち上げたPodにはtext.txtファイルが無いということです。
k8sのメリットの一つはオートスケールができることなので、EmptyDirの使い所には注意が必要ですね。
hostPath Volume
hostPath Volumeを試してみましょう。
このドライバーは、ホストマシン上のディレクトリパスをPod内にセットすることができるようです。ちょうどdocker-compose.ymlのvolumesみたいなものですね。
ホストマシン上のパスをPodと共有するので、Podが増えたとしてもデータを共有することができるはずです。
deployment.ymlのvolumesを書き換えましょう。
1 2 3 4 5 |
volumes: - name: story-volume hostPath: path: /data # /dataを公開 type: DirectoryOrCreate # /dataが無い場合は、新しく作る |
emptyDirからhostPathに変更しました。pathには/dataを指定しており、podの中でdataフォルダが生成されます。
そして、dataフォルダに「/app/story 」がマウントされるはずです。
1 2 3 4 5 6 7 8 9 10 |
# 一度Podを削除 $ kubectl delete -f=deployment.yml deployment.apps "story-deployment" deleted # Podを再作成 $ kubectl apply -f=deployment.yml deployment.apps/story-deployment created # アプリ起動 $ minikube service story-service |
アプリが立ち上がったら、先ほどと同様にPOSTリクエストでデータ投入->/errorでアプリケーションの遮断->GETリクエストでデータ表示をしてみてください。データが表示できるはずです。
データの永続化
次は、データの永続化について検証していきましょう。
先程までの設定は、PodとNodeがVolumeに依存している状態でしたが、今度は、それらの依存関係を切り離すようにVolumeを設定したいと思います。
まずは、永続Volumeを設定するためのファイルを用意します。
PersistentVolume
1 2 |
# PersistentVolume touch host-pv.yml |
host-pv.ymlには、PersistentVolumeを実装します。
PersistentVolume (PV)はストレージクラスを使って管理者もしくは動的にプロビジョニングされるクラスターのストレージの一部です。これはNodeと同じようにクラスターリソースの一部です。PVはVolumeのようなボリュームプラグインですが、PVを使う個別のPodとは独立したライフサイクルを持っています。このAPIオブジェクトはNFS、iSCSIやクラウドプロバイダー固有のストレージシステムの実装の詳細を捕捉します。
公式サイトより
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# host-pv.yml apiVersion: v1 kind: PersistentVolume metadata: name: host-pv spec: capacity: storage: 1Gi volumeMode: Filesystem storageClassName: standard # storage class name accessModes: - ReadWriteOnce # - ReadOnlyMany # - ReadWriteMany hostPath: path: /data type: DirectoryOrCreate |
PersistentVolumeClaim
さらにClaimの設定を行います。
1 2 |
# PersistentVolumeClaim touch host-pvc.yml |
このファイルには、PersistentVolumeClaimの設定を書きます。
PersistentVolumeClaim (PVC)はユーザーによって要求されるストレージです。これはPodと似ています。PodはNodeリソースを消費し、PVCはPVリソースを消費します。Podは特定レベルのCPUとメモリーリソースを要求することができます。クレームは特定のサイズやアクセスモード(例えば、ReadWriteOnce、ReadOnlyMany、ReadWriteManyにマウントできます。詳しくはアクセスモードを参照してください)を要求することができます。
公式サイトより
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# host-pvc.yml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: host-pvc spec: volumeName: host-pv accessModes: - ReadWriteOnce storageClassName: standard # storage class name resources: requests: storage: 1Gi |
プロビジョニング
先ほどのVolume設定をdeployment.ymlに反映します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
apiVersion: apps/v1 kind: Deployment metadata: name: story-deployment spec: replicas: 2 selector: matchLabels: app: story template: metadata: labels: app: story spec: containers: - name: story image: アカウント名/kub-data-demo:1 imagePullPolicy: Always volumeMounts: - mountPath: /app/story name: story-volume resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 8080 volumes: # pvcを指定 - name: story-volume persistentVolumeClaim: claimName: host-pvc |
volumesの設定に、pvcを指定しました。
設定の反映
先ほど設定したVolumeをapplyコマンドで反映しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# storage classを確認 $ kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE standard (default) k8s.io/minikube-hostpath Delete Immediate false 20d # volume反映 $ kubectl apply -f=host-pv.yml persistentvolume/host-pv created $ kubectl apply -f=host-pvc.yml persistentvolumeclaim/host-pvc created $ kubectl apply -f=deployment.yml deployment.apps/story-deployment configured $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE host-pv 1Gi RWO Retain Bound default/host-pvc standard 105s $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE host-pvc Bound host-pv 1Gi RWO standard 115s $ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE story-deployment 2/2 2 2 59s # アプリ起動 $ minikube service story-service |
普通に起動すれば、OKです。
環境変数を利用する
次は、環境変数を利用してみましょう。
main.goのファイル読み込み処理で、環境変数からファイルパスを読み込みます。
1 2 3 4 5 6 |
// filePath -> os.Getenv("STORY_FOLDER")に変更 bytes, err := ioutil.ReadFile(os.Getenv("STORY_FOLDER")) ... f, err := os.OpenFile(os.Getenv("STORY_FOLDER"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) |
STORY_FOLDERは、deployment.ymlに設定する環境変数名です。
1 2 3 4 5 6 7 8 |
// deployment.yml spec: containers: - name: story image: アカウント/kub-data-demo:2 # 2に変更 env: // envファイルの実装 - name: STORY_FOLDER value: './story/text.txt' |
続いて、イメージを更新し、DockerHubにPush後、deploymentをapplyします。
1 2 3 4 |
docker build -t アカウント/kub-data-demo:2 . docker push アカウント/kub-data-demo:2 kubectl apply -f=deployment.yml minikube service story-service |

「Hello, World]」が表示されているので、、環境変数名からパスを読み込めたようですね。
ConfigMap
最後に、ConfigMapを学んで終わりにしましょう。
ConfigMapは環境変数などをKey Valueペアとして保存するリソースです。設定は、yamlファイルに書きます。
1 |
touch environment.yml |
1 2 3 4 5 6 7 |
apiVersion: v1 kind: ConfigMap metadata: name: data-store-env # ConfigMapの名前 data: # key: value path: './story/text.txt' |
このファイルをapplyします。
1 2 3 4 5 6 7 8 |
# configmap作成 $ kubectl apply -f=environment.yml configmap/data-store-env created # 状態確認 $ kubectl get configmap NAME DATA AGE data-store-env 1 32s |
ConfigMapの設定をdeployment.ymlに追加します。
1 2 3 4 5 6 7 8 9 10 |
spec: containers: - name: story image: アカウント名/kub-data-demo:2 env: - name: STORY_FOLDER valueFrom: configMapKeyRef: name: data-store-env # configmapの名前 key: path # keyの名前 |
deployment.ymlファイルをapplyします。
1 2 3 4 5 |
$ kubectl apply -f=deployment.yml deployment.apps/story-deployment configured # アプリスタート $ minikube service story-service |

OKですね。ConfigMapは結構使えそうですね。
おわりに
長くなりましたが、volume編は以上です。
ローカルでアプリ開発ならDockerだけで十分ですが、運用面も踏まえてポートフォリをを作りたいという人は、k8sを検討するのも良いと思います。
ガンガン使いこなしていきましょう!
次回
次回は、Networking編です。
最近のコメント