feat: チャートをゼロから再設計(外部DB前提のYOURLS+Nginx構成に作り直し)
Helm Chart Release / release-chart (push) Successful in 7s

旧チャートは存在しないDocker Hubイメージ(yourls/yourls)を参照し、Bitnami MySQL
サブチャートに依存するなど正しく動作しない状態だったため、YOURLS公式イメージ
(docker.io/yourls, container-entrypoint.sh/config-container.php)の実装を確認した上で
WordPressチャートと同一方針(外部DB、emptyDir+PVC分離、fsGroup非root実行、
real IP取得対応)でスクラッチから再構築した。
This commit is contained in:
2026-09-12 07:56:17 +09:00
parent a1cf9dced9
commit de1a4144b5
18 changed files with 1056 additions and 397 deletions
+25 -17
View File
@@ -9,36 +9,44 @@ To learn more about the release, try:
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
http{{ if or .Values.ingress.tls .Values.ingress.extraTls }}s{{ end }}://{{ .Values.ingress.hostname }}{{ .Values.ingress.path }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "yourls-nginx.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "yourls-nginx.fullname" . }}'
You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "yourls-nginx.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "yourls-nginx.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "yourls-nginx.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80
{{- end }}
2. Default credentials (CHANGE IMMEDIATELY):
Username: {{ .Values.yourls.config.username }}
Password: {{ .Values.yourls.config.password }}
2. Important: YOURLS_SITE must match the URL you access this instance from.
Current value: {{ .Values.yourls.site }}
Short URLs are generated using this fixed value, unlike WordPress it is NOT
derived automatically from the request. Update .Values.yourls.site (and
re-run helm upgrade) if it does not match your real domain yet.
3. Access the admin interface at:
http://your-domain/admin/
3. Default admin credentials:
Username: {{ .Values.yourls.admin.username }}
{{- if .Values.yourls.admin.existingSecret }}
Password: stored in existing Secret "{{ .Values.yourls.admin.existingSecret }}" (key: {{ .Values.yourls.admin.existingSecretKey }})
{{- else }}
Password: check the generated Secret if you did not set yourls.admin.password:
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "yourls-nginx.secretName" . }} -o jsonpath="{.data.admin-password}" | base64 -d; echo
{{- end }}
4. Access the admin interface at:
{{ .Values.yourls.site }}/admin/
(first visit creates the database tables automatically; the DB user needs
CREATE/ALTER privileges on {{ .Values.database.name }} for this to succeed)
IMPORTANT SECURITY NOTES:
- Change the default admin password immediately
- Update the YOURLS_COOKIEKEY in values.yaml to a random string
- Consider enabling HTTPS via Ingress with TLS
- Review and adjust resource limits based on your needs
- Change the default admin username/password before exposing this publicly.
- yourls.privateMode is {{ .Values.yourls.privateMode }} (true = admin login required to
create/manage short URLs; recommended default).
- Enable HTTPS via Ingress (ingress.tls: true) or your LoadBalancer/CDN in production.
+29 -1
View File
@@ -46,4 +46,32 @@ Selector labels
{{- define "yourls-nginx.selectorLabels" -}}
app.kubernetes.io/name: {{ include "yourls-nginx.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- end }}
{{/*
Secret name used for chart-managed credentials.
*/}}
{{- define "yourls-nginx.secretName" -}}
{{- printf "%s-secret" (include "yourls-nginx.fullname" .) }}
{{- end }}
{{/*
Fetch a previously-generated value from the existing chart-managed Secret, if any.
Used so that random values (cookiekey, auto-generated admin password) survive
"helm upgrade" instead of rotating on every release, which would otherwise
invalidate admin login cookies and previously-issued passwords.
Usage: {{ include "yourls-nginx.existingSecretValue" (list . "cookiekey") }}
Returns an empty string on first install (no Secret exists yet) or when
running "helm template" without cluster access.
*/}}
{{- define "yourls-nginx.existingSecretValue" -}}
{{- $ctx := index . 0 -}}
{{- $key := index . 1 -}}
{{- $secret := lookup "v1" "Secret" $ctx.Release.Namespace (include "yourls-nginx.secretName" $ctx) -}}
{{- if $secret -}}
{{- $value := index $secret.data $key -}}
{{- if $value -}}
{{- $value | b64dec -}}
{{- end -}}
{{- end -}}
{{- end }}
+89 -23
View File
@@ -6,51 +6,117 @@ metadata:
{{- include "yourls-nginx.labels" . | nindent 4 }}
data:
default.conf: |
upstream php-fpm {
server localhost:9000;
upstream php {
server 127.0.0.1:9000;
}
{{- if .Values.nginx.forwardRealIP.enabled }}
# 実IPアドレスの抽出(X-Forwarded-Forから最初のIPを取得)
map $http_x_forwarded_for $real_ip {
~^(\d+\.\d+\.\d+\.\d+) $1;
default $remote_addr;
}
{{- end }}
# HTTPSプロトコルの判定
map $http_x_forwarded_proto $fastcgi_https {
default '';
https on;
}
server {
listen 80;
server_name _;
root /var/www/html;
index index.php index.html;
# ルート直下は yourls-loader.php が、/admin/ 配下は admin/index.php がエントリポイント。
# ディレクトリ末尾スラッシュでのアクセス(例: /admin/)を両方とも正しく解決するため
# 2つ指定する(存在する方が使われる)。
index yourls-loader.php index.php;
client_max_body_size 100M;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
client_max_body_size 10M;
{{- if .Values.nginx.forwardRealIP.enabled }}
# 信頼できるプロキシからのX-Forwarded-Forヘッダーを使用
real_ip_header X-Forwarded-For;
{{- range .Values.nginx.forwardRealIP.trustedProxies }}
set_real_ip_from {{ . }};
{{- end }}
real_ip_recursive on;
{{- end }}
# DB接続情報等を保持する設定ファイルへの直接アクセスを拒否
# (通常はfastcgiで.phpとして実行されるため到達しないが、多層防御として明示的に拒否する)
location = /user/config.php {
deny all;
}
# 内部処理用ファイル群(直接リクエストされることを想定しない)へのアクセスを拒否
location ^~ /includes/ {
deny all;
}
# 隠しファイルへのアクセス拒否
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# 短縮URLのリダイレクト・favicon/robots.txt・管理画面を含む全ての動的処理は
# yourls-loader.php が担う(favicon.ico/robots.txtは物理ファイルが存在しないため、
# ここで意図的にPHPへフォールバックさせて動的応答させる)
location / {
try_files $uri $uri/ /yourls-loader.php$is_args$args;
}
# PHPファイルの処理
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm;
fastcgi_pass php;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_buffering off;
fastcgi_read_timeout 300;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
# HTTPS判定をPHP側に伝える
fastcgi_param HTTPS $fastcgi_https if_not_empty;
{{- if .Values.nginx.forwardRealIP.enabled }}
# プロキシ経由のリクエスト情報をPHPに伝える(リアルIP取得有効時)
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param REMOTE_ADDR $real_ip;
{{- else }}
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param REMOTE_ADDR $remote_addr;
{{- end }}
fastcgi_read_timeout 60;
fastcgi_send_timeout 60;
fastcgi_connect_timeout 60;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
# 静的ファイルのキャッシュ
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public";
log_not_found off;
access_log off;
}
location = /robots.txt {
log_not_found off;
access_log off;
allow all;
}
location ~* \.(css|gif|ico|jpeg|jpg|js|png|svg|woff|woff2|ttf|eot)$ {
expires max;
log_not_found off;
}
}
{{- with .Values.nginx.extraConfig }}
{{- . | nindent 8 }}
{{- end }}
}
+153 -61
View File
@@ -14,19 +14,74 @@ spec:
labels:
{{- include "yourls-nginx.selectorLabels" . | nindent 8 }}
spec:
automountServiceAccountToken: false
securityContext:
fsGroup: 82
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: wait-for-mysql
image: busybox:1.36
command:
- sh
- -c
- name: yourls-init
image: "{{ .Values.image.yourls.registry }}/{{ .Values.image.yourls.repository }}:{{ .Values.image.yourls.tag }}"
imagePullPolicy: {{ .Values.image.yourls.pullPolicy }}
command: ["/bin/sh", "-c"]
args:
- |
until nc -z {{ include "yourls-nginx.fullname" . }}-mysql 3306; do
echo "Waiting for MySQL..."
sleep 2
set -e
echo "=== YOURLS initialization started ==="
echo "Copying YOURLS core files..."
# user/ ディレクトリ(プラグイン・ページ・言語・設定)は別ボリュームで扱うため、
# コアファイルのコピー対象から除外する。
for item in /usr/src/yourls/*; do
name=$(basename "$item")
if [ "$name" = "user" ]; then
continue
fi
cp -r "$item" /var/www/html/
done
echo "Core files copied"
mkdir -p /var/www/html/user
# user/plugins, user/pages, user/languages はPVC(またはemptyDir)。
# 初回(空)の場合のみYOURLS同梱のデフォルト内容(サンプルプラグイン等)を投入し、
# 以降は管理画面経由で追加/削除された内容をPod再起動のたびに上書きしない。
for sub in plugins pages languages; do
target="/var/www/html/user/$sub"
mkdir -p "$target"
if [ -z "$(ls -A "$target" 2>/dev/null)" ]; then
echo "Seeding default user/$sub (first run)"
cp -rp "/usr/src/yourls/user/$sub/"* "$target/" 2>/dev/null || true
else
echo "user/$sub already has content, preserving"
fi
done
# user/config.phpは環境変数(getenv)を読むだけのテンプレートで、YOURLS自体が
# 書き込むことはないため、永続化せず毎回生成し直す(values.yaml変更を確実に反映)。
echo "Generating user/config.php from environment variables..."
cp /usr/src/yourls/user/config-container.php /var/www/html/user/config.php
echo "=== YOURLS initialization completed ==="
securityContext:
runAsUser: 82
runAsGroup: 82
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: yourls-data
mountPath: /var/www/html
- name: yourls-persistent
mountPath: /var/www/html/user/plugins
subPath: plugins
- name: yourls-persistent
mountPath: /var/www/html/user/pages
subPath: pages
- name: yourls-persistent
mountPath: /var/www/html/user/languages
subPath: languages
containers:
# NGINX Container
- name: nginx
image: "{{ .Values.image.nginx.registry }}/{{ .Values.image.nginx.repository }}:{{ .Values.image.nginx.tag }}"
imagePullPolicy: {{ .Values.image.nginx.pullPolicy }}
@@ -35,87 +90,124 @@ spec:
containerPort: 80
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
- name: yourls-data
mountPath: /var/www/html
readOnly: true
- name: yourls-persistent
mountPath: /var/www/html/user/plugins
subPath: plugins
readOnly: true
- name: yourls-persistent
mountPath: /var/www/html/user/pages
subPath: pages
readOnly: true
- name: yourls-persistent
mountPath: /var/www/html/user/languages
subPath: languages
readOnly: true
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
{{- if .Values.healthCheck.enabled }}
livenessProbe:
{{- toYaml .Values.healthCheck.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.healthCheck.readinessProbe | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources.nginx | nindent 12 }}
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
# YOURLS PHP-FPM Container
- name: yourls
image: "{{ .Values.image.yourls.registry }}/{{ .Values.image.yourls.repository }}:{{ .Values.image.yourls.tag }}"
imagePullPolicy: {{ .Values.image.yourls.pullPolicy }}
command: ["php-fpm"]
securityContext:
runAsUser: 82
runAsGroup: 82
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
env:
- name: YOURLS_DB_HOST
value: "{{ include "yourls-nginx.fullname" . }}-mysql"
- name: YOURLS_DB_USER
value: {{ .Values.mysql.auth.username | quote }}
- name: YOURLS_DB_PASS
valueFrom:
secretKeyRef:
name: {{ include "yourls-nginx.fullname" . }}-mysql
key: mysql-password
- name: YOURLS_DB_NAME
value: {{ .Values.mysql.auth.database | quote }}
- name: YOURLS_SITE
value: {{ .Values.yourls.config.site | quote }}
value: {{ .Values.yourls.site | quote }}
- name: YOURLS_USER
value: {{ .Values.yourls.config.username | quote }}
value: {{ .Values.yourls.admin.username | quote }}
- name: YOURLS_PASS
valueFrom:
secretKeyRef:
name: {{ include "yourls-nginx.fullname" . }}-secret
key: yourls-password
{{- if .Values.yourls.admin.existingSecret }}
name: {{ .Values.yourls.admin.existingSecret }}
key: {{ .Values.yourls.admin.existingSecretKey }}
{{- else }}
name: {{ include "yourls-nginx.secretName" . }}
key: admin-password
{{- end }}
- name: YOURLS_COOKIEKEY
valueFrom:
secretKeyRef:
name: {{ include "yourls-nginx.fullname" . }}-secret
key: yourls-cookiekey
name: {{ include "yourls-nginx.secretName" . }}
key: cookiekey
- name: YOURLS_DB_HOST
value: {{ printf "%s:%v" .Values.database.host .Values.database.port | quote }}
- name: YOURLS_DB_USER
value: {{ .Values.database.user | quote }}
- name: YOURLS_DB_PASS
valueFrom:
secretKeyRef:
{{- if .Values.database.existingSecret }}
name: {{ .Values.database.existingSecret }}
key: {{ .Values.database.existingSecretKey }}
{{- else }}
name: {{ include "yourls-nginx.secretName" . }}
key: db-password
{{- end }}
- name: YOURLS_DB_NAME
value: {{ .Values.database.name | quote }}
- name: YOURLS_DB_PREFIX
value: {{ .Values.database.tablePrefix | quote }}
- name: YOURLS_HOURS_OFFSET
value: {{ .Values.yourls.config.hours_offset | quote }}
value: {{ .Values.yourls.hoursOffset | quote }}
- name: YOURLS_LANG
value: {{ .Values.yourls.config.lang | quote }}
value: {{ .Values.yourls.lang | quote }}
- name: YOURLS_PRIVATE
value: {{ .Values.yourls.privateMode | quote }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.extraEnvFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
# YOURLS本体はinitContainerが書き込んだ後は変更不要なため読み取り専用でマウントする
# (実行時に侵害された場合でも配信コードの改ざん・永続化を防ぐ多層防御)。
- name: yourls-data
mountPath: /var/www/html
readOnly: true
- name: yourls-persistent
mountPath: /var/www/html/user/plugins
subPath: plugins
- name: yourls-persistent
mountPath: /var/www/html/user/pages
subPath: pages
- name: yourls-persistent
mountPath: /var/www/html/user/languages
subPath: languages
resources:
{{- toYaml .Values.resources.yourls | nindent 12 }}
livenessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: nginx-config
configMap:
name: {{ include "yourls-nginx.fullname" . }}-nginx-config
- name: yourls-data
{{- if .Values.yourls.persistence.enabled }}
emptyDir: {}
- name: yourls-persistent
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "yourls-nginx.fullname" . }}-pvc
{{- else }}
emptyDir: {}
{{- end }}
- name: nginx-config
configMap:
name: {{ include "yourls-nginx.fullname" . }}-nginx-config
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
@@ -127,4 +219,4 @@ spec:
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
+24 -9
View File
@@ -1,4 +1,4 @@
{{- if .Values.ingress.enabled -}}
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -13,29 +13,44 @@ spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
{{- if or .Values.ingress.tls .Values.ingress.extraTls }}
tls:
{{- range .Values.ingress.tls }}
{{- if .Values.ingress.tls }}
- hosts:
- {{ .Values.ingress.hostname | quote }}
secretName: {{ .Values.ingress.hostname | replace "." "-" }}-tls
{{- end }}
{{- if .Values.ingress.extraTls }}
{{- range .Values.ingress.extraTls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
- host: {{ .Values.ingress.hostname | quote }}
http:
paths:
- path: {{ .Values.ingress.path }}
pathType: {{ .Values.ingress.pathType }}
backend:
service:
name: {{ include "yourls-nginx.fullname" . }}
port:
number: {{ $.Values.service.port }}
{{- range .Values.ingress.extraHosts }}
- host: {{ .name | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
pathType: Prefix
backend:
service:
name: {{ include "yourls-nginx.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+6 -6
View File
@@ -1,4 +1,4 @@
{{- if .Values.yourls.persistence.enabled }}
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -7,11 +7,11 @@ metadata:
{{- include "yourls-nginx.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.yourls.persistence.accessMode }}
{{- if .Values.yourls.persistence.storageClass }}
storageClassName: {{ .Values.yourls.persistence.storageClass }}
- {{ .Values.persistence.accessMode }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.yourls.persistence.size }}
{{- end }}
storage: {{ .Values.persistence.size }}
{{- end }}
+9 -4
View File
@@ -1,10 +1,15 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "yourls-nginx.fullname" . }}-secret
name: {{ include "yourls-nginx.secretName" . }}
labels:
{{- include "yourls-nginx.labels" . | nindent 4 }}
type: Opaque
data:
yourls-password: {{ .Values.yourls.config.password | b64enc | quote }}
yourls-cookiekey: {{ .Values.yourls.config.cookiekey | b64enc | quote }}
stringData:
{{- if not .Values.database.existingSecret }}
db-password: {{ .Values.database.password | quote }}
{{- end }}
{{- if not .Values.yourls.admin.existingSecret }}
admin-password: {{ .Values.yourls.admin.password | default (include "yourls-nginx.existingSecretValue" (list . "admin-password")) | default (randAlphaNum 20) | quote }}
{{- end }}
cookiekey: {{ .Values.yourls.cookiekey | default (include "yourls-nginx.existingSecretValue" (list . "cookiekey")) | default (randAlphaNum 64) | quote }}
+6 -1
View File
@@ -6,10 +6,15 @@ metadata:
{{- include "yourls-nginx.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
{{- if and .Values.service.externalTrafficPolicy (ne .Values.service.type "ClusterIP") }}
# ベアメタル環境(MetalLB等)でkube-proxyのSNATを回避し、クライアントの実IPを
# $remote_addr にそのまま渡すための設定。LoadBalancer/NodePortでのみ有効。
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
{{- end }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "yourls-nginx.selectorLabels" . | nindent 4 }}
{{- include "yourls-nginx.selectorLabels" . | nindent 4 }}