Kubernetes: ConfigMapを作成・Podにマウントしたいときのtips

# はじめに

Kubernetesでは設定ファイルや環境変数をConfigMapとして登録し、それをPodから利用できます。
ただ、少しクセがあるので、自分の中の知識をtipsとしてまとめておきます。
記事は、ConfigMapの作成編・ConfigMapのマウント編の2章構成になっています。
また、内容はほとんど各節に収まるように書いているので、逆引きのように見ていただくことが可能です。

### 実施環境

- Kubernetes v1.20.2

# ConfigMapの作成編

この章ではやりたいこと別にConfigMapをどう定義すれば良いかをまとめます。

## Podから環境変数として利用したい

この場合は環境変数として定義したい変数を、それぞれdataのサブマッピングとして書きましょう。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  ENV1: foo
  ENV2: bar
```

## Podにファイルとしてマウントしたい

dataのサブマッピングにファイル内容を直接記載しましょう。
ただし、例に挙げている`.env`をPodに環境変数として直接展開することはできません (例えば、docker-composeの`env_file`のような使い方) 。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  .env: |
    ENV1=foo
    ENV2=bar
  nginx.conf: |
    user  nginx;
    worker_processes  1;
    pid   /var/run/nginx.pid;
    http {
        include  /etc/nginx/conf.d/*.conf;
    }
```

## Kustomizeで環境変数をPodに渡したい

Kustomizeを使用している場合、`configMapGenerator`を用いてConfigMapを作成することが可能です。

```yaml:kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
configMapGenerator:
- name: sample-config-map
  literals:
  - ENV1=foo
  - ENV2=bar
```

### 環境変数をPodに埋め込む場合、configMapGeneratorと通常のConfigMapはどっちが良いのか？

ここで実例を紹介すると本筋から逸れるので、参考記事を紹介しておきます。
結論だけ言うと、

- 通常のConfigMapをPodから`envFrom`等で環境変数として読み込む場合は、ConfigMapを変更してもそれを利用するPodは再作成されない
- つまり、Podに最新値は自動的に反映されないので、別途Podの再作成を考慮してあげる必要がある
    - Deploymentの場合は、`kubectl rollout restart`
- configMapGeneratorを使う場合、ConfigMapに毎回ハッシュ値が付与され名前が変わるため、Podが毎回自動的に再作成され最新値が反映される

という違いを抑えて、どちらを使うか選びましょうということです。

**参考記事**

- [kustomizeでconfigMapを取り扱うときの注意](https://qiita.com/Sho2010@github/items/548582996d5ebfc63b1d)
- [kustomize の configGeneration を使って ConfigMap リソースをデプロイする](https://kakakakakku.hatenablog.com/entry/2020/08/25/091308)

## KustomizeでファイルからConfigMapを作成したい

Kustomizeを使用している場合、`configMapGenerator`を用いてファイルからConfigMapを作成することも可能です。
この例の場合、kustomization.yamlが置いてあるディレクトリ直下に`.env`と`nginx.conf`が置いてある必要があります。
kustomization.yamlが置いてあるディレクトリ以下であれば、さらにディレクトリを掘っても構いません。

```yaml:kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
configMapGenerator:
- name: sample-config-map
  files:
  - .env
  - nginx.conf
```

### Volumeを使ってファイルをマウントする場合、configMapGeneratorと通常のConfigMapはどっちが良いのか？

先の環境変数を埋め込む場合と同様の議論ですが少し違うところがあります。
configMapGeneratorを使う場合は変更の度にPodが再作成され、毎回自動的に値が反映されます。
通常のConfigMapをマウントする場合、次章で紹介する`subPath`を指定するかどうかで挙動が変わります。

- subPathを指定しない場合
    - Podの再作成をすることなく値が自動的に反映される
    - ただし、多少の遅延あり
- subPathを指定する場合
    - Podの再作成を手動でしてあげないと値が反映されない

Volumeを使用するマウント方法は、Podの再作成なしに値を反映することも可能なので、よく考えて適切な方法を選びましょう。

**参考記事**

- [公式ドキュメント](https://kubernetes.io/ja/docs/tasks/configure-pod-container/configure-pod-configmap/#%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88%E3%81%95%E3%82%8C%E3%81%9Fconfigmap%E3%81%AF%E8%87%AA%E5%8B%95%E7%9A%84%E3%81%AB%E6%9B%B4%E6%96%B0%E3%81%95%E3%82%8C%E3%82%8B)

# ConfigMapのマウント編

この章では、ConfigMapをマウントする方法を紹介します。
ただし簡単のため、例として挙げるConfigMapには通常のConfigMapを使用します。
KustomizeのconfigMapGeneratorを使う場合もやり方は変わりません。

## 環境変数として読み込みたい

まずは、ConfigMapに記載してあるdataのサブマッピングを全て環境変数として読み込む方法です。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  ENV1: foo
  ENV2: bar
---
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
  - name: nginx
    image: nginx:stable
    envFrom:
    - configMapRef:
        name: sample-config-map
```

Pod作成後に次のコマンドで確認してみると、環境変数として定義されていることがわかります。

```bash
$ kubectl exec -it sample-pod -- sh -c 'echo $ENV1; echo $ENV2'
foo
bar
```

## 特定の変数だけ環境変数として読み込みたい

先の例の`sample-pod`の`spec`を次の内容に書き換えると、ENV2のみ環境変数を定義することが可能です。
フォーマットから分かるように、このやり方はConfigMapに記載してある名前とは別の名前で環境変数を定義することも可能です。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  ENV1: foo
  ENV2: bar
---
spec:
  containers:
  - name: nginx
    image: nginx:stable
    env:
    - name: ENV2
      valueFrom:
        configMapKeyRef:
          name: sample-config-map
          key: ENV2
```

Pod作成後に次のコマンドを実行すると、`ENV2`だけ定義されていることがわかります。

```bash
$ kubectl exec -it sample-pod -- sh -c 'echo $ENV1; echo $ENV2'

bar
```

## ファイルをコンテナにマウントしたい

ConfigMapに記載した内容をファイルとしてPodにマウントする方法です。
ConfigMapから作成したVolumeのpathを`volumeMounts[].subPath`に指定することで、`volumeMounts[].mountPath`に指定したコンテナ内のファイルに1:1の関係でマウントすることができます。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  nginx.conf: |
    user  nginx;
    worker_processes 1;
    pid   /var/run/nginx.pid;
    events {
        worker_connections  1024;
    }
    http {
        include  /etc/nginx/conf.d/*.conf;
    }
---
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
  - name: nginx
    image: nginx:stable
    volumeMounts:
    - mountPath: /etc/nginx/nginx.conf
      subPath: nginx.conf
      name: nginx-conf
  volumes:
  - name: nginx-conf
    configMap:
      name: sample-config-map
      items:
      - key: nginx.conf
        path: nginx.conf
```

Pod作成後に次のコマンドを実行してみると、マウントできていることが確認できます。

```bash
$ kubectl exec -it sample-pod -- ls /etc/nginx
conf.d          mime.types  nginx.conf   uwsgi_params
fastcgi_params  modules     scgi_params

$ kubectl exec -it sample-pod -- cat /etc/nginx/nginx.conf
user  nginx;
worker_processes 1;
pid   /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include  /etc/nginx/conf.d/*.conf;
}
```

## ディレクトリごとコンテナにマウントしたい

ConfigMapから作成したVolumeをディレクトリとしてマウントする方法です。
マウント先に指定したディレクトリの内容は、マウントしたVolumeの内容に置き換わるので、元ファイルが消える(使えない)ことに注意してください。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  nginx.conf: |
    user  nginx;
    worker_processes 1;
    pid   /var/run/nginx.pid;
    events {
        worker_connections  1024;
    }
    http {
        server {
            listen 80;

            location / {
                root   /usr/share/nginx/html;
                index  index.html;
            }
        }
    }
---
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
  - name: nginx
    image: nginx:stable
    volumeMounts:
    - mountPath: /etc/nginx
      name: nginx-conf
  volumes:
  - name: nginx-conf
    configMap:
      name: sample-config-map
      items:
      - key: nginx.conf
        path: nginx.conf
```

Pod作成後に次のコマンドを実行してみると、`/etc/nginx`がマウントしたVolumeに置き換わっていることがわかります。

```bash
$ kubectl exec -it sample-pod -- ls /etc/nginx
nginx.conf

$ kubectl exec -it sample-pod -- cat /etc/nginx/nginx.conf
user  nginx;
worker_processes 1;
pid   /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    server {
        listen 80;

        location / {
            root   /usr/share/nginx/html;
            index  index.html;
        }
    }
}
```

## 実行可能ファイルをマウントしたい

ConfigMapから作成したVolumeのファイルは原則`644`のパーミッションとなります。
実行権限を付けたい場合は、`mode`を指定してあげる必要があります。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  entrypoint.sh: |
    #!/bin/sh
    echo "Hello, ConfigMap!\n"
---
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
  - name: nginx
    image: nginx:stable
    volumeMounts:
    - mountPath: /run/entrypoint.sh
      subPath: entrypoint.sh
      name: entrypoint
  volumes:
  - name: entrypoint
    configMap:
      name: sample-config-map
      items:
      - key: entrypoint.sh
        path: entrypoint.sh
        mode: 0755
```

Pod作成後に次のコマンドを実行すると、実行可能なパーミッションが付与されていることがわかります。

```bash
$ kubectl exec -it sample-pod -- ls -l /run/entrypoint.sh
-rwxr-xr-x 1 root root 37 May 23 08:48 /run/entrypoint.sh
```

### 補足

`mode: 0755`を指定せずにPodを作成した場合のパーミッションは次のようになります。

```bash
-rw-r--r-- 1 root root 37 May 23 08:49 /run/entrypoint.sh
```

## マウントするファイルのグループ権限をroot以外にしたい

これまでの例を見てお気づきの方もいらっしゃると思いますが、ConfigMapのVolumeをPodにマウントするとき、デフォルトではroot権限でマウントされます。
これが意味するところは、アプリケーションの実行ユーザーがroot以外のときに、マウントしたファイルを読み込むことができないということになります(厳密に言うとできますが、、)。
ここでは、マウントするファイルのグループ権限を、実行ユーザーの所属するグループに変更する方法を紹介します。
`spec.securityContext.fsGroup`に付与するグループのidを指定します。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  entrypoint.sh: |
    #!/bin/sh
    echo "Hello, ConfigMap!\n"
---
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  securityContext:
    fsGroup: 101
  containers:
  - name: nginx
    image: nginx:stable
    volumeMounts:
    - mountPath: /run/entrypoint.sh
      subPath: entrypoint.sh
      name: entrypoint
  volumes:
  - name: entrypoint
    configMap:
      name: sample-config-map
      items:
      - key: entrypoint.sh
        path: entrypoint.sh
```

Pod作成後に次のコマンドを実行すると、マウントしたファイルのグループ権限が変更されていることがわかります。

```bash
$ kubectl exec -it sample-pod -- ls -l /run/entrypoint.sh
-rw-r--r-- 1 root nginx 37 May 23 09:05 /run/entrypoint.sh
```

### 注意

フォーマットから分かる通り、`spec.securityContext`は、そのPod内のコンテナ全てに影響します。
次の例は、[PHPの公式イメージ](https://hub.docker.com/_/php)に同じファイルをマウントしてみた例です。

```bash
$ kubectl exec -it sample-pod -c php-fpm -- ls -l /run/entrypoint.sh
-rw-r--r-- 1 root 101 37 May 23 12:53 /run/entrypoint.sh
```

ここから分かることは、意図しないグループ権限の変更をしてしまう可能性が高いということです。
例えば、`php.ini`をマウントしたとしてもグループ権限が101になってしまいます。
言い換えると、nginxコンテナには101、phpコンテナには102、のようなコンテナごとに権限を変えることができず、ひとつしか指定できないことを意味します。

また、実は`spec.containers[].securityContext`というのもありますが、`fsGroup`が存在しないため、残念ながらコンテナごとに設定することはできません。

## マウントするファイルのオーナー権限をroot以外にしたい

これは少しトリッキーな方法を使う必要があります。
Podが起動しているときだけ有効な一時的な保存領域`emptyDir`を使用する方法です。
`initContainers`の前処理を利用して、`ConfigMap`の中身を`emptyDir`の方に移し権限を書き換えておき、本体のコンテナでは`emptyDir`をマウントするというものです。

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample-config-map
data:
  entrypoint.sh: |
    #!/bin/sh
    echo "Hello, ConfigMap!\n"
---
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  initContainers:
  - name: acquire-ownership
    image: nginx:stable
    args: ["sh", "-c", "cp /run/entrypoint.sh /ownerships; chown -R 101:101 /ownerships"]
    volumeMounts:
    - mountPath: /run/entrypoint.sh
      subPath: entrypoint.sh
      name: entrypoint
    - mountPath: /ownerships
      name: entrypoint-with-ownership
  containers:
  - name: nginx
    image: nginx:stable
    volumeMounts:
    - mountPath: /ownerships
      name: entrypoint-with-ownership
  volumes:
  - name: entrypoint
    configMap:
      name: sample-config-map
      items:
      - key: entrypoint.sh
        path: entrypoint.sh
  - name: entrypoint-with-ownership
    emptyDir: {}
```

Pod作成後に次のコマンドを実行すると、権限が書き換わっていることを確認できます。

```bash
$ kubectl exec -it sample-pod -c nginx -- ls -l ownerships
total 4
-rw-r--r-- 1 nginx nginx 37 May 23 13:32 entrypoint.sh
```

# おわりに

Kubernetesは進化が非常に早いです。
ここにまとめた方法より良い方法がすぐに登場するかもしれません。
もしくは、書いている時点でそのやり方古いよというのもあるかもしれません。
気づかれた方はコメント頂けるとありがたいです。