KubernetesでVolumeトリックを行う方法

この記事はRetty Advent Calendar 2019 11日目の記事です。
昨日はデータアナリスト飯田のカフェとラーメン、どちらに軍配? ~ネットワーク分析を用いたコミュニティ構造の比較~でした。

はじめに

こんにちは、エンジニアの櫻井です。
Rettyでは2013年に入社して以来、iOSやらサーバーサイドやら時にはインフラのマネごとまで幅広くやってきているなんでも屋さんです。

そのなんでも屋の経歴の中で2年前くらいにKubernetesを使って社内開発環境を作る、ということを当時はインターンであった弊社エンジニアの神(注・人名であり実名です)と一緒におこなってました。
本体となるRettyそのものについてはそこに載せて動かせるようになったものの「他のサービスなんかも色々載っけて動かしたいよ」という話があったため暇を見つけて対応していたのですが、その時にKubernetes上でDockerのVolumeトリックを使いたい環境が出てきたためそれを行った際の記録となります。

また今回はローカルのDocker for Macに同梱されているKubernetesですぐに本記事の内容を試せるように、GithubにサンプルコードとREADMEのセットを用意したのとDockerHubにパブリックリポジトリを用意したため、興味がある方はぜひお試しくださいませ

github.com

hub.docker.com

この記事ではKubernetesの説明やコマンドの使い方の詳細などは省略するため、それらについての詳細は下記のサイトなどをご覧ください。

qiita.com kubernetes.io

KubernetesでVolumeトリックを行う方法

Volumeトリックとは?

開発においてDockerを使う場合、開発ディレクトリをコンテナにマウントしてローカルファイルを変更したらコンテナ内のファイルも更新したい、ということがよくあります。
その際、composerやbundlerといったパッケージ管理ツールでインストールされるようなライブラリ群はGit管理しないため、開発ディレクトリを単純にマウントするとライブラリ群がコンテナ内で消えてしまい、コンテナが期待どおりの動作をしなくなってしまいます。

このような問題を解消するのがVolumeトリックです。

PHPのcomposerを例にした場合、composerはvendorというディレクトリ配下にパッケージが入ります。
その際に docker-compose.yml をこのように書くとカレントディレクトリを /app/src 配下にマウントしつつも、 /app/src/vendor はコンテナのものを使う、といったことができるようになります。

version: '3'

services:
  laravel:
    image: saku2saku/2019-advent-calendar:base-title
    ports:
      - "18000:8000"
    volumes:
      - .:/app/src:cached
      - /app/src/vendor

KubernetesでVolumeトリックを行う方法

VolumeトリックをKubernetesで行う場合には initContainersEmptyDir を使うことで実現できます。
initContainersは対象のコンテナを起動させる前にコンテナを起動させて処理を行うことができる機能で、EmptyDirは名前の通り空のディレクトリをVolumeとして定義する機能です。

この2つを組み合わせることで、下記のような流れの処理を実現できます。

  1. initContainersでコンテナを起動してEmptyDirをマウントし、コンテナのライブラリ群のディレクトリをEmptyDirにコピーする
  2. 開発ディレクトリ(ライブラリ群は無し)をコンテナにマウントする
  3. ライブラリ群がコピーされたEmptyDirを起動したコンテナに対してマウントする

具体的には下記のようなyamlを書くことになります。
(注・開発ディレクトリのマウントにhostPathのボリュームを指定していますが、あくまでサンプルとして試すローカルのKubernetesという前提があるため、ちゃんとしたクラスタを組む環境ではNFSなりなんなりを使うことになります)

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  labels:
    run: 2019-advent-calendar
  name: 2019-advent-calendar
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      run: 2019-advent-calendar
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        run: 2019-advent-calendar
    spec:
      initContainers:
      - name: vendor-clone
        image: saku2saku/2019-advent-calendar:base-title
        command: ["sh", "-c"]
        args:
        - |
          cp -a /app/src/vendor/* /vendor
        volumeMounts:
        - name: vendor-volume
          mountPath: /vendor
      containers:
      - name: 2019-advent-calendar
        image: saku2saku/2019-advent-calendar:base-title
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8000
        volumeMounts:
        - name: contents
          mountPath: /app/src
        - name: vendor-volume
          mountPath: /app/src/vendor
      volumes:
        - name: contents
          hostPath:
            path: /path/to/2019-advent-calendar/application
        - name: vendor-volume
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  labels:
    run: 2019-advent-calendar
  name: advent-calendar
  namespace: default
spec:
  ports:
  - port: 80
    targetPort: 8000
  selector:
    run: 2019-advent-calendar
  type: NodePort

上記の initContainers を記述する際のポイントが1つ。
initContainersでcpコマンドを実行する際に、参考にしたページでは command (DockerでいうENTRYPOINT) に cp -a コマンドを書いて対象ディレクトリをコピーとあったけど、自分のコンテナイメージではうまく行かなかったので args (DockerでいうCMD) も使ってcpを指定して事なきを得ました。

DockerイメージにおいてENTRYPOINTもCMDもどちらも指定されている場合もあるので、今回のようにどちらも指定して sh -c cp -a /app/src/vendor/* /vendor としてしまうのが個人的に確実でいいのではないかなと思います。

最後に

本当はKubernetesで作った開発環境の構成とか、なんでそういう構成になったのかといった背景も含めて書きたかったのですが、ここまでで力つきてしまいました(´・ω・`)
もしそのあたり興味あるよ!というお声がありましたらぜひWantedlyの記事にある話を聞きに行きたいというボタンをポチッとしていただければと思います(違

www.wantedly.com

参考記事

docker - kubernetes volume for node_modules - Stack Overflow