diff --git a/CLAUDE.md b/CLAUDE.md index ea59e8a..12963fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,16 @@ Websites 組織の各サイト(e-learn.jp / fcdomain.com / hotels / tskholding ## チャート改修履歴 +### Cron(CronJob)定期実行機能を追加(2026-09-27, v8.5.10-e) + +- **背景**: 定期実行したいPHPスクリプト(レポート送信・バッチ処理等)を動かす手段がなく、外部で別途スケジューラを用意する必要があった。 +- **追加内容**: `cron.jobs`(配列)にジョブを定義すると、`templates/cronjob.yaml`がジョブごとにKubernetes CronJobを生成する。php-fpmと同じPHPイメージを使用し、PVCルートを`/var/www`にマウント、`externalDatabase`/`phpEnv`/`extraEnv`/`extraEnvFrom`/`SELENIUM_URL`をphp-fpmコンテナと同じ内容で注入する。`smtp.enabled: true`の場合はCronJob内でも自動的にmsmtpを導入する(`templates/deployment.yaml`のSMTP設定と重複実装、変更時は両方揃えること)。 +- **設計上の注意点**: + - CronJobが生成するPodのラベル(`app.kubernetes.io/name`)は`phpfpm.cron.name`ヘルパーで`-cron`というバリアント名にしている。Selenium分離(v8.5.10-b)と同じ理由で、nginx/php-fpm DeploymentのmatchLabels(`name: phpfpm, instance: `)と一致させると、ReplicaSetコントローラーがCronJob由来のPodを誤って自身の管理対象とみなし削除する事故につながるため。 + - CronJob名は`trunc 52`で切り詰めている(Kubernetesの実務上の制約。CronJobは生成するJob/Pod名にタイムスタンプ等のsuffixが付くため、フルの253/63文字ではなく52文字程度に収める必要がある)。 + - Composer(`/composer`)・Python venv(`/venv`)はデフォルトでPVC非マウントの使い捨て領域のため、CronJobのPodには存在しない。意図的にCronJob側では再インストールを行わない設計とした(頻繁な実行での再インストールコストを避けるため)。CronJobから使いたい場合は`composer.workingDir`/`python.venvDir`を`/var/www`配下に変更しPVC上へ永続化することをREADMEで案内している。 +- **対象ファイル**: `templates/cronjob.yaml`(新規), `templates/_helpers.tpl`, `values.yaml`, `README.md`, `Chart.yaml` + ### ingress.tls[].secretNameをhostから自動導出可能に(2026-09-27, v8.5.10-d) - **背景**: `ingress.hosts[].host` は既にYAMLアンカー(`&appDomain`)で `ingress.tls[].hosts` と値を共有できるようになっていたが、`ingress.tls[].secretName` は独立した文字列指定が必須で、ドメイン変更時に別途書き換えが必要だった。 diff --git a/Chart.yaml b/Chart.yaml index e125444..3871c70 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -2,6 +2,6 @@ apiVersion: v2 name: phpfpm description: Nginx + PHP-FPM Helm Chart with external DB and optional Selenium support type: application -version: 8.5.10-d +version: 8.5.10-e appVersion: "8.5.10" icon: https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/330px-PHP-logo.svg.png diff --git a/README.md b/README.md index 3e60131..6e9db2e 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,43 @@ v8.5.10-b以降、Seleniumは独立したDeployment/Service(`-phpfpm- `selenium.enabled: true`のとき、php-fpmコンテナの`SELENIUM_URL`環境変数は自動的にこのServiceを指す値(`http://-phpfpm-selenium:4444/wd/hub`)に設定される。`phpEnv.SELENIUM_URL`を独自に指定していても、この自動注入された値が優先される。 +### Cron(定期実行)設定 + +`cron.jobs`に1件以上定義すると、php-fpmと同じPHPイメージ(`image.php`)を使ってスケジュール実行するKubernetes CronJobが、ジョブごとに1つずつ作成される。既定は空配列で、何も作成されない。 + +| パラメータ | 説明 | デフォルト | +|-----------|------|-----------| +| `cron.jobs` | CronJob定義の配列 | `[]` | +| `cron.jobs[].name` | ジョブ名(英数字とハイフン、CronJob名の一部に使用) | 必須 | +| `cron.jobs[].schedule` | 標準cron形式のスケジュール | 必須 | +| `cron.jobs[].command` | 実行するシェルコマンド(文字列) | 必須 | +| `cron.jobs[].timeZone` | タイムゾーン(Kubernetes 1.27+) | 省略時UTC | +| `cron.jobs[].concurrencyPolicy` | `Allow`/`Forbid`/`Replace` | `Forbid` | +| `cron.jobs[].restartPolicy` | `OnFailure`/`Never` | `OnFailure` | +| `cron.jobs[].successfulJobsHistoryLimit` | 成功ジョブの保持数 | `3` | +| `cron.jobs[].failedJobsHistoryLimit` | 失敗ジョブの保持数 | `1` | +| `cron.jobs[].activeDeadlineSeconds` | 実行タイムアウト(秒) | 省略可 | +| `cron.jobs[].suspend` | `true`でこのジョブだけ一時停止 | `false` | +| `cron.jobs[].resources` | CPU/メモリ requests・limits | `resources.phpfpm`を流用 | + +```yaml +# values.yaml +cron: + jobs: + - name: daily-report + schedule: "0 18 * * *" + timeZone: "Asia/Tokyo" + command: "php /var/www/html/cron/daily-report.php" +``` + +> **📌 CronJobの構成について** +> - PVCルートを`/var/www`にマウントする(php-fpmコンテナと同じ)。`html/`配下の公開コード、または非公開ディレクトリのスクリプト・出力先どちらも利用できる。 +> - `externalDatabase` / `phpEnv` / `extraEnv` / `extraEnvFrom` / `SELENIUM_URL`(selenium有効時)は、php-fpmコンテナと同じ内容が渡される。 +> - `smtp.enabled: true`の場合、CronJobのPodでも自動的にmsmtpを導入してmsmtprcを配置するため、PHPの`mail()`関数がそのまま使える。 +> - **Composer/Pythonとの併用**: `composer.workingDir`/`python.venvDir`が既定の`/composer`・`/venv`(PVC非マウントの使い捨て領域)のままだと、php-fpmの常駐Pod内にしか存在しないためCronJobからは使えない。CronJobから使いたい場合は`composer.workingDir: "/var/www/composer"`のように`/var/www`配下へ変更し、PVC上に永続化すること。 +> - CronJobの名前は52文字を超えないよう自動的に切り詰められる(Kubernetesの制約)。`cron.jobs[].name`は短く分かりやすい名前にすること。 +> - 生成されるPodのラベル(`app.kubernetes.io/name`)は`-cron`というバリアント名になっており、nginx/php-fpmのDeploymentのselectorとは一致しない(Selenium分離と同じ設計)。 + ### 外部データベース設定 | パラメータ | 説明 | デフォルト | diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index d6cb932..cb36db5 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -41,3 +41,23 @@ name を variant にすることで、この重複を構造的に防止する。 app.kubernetes.io/name: {{ include "phpfpm.selenium.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{/* +Cron(CronJob)用の名前・ラベル(v8.5.10-e)。 + +Selenium と同じ理由で、CronJobが生成するPodのラベルにも nginx/php-fpm の +Deployment と同じ app.kubernetes.io/name("phpfpm")+ instance を持たせては +いけない。同じ組み合わせだとDeploymentのReplicaSetコントローラーが +matchLabels(name=phpfpm, instance=)に一致するPodとしてCronJob由来 +のPodまで対象に含めてしまい、replicaCountとの差分調整でCronJobのPodを +意図せず削除する等の事故につながる。name自体をvariant("phpfpm-cron")に +することで構造的に衝突を防ぐ。 +*/}} +{{- define "phpfpm.cron.name" -}} +{{- printf "%s-cron" (include "phpfpm.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{- define "phpfpm.cron.selectorLabels" -}} +app.kubernetes.io/name: {{ include "phpfpm.cron.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/templates/cronjob.yaml b/templates/cronjob.yaml new file mode 100644 index 0000000..9c573c2 --- /dev/null +++ b/templates/cronjob.yaml @@ -0,0 +1,157 @@ +{{- range .Values.cron.jobs }} +--- +# CronJobのPodは、nginx/php-fpmのDeploymentとは別のReplicaSetに属するため、 +# jobTemplate.spec.template.metadata.labels には phpfpm.cron.selectorLabels +# (app.kubernetes.io/name: -cron)を使う。ここで通常の +# phpfpm.selectorLabels(name: phpfpm)を使ってしまうと、nginx/php-fpm +# DeploymentのmatchLabelsと一致するPodが増え、ReplicaSetコントローラーが +# replicaCountとの差分調整でCronJob由来のPodを意図せず削除する事故につながる +# (Selenium分離(v8.5.10-b)と同じ理由)。 +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ printf "%s-cron-%s" (include "phpfpm.fullname" $) .name | trunc 52 | trimSuffix "-" }} + labels: + {{- include "phpfpm.labels" $ | nindent 4 }} + app.kubernetes.io/component: cron +spec: + schedule: {{ .schedule | quote }} + {{- if .timeZone }} + timeZone: {{ .timeZone | quote }} + {{- end }} + concurrencyPolicy: {{ .concurrencyPolicy | default "Forbid" }} + {{- if hasKey . "suspend" }} + suspend: {{ .suspend }} + {{- end }} + successfulJobsHistoryLimit: {{ .successfulJobsHistoryLimit | default 3 }} + failedJobsHistoryLimit: {{ .failedJobsHistoryLimit | default 1 }} + jobTemplate: + spec: + {{- if .activeDeadlineSeconds }} + activeDeadlineSeconds: {{ .activeDeadlineSeconds }} + {{- end }} + template: + metadata: + labels: + {{- include "phpfpm.cron.selectorLabels" $ | nindent 12 }} + app.kubernetes.io/component: cron + spec: + restartPolicy: {{ .restartPolicy | default "OnFailure" }} + containers: + - name: cron + image: "{{ $.Values.image.php.registry }}/{{ $.Values.image.php.repository }}:{{ $.Values.image.php.tag }}" + imagePullPolicy: {{ $.Values.image.php.pullPolicy }} + command: + - sh + - -c + - | + echo "Starting cron job: {{ .name }}" + + {{- if $.Values.smtp.enabled }} + # ======================================== + # SMTP設定(msmtp) — templates/deployment.yaml のSMTP設定と同じ内容。 + # 変更する場合は両方のファイルを揃えること。 + # ======================================== + echo "Installing msmtp for mail sending..." + apk add --no-cache msmtp ca-certificates + + cp /etc/smtp-config/msmtprc /etc/msmtprc + chmod 644 /etc/msmtprc + + cat > /usr/local/etc/php/conf.d/99-smtp.ini << 'PHP_SMTP_EOF' + sendmail_path = "/usr/bin/msmtp -t" + sendmail_from = "{{ $.Values.smtp.from }}" + PHP_SMTP_EOF + {{- end }} + + echo "Running: {{ .command }}" + {{ .command }} + {{- with (.resources | default $.Values.resources.phpfpm) }} + resources: + {{- toYaml . | nindent 16 }} + {{- end }} + volumeMounts: + # php-fpmコンテナと同様、PVCルートを/var/wwwにマウントする。 + # html/ は公開コード、それ以外は非公開ファイルの置き場として利用できる。 + - name: app-storage + mountPath: /var/www + {{- if $.Values.smtp.enabled }} + - name: smtp-config + mountPath: /etc/smtp-config + readOnly: true + {{- if and $.Values.smtp.auth.enabled (not $.Values.smtp.existingMsmtprcSecret) }} + - name: smtp-secrets + mountPath: /etc/smtp-secrets + readOnly: true + {{- end }} + {{- end }} + env: + {{- if $.Values.externalDatabase.enabled }} + - name: DB_HOST + value: {{ $.Values.externalDatabase.host | quote }} + - name: DB_PORT + value: {{ $.Values.externalDatabase.port | quote }} + - name: DB_NAME + value: {{ $.Values.externalDatabase.database | quote }} + - name: DB_USER + value: {{ $.Values.externalDatabase.username | quote }} + - name: DB_PASS + valueFrom: + secretKeyRef: + name: {{ include "phpfpm.fullname" $ }}-db-secret + key: {{ include "phpfpm.fullname" $ }}-db-key + {{- end }} + {{- range $key, $value := $.Values.phpEnv }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- if $.Values.selenium.enabled }} + - name: SELENIUM_URL + value: "http://{{ include "phpfpm.fullname" $ }}-selenium:4444/wd/hub" + {{- end }} + {{- with $.Values.extraEnv }} + {{- toYaml . | nindent 16 }} + {{- end }} + {{- with $.Values.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 16 }} + {{- end }} + dnsPolicy: ClusterFirst + dnsConfig: + options: + - name: ndots + value: "1" + volumes: + - name: app-storage + {{- if $.Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "phpfpm.fullname" $ }}-pvc + {{- else }} + emptyDir: {} + {{- end }} + {{- if $.Values.smtp.enabled }} + - name: smtp-config + {{- if $.Values.smtp.existingMsmtprcSecret }} + secret: + secretName: {{ $.Values.smtp.existingMsmtprcSecret }} + items: + - key: {{ $.Values.smtp.existingMsmtprcSecretKey | default "msmtprc" }} + path: msmtprc + {{- else }} + configMap: + name: {{ include "phpfpm.fullname" $ }}-smtp-config + items: + - key: msmtprc + path: msmtprc + {{- end }} + {{- if and $.Values.smtp.auth.enabled (not $.Values.smtp.existingMsmtprcSecret) }} + - name: smtp-secrets + secret: + secretName: {{ $.Values.smtp.auth.existingSecret | default (printf "%s-smtp" (include "phpfpm.fullname" $)) }} + items: + - key: {{ $.Values.smtp.auth.existingSecretKey | default "password" }} + path: password + mode: 0644 + {{- end }} + {{- end }} +{{- end }} diff --git a/values.yaml b/values.yaml index 888e71d..c4b298d 100644 --- a/values.yaml +++ b/values.yaml @@ -204,6 +204,41 @@ selenium: # では不要なため、既定はfalse。 mountHtml: false +# 定期実行タスク(Kubernetes CronJob) +# php-fpmと同じPHPイメージ(image.php)を使い、指定したスケジュールで +# 1回限りのコマンドを実行するCronJobを作成する。複数ジョブを配列で定義できる。 +# jobsが空の場合、CronJobは1つも作成されない。 +# +# 【マウント/環境変数】/var/wwwにPVCルートをマウントし、externalDatabase/phpEnv/ +# extraEnv/extraEnvFrom/SELENIUM_URLはphp-fpmコンテナと同じ内容が渡される。 +# html/ 配下の公開コード、または /var/www/<非公開フォルダ> 配下の +# 非公開スクリプト・出力先のどちらも利用できる。 +# +# 【Composer/Pythonとの併用について】 +# composer.workingDir / python.venvDir が既定の /composer, /venv +# (PVC非マウントの使い捨て領域)のままの場合、CronJobのPodには存在しない +# (php-fpmの長時間稼働Pod内だけに存在する)ため利用できない。 +# CronJobからComposer/Pythonの成果物を使いたい場合は、 +# composer.workingDir: "/var/www/composer" のように/var/www配下へ変更し、 +# PVC上に永続化すること(本体Deployment側で一度セットアップされていれば、 +# CronJob側は改めてインストールし直す必要はない)。 +# +# 【SMTPとの併用】smtp.enabled: true の場合、CronJobのPodでもmsmtpの導入・ +# msmtprc配置・sendmail_path設定を自動で行うため、PHPのmail()関数がそのまま使える。 +cron: + jobs: [] + # - name: daily-report # 必須。CronJob名の一部に使用(英数字とハイフンのみ、小文字推奨) + # schedule: "0 18 * * *" # 必須。標準cron形式(timeZone省略時はUTC基準) + # command: "php /var/www/html/cron/daily-report.php" + # timeZone: "Asia/Tokyo" # 省略時はUTC(Kubernetes 1.27+で安定サポート) + # concurrencyPolicy: "Forbid" # Allow / Forbid / Replace(既定: Forbid。前回実行の長期化による多重起動を防止) + # restartPolicy: "OnFailure" # OnFailure / Never(既定: OnFailure) + # successfulJobsHistoryLimit: 3 + # failedJobsHistoryLimit: 1 + # activeDeadlineSeconds: 3600 # 実行タイムアウト(秒)。省略可 + # suspend: false # trueにするとコード変更なしでこのジョブだけ一時停止できる + # resources: {} # 未指定時は resources.phpfpm を流用 + externalDatabase: enabled: false host: mariadb-hostname