HelmでRedmineをデプロイする際にNFSを使った永続ボリュームを設定する方法

# はじめに
Kubernetes上でBitnami Helm Chartを使ってRedmineを構築し，永続ボリュームとしてSynology NASをNFS経由で使えるようにしました．
私が実際に構築した際の手順を記録しておきます．

:::note warn
この記事はRedmine を Helm で初めてデプロイする際に，最初から NFS を使った永続ボリュームを設定したい方に向けています
:::
# 環境構成
- Kubernates： `v1.30.6+k3s1`
- Helm： `v3.17.3`
- Redmine： Bitnami Helm Chart `redmine-32.2.5`
- NFSサーバ： Synology NAS

# 1. NFSの準備
RedmineのPVをNFSで扱うために，以下の準備を行いました．
## 1-1. NAS側でNFS共有ディレクトリを作成
Synology NASの管理画面で以下のようなNFS共有を作成しました．
- 共有フォルダ名：redmine-nfs
- パス：/volume1/redmine-nfs
- アクセス許可：RedmineをデプロイしているkubernetesノードのIPアドレスを許可

## 1-2. Kubernetesノードにnfs-commonをインストール
ノード側でNFSマウントができるように，各ノードに以下をインストールしました．
```
sudo apt-get install nfs-common
```
kubernetesクラスターを使用している場合，マスターノードだけでなく全てのノードに対してインストールを行ってください．

## 1-3. NFS用のProvisionerをHelmでデプロイ
Helmの nfs-subdir-external-provisioner を使用し，NFS用のStorageClassを作成しました．
NFS用のStorageClassはクラスター全体で利用できるリソースですが，それを動作させるpodを動かすためのネームスペースを作成し，そこに対してデプロイするのがいいと思います．
今回はnfsというネームスペースにデプロイしています．

```
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
```

```
helm install nfs nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --set nfs.server=<NASのIPアドレス> --set nfs.path=<共有フォルダのパス>　-n nfs
```

 ## 1-4. StorageClassの確認
デプロイ後，以下のコマンドで nfs-client というStorageClassが追加されていることを確認しました．
```
kubectl get strageclass 
```
出力例
```bash
NAME                   PROVISIONER                                     RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path (default)   rancher.io/local-path                           Delete          WaitForFirstConsumer   false                  207d
nfs-client             cluster.local/nfs-subdir-external-provisioner   Delete          Immediate              true                   2d9h
```
この nfs-client を values.yaml でRedmineのPVCに指定することで，NFSへの切り替えが可能になります．

StorageClass が存在していても，NFS Provisioner が正常に動いていなければ意味がありません．
以下のコマンドでNFS Provisionerのpodが起動しているかを確認しました．
```
kubectl get pod -n nfs
```

出力例
```bash
NAME                                              READY   STATUS    RESTARTS   AGE
nfs-subdir-external-provisioner-6dc4f545f-c5fnw   1/1     Running   0          2d9h
```

# 2. RedmineのHelmデプロイ
## 2-1. values.yamlの準備
[Helm チャートに含まれている values.yaml](https://github.com/bitnami/charts/blob/main/bitnami/redmine/values.yaml) をベースに，必要な項目のみを上書きします．
この values.yaml にある設定のうち,以下のように項目を編集しました．
```
persistence:
  storageClass: "nfs-client"
```
storageClassを**nfs-client**と明示します．
私はRedmineとmariaDBの両方に対して個別にstorageClass: "nfs-client"と指定しましたが，このvalues.yaｍlにはglobal.defaultStorageClassがあるのでそこで指定してもいいかもしれません．
私は作業時にはglobal.defaultStorageClassに気が付けませんでした．

:::note info
values.yaml 内では，MariaDB の root パスワード（auth.rootPassword）や，Redmine の公開用ポート番号（例：service.nodePorts.http）などの設定も指定できます
:::

## 2-2. Helmリポジトリの追加と更新
```
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
```
Helm Chart の提供元は Bitnamiです．
redmine は公式チャートに含まれています．

## 2-3. Namespaceの作成
任意ですが，運用の管理のためにRedmine用のネームスペースの作成をお勧めします．
```
kubectl create namespace redmine
```

## 2-4. RedmineのHelmデプロイ
こちらのコマンドを使用してデプロイしました．
```
helm install my-redmine oci://registry-1.docker.io/bitnamicharts/redmine -n redmine -f values.yaml
```
- my-redmine：リリース名（任意に変更可能）
- -n redmine：redmineというネームスペースにデプロイ
- -f values.yaml：先ほどカスタマイズしたvalues.yamlを指定

出力例
```bash
Pulled: registry-1.docker.io/bitnamicharts/redmine:32.2.5
Digest: sha256:120baaa62fc616851a0189cab4009b08354ecdf3e543ceacb122f4c3666e1941
NAME: my-redmine
LAST DEPLOYED: Thu Jun  5 00:31:45 2025
NAMESPACE: redmine
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: redmine
CHART VERSION: 32.2.5
APP VERSION: 6.0.5

Did you know there are enterprise versions of the Bitnami catalog? For enhanced secure software supply chain features, unlimited pulls from Docker, LTS support, or application customization, see Bitnami Premium or Tanzu Application Catalog. See https://www.arrow.com/globalecs/na/vendors/bitnami for more information.

** Please be patient while the chart is being deployed **

1. Get the Redmine URL:

  export NODE_PORT=$(kubectl get --namespace redmine -o jsonpath="{.spec.ports[0].nodePort}" services my-redmine)
  export NODE_IP=$(kubectl get nodes --namespace redmine -o jsonpath="{.items[0].status.addresses[0].address}")
  echo "Redmine URL: http://$NODE_IP:$NODE_PORT/"

2. Login with the following credentials

  export REDMINE_USERNAME=user
  export REDMINE_PASSWORD=$(kubectl get secret --namespace "redmine" my-redmine -o jsonpath="{.data.redmine-password}" | base64 -d)

  echo Username: $REDMINE_USERNAME
  echo Password: $REDMINE_PASSWORD

  You can access the DB using the following password:
  export MARIADB_PASSWORD=$(kubectl get secret --namespace "redmine" my-redmine-mariadb -o jsonpath="{.data.mariadb-password}" | base64 -d)

⚠ SECURITY WARNING: Original containers have been substituted. This Helm chart was designed, tested, and validated on multiple platforms using a specific set of Bitnami and Tanzu Application Catalog containers. Substituting other containers is likely to cause degraded security and performance, broken chart features, and missing environment variables.

Substituted images detected:
  - docker.io/bitnami/redmine:6.0.4-debian-12-r6
  - docker.io/bitnami/os-shell:12-debian-12-r42

⚠ WARNING: Original containers have been retagged. Please note this Helm chart was tested, and validated on multiple platforms using a specific set of Tanzu Application Catalog containers. Substituting original image tags could cause unexpected behavior.

Retagged images:
  - docker.io/bitnami/redmine:6.0.4-debian-12-r6
  - docker.io/bitnami/os-shell:12-debian-12-r42
```


# 3. 動作確認

## 3-1. podの起動確認
以下のコマンドを実行しました．
```
kubectl get pods -n redmine
```
my-redmine（先ほど指定したリリース名）及びmy-redmine-mariaDBのpodのSTATUSがRunningであれば正常に起動しています．

出力例
```bash
NAME                                       READY   STATUS    RESTARTS     AGE
my-redmine-75f8b76c9c-x99hp                1/1     Running   0            2d8h
my-redmine-mariadb-0                       1/1     Running   0            2d9h
```

## 3-2. PVCの確認
以下のコマンドを実行しました．
```
kubectl get pvc -n redmine
```
- Redmine用のPVC
- MariaDB用のPVC

の両方のSTATUSがBoundと表示されていて，STORAGECLASSがnfs-clientであればNFS経由でストレージが正しく接続されています．

出力例
```
NAME                        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
data-my-redmine-mariadb-0   Bound    pvc-eb61ab83-f3b7-4696-9d80-beb34f18b289   8Gi        RWO            nfs-client     <unset>                 2d9h
my-redmine                  Bound    pvc-e4ceff1f-83b1-483a-bc03-7f48082b3acf   8Gi        RWO            nfs-client     <unset>                 2d9h
```

## 3-3. RedmineのWebUIへのアクセス確認
以下のコマンドを実行し，EXTERNAL-IP または NodePort を確認し，ブラウザから Redmine にアクセスできるかを確認します．
```
kubectl get svc -n redmine
```
出力例
```
NAME                          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
my-redmine                    NodePort    10.43.34.142   <none>        80:XXXXX/TCP     2d9h
my-redmine-mariadb            ClusterIP   10.43.32.162   <none>        3306/TCP         2d9h
```

XXXXXはポート番号です．values.yaml内で指定することができるので，実際のポートが環境に応じて異なります．

## 3-4. ファイルのアップロード確認
チケットを作成し，ファイルを添付することができるか確認します．
その後，NAS上もしくはRedmineのpod内でアップロードしたファイルが確認できれば永続化の動作も正常です．

:::note info
podを再起動してもファイルが保持されているか確認してください
:::


# おわりに
RedmineをHelm ChartでKubernetesにデプロイする際に，NFSを使った永続ボリュームを利用する方法を紹介しました．
同様の構成を検討している方の参考になれば幸いです．