From de1a4144b58052a493bfedf0ab1d360a645ef1bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 07:56:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=81=E3=83=A3=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=82=92=E3=82=BC=E3=83=AD=E3=81=8B=E3=82=89=E5=86=8D=E8=A8=AD?= =?UTF-8?q?=E8=A8=88(=E5=A4=96=E9=83=A8DB=E5=89=8D=E6=8F=90=E3=81=AEYOURLS?= =?UTF-8?q?+Nginx=E6=A7=8B=E6=88=90=E3=81=AB=E4=BD=9C=E3=82=8A=E7=9B=B4?= =?UTF-8?q?=E3=81=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧チャートは存在しないDocker Hubイメージ(yourls/yourls)を参照し、Bitnami MySQL サブチャートに依存するなど正しく動作しない状態だったため、YOURLS公式イメージ (docker.io/yourls, container-entrypoint.sh/config-container.php)の実装を確認した上で WordPressチャートと同一方針(外部DB、emptyDir+PVC分離、fsGroup非root実行、 real IP取得対応)でスクラッチから再構築した。 --- .agents/rules/claude-md.md | 7 + .gitea/workflows/helm-release.yaml | 106 ++++++ .../workflows/image-update-and-release.yaml | 321 ++++++++++-------- .gitignore | 2 + .helmignore | 22 ++ CLAUDE.md | 43 +++ Chart.yaml | 22 +- GEMINI.md | 23 ++ README.md | 224 ++++++++---- templates/NOTES.txt | 42 ++- templates/_helpers.tpl | 30 +- templates/configmap.yaml | 112 ++++-- templates/deployment.yaml | 214 ++++++++---- templates/ingress.yaml | 33 +- templates/pvc.yaml | 12 +- templates/secret.yaml | 13 +- templates/service.yaml | 7 +- values.yaml | 220 ++++++++---- 18 files changed, 1056 insertions(+), 397 deletions(-) create mode 100644 .agents/rules/claude-md.md create mode 100644 .gitea/workflows/helm-release.yaml create mode 100644 .gitignore create mode 100644 .helmignore create mode 100644 CLAUDE.md create mode 100644 GEMINI.md diff --git a/.agents/rules/claude-md.md b/.agents/rules/claude-md.md new file mode 100644 index 0000000..130ecd1 --- /dev/null +++ b/.agents/rules/claude-md.md @@ -0,0 +1,7 @@ +--- +trigger: always_on +description: Read the CLAUDE.md hierarchy via GEMINI.md before doing anything in this workspace +--- + +最初に、このディレクトリの `GEMINI.md` を読み、そこに列挙された CLAUDE.md を上から順にすべて読んで従うこと。 +Read `GEMINI.md` in this directory first, then read and follow every CLAUDE.md it lists, in order. diff --git a/.gitea/workflows/helm-release.yaml b/.gitea/workflows/helm-release.yaml new file mode 100644 index 0000000..0330d2f --- /dev/null +++ b/.gitea/workflows/helm-release.yaml @@ -0,0 +1,106 @@ +# .gitea/workflows/helm-release.yaml +# 汎用Helmチャート公開ワークフロー(全リポジトリ共通) + +name: Helm Chart Release + +on: + push: + branches: + - main + - master + workflow_dispatch: + +env: + REGISTRY_URL: https://git.cafepieters.com + OWNER: helmchart + +jobs: + release-chart: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + echo "🔄 Cloning repository..." + git clone --depth 1 $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git . + echo "✅ Repository cloned" + + - name: Install Helm + run: | + echo "📦 Installing Helm..." + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + helm version + echo "✅ Helm installed" + + - name: Get Chart Info + id: chart_info + run: | + # Chart.yamlからチャート名とバージョンを取得 + CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}') + CHART_VERSION=$(grep '^version:' Chart.yaml | awk '{print $2}') + + echo "Chart Name: ${CHART_NAME}" + echo "Chart Version: ${CHART_VERSION}" + + # GitHub Actionsの環境変数として設定 + echo "CHART_NAME=${CHART_NAME}" >> $GITHUB_ENV + echo "CHART_VERSION=${CHART_VERSION}" >> $GITHUB_ENV + + - name: Validate Chart + run: | + echo "🔍 Validating Helm chart..." + helm lint . + echo "✅ Chart validation passed" + + - name: Package Chart + run: | + echo "📦 Packaging Helm chart..." + helm package . + + CHART_FILE=$(ls *.tgz) + echo "✅ Packaged: ${CHART_FILE}" + echo "CHART_FILE=${CHART_FILE}" >> $GITHUB_ENV + + - name: Publish to Gitea Package Registry + run: | + echo "🚀 Publishing ${CHART_FILE} to Gitea Package Registry..." + + curl --fail-with-body \ + -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \ + -X POST \ + --upload-file "${CHART_FILE}" \ + "${REGISTRY_URL}/api/packages/${OWNER}/helm/api/charts" + + echo "✅ Chart published successfully!" + + - name: Summary + if: success() + run: | + echo "================================" + echo "✅ Deployment Successful!" + echo "================================" + echo "Repository: ${{ github.repository }}" + echo "Chart Name: ${CHART_NAME}" + echo "Chart Version: ${CHART_VERSION}" + echo "Chart File: ${CHART_FILE}" + echo "Branch: ${{ github.ref }}" + echo "Commit: ${{ github.sha }}" + echo "================================" + echo "" + echo "📦 Install with:" + echo "helm repo add cafepieters ${REGISTRY_URL}/api/packages/${OWNER}/helm" + echo "helm repo update" + echo "helm install my-${CHART_NAME} cafepieters/${CHART_NAME}" + + - name: Error Report + if: failure() + run: | + echo "================================" + echo "❌ Deployment Failed!" + echo "================================" + echo "Check the logs above for details" + echo "Common issues:" + echo "- Missing Chart.yaml" + echo "- Invalid Helm chart structure" + echo "- Missing REGISTRY_USER or REGISTRY_TOKEN secrets" + echo "- Insufficient permissions on Personal Access Token" + echo "================================" diff --git a/.gitea/workflows/image-update-and-release.yaml b/.gitea/workflows/image-update-and-release.yaml index 09fd738..8d4b3c3 100644 --- a/.gitea/workflows/image-update-and-release.yaml +++ b/.gitea/workflows/image-update-and-release.yaml @@ -2,96 +2,113 @@ name: Update Docker Image Tags and Release Helm Chart on: schedule: - - cron: '0 4 * * 1' # 毎週月曜日 4:00 AM (JST 1:00 PM) + - cron: "0 4 * * 1" # 毎週月曜日 04:00 UTC (JST 13:00) workflow_dispatch: jobs: update-and-release: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install Helm - uses: azure/setup-helm@v3 - with: - version: 'v3.12.0' - - - name: Check for new nginx version - id: nginx - run: | - set -e - echo "Checking nginx versions..." - CURRENT=$(grep -A3 "nginx:" values.yaml | grep "tag:" | head -1 | sed 's/.*tag: *"\([^"]*\)".*/\1/' | tr -d ' ') - echo "Current nginx: $CURRENT" - - # Docker Hub API v2を使用してタグを取得 - LATEST=$(curl -s "https://registry.hub.docker.com/v2/repositories/library/nginx/tags?page_size=100" | \ - jq -r '.results[].name' | \ - grep -E '^[0-9]+\.[0-9]+\.[0-9]+-alpine-perl$' | \ - sort -V | tail -1) - - if [ -z "$LATEST" ]; then - echo "Warning: Could not fetch latest nginx version, using current" - LATEST="$CURRENT" - fi - echo "Latest nginx: $LATEST" - echo "current=$CURRENT" >> $GITHUB_OUTPUT - echo "latest=$LATEST" >> $GITHUB_OUTPUT - - - name: Check for new YOURLS version + - name: Check for new YOURLS FPM Alpine version id: yourls run: | set -e - echo "Checking YOURLS versions..." + echo "Checking YOURLS fpm-alpine versions..." CURRENT=$(grep -A3 "yourls:" values.yaml | grep "tag:" | head -1 | sed 's/.*tag: *"\([^"]*\)".*/\1/' | tr -d ' ') echo "Current YOURLS: $CURRENT" - - # Docker Hub API v2を使用してタグを取得 - # パターン: 1.10.2-fpm-alpine 形式 - LATEST=$(curl -s "https://registry.hub.docker.com/v2/repositories/yourls/yourls/tags?page_size=100" | \ + + # 公式イメージは docker.io/yourls (docker-library配下)。 + # docker.io/yourls/yourls という別名前空間のイメージは非公式のため使用しない。 + LATEST=$(curl -s "https://hub.docker.com/v2/repositories/library/yourls/tags?page_size=100&name=fpm-alpine" | \ jq -r '.results[].name' | \ grep -E '^[0-9]+\.[0-9]+\.[0-9]+-fpm-alpine$' | \ - sort -V | tail -1) - + grep -v 'rc' | \ + grep -v 'beta' | \ + grep -v 'alpha' | \ + sort -V | \ + tail -n 1) + if [ -z "$LATEST" ]; then echo "Warning: Could not fetch latest YOURLS version, using current" LATEST="$CURRENT" fi - - # YOURLSバージョンを抽出 (1.10.2の部分) + APP_VERSION=$(echo "$LATEST" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') if [ -z "$APP_VERSION" ]; then - echo "Error: Could not extract YOURLS version" - exit 1 + echo "Warning: Could not extract YOURLS version from: $LATEST" + APP_VERSION=$(echo "$CURRENT" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') fi - + echo "Latest YOURLS: $LATEST" echo "YOURLS version: $APP_VERSION" + echo "current=$CURRENT" >> $GITHUB_OUTPUT echo "latest=$LATEST" >> $GITHUB_OUTPUT echo "app_version=$APP_VERSION" >> $GITHUB_OUTPUT + - name: Check for new Nginx Alpine Perl version + id: nginx + run: | + set -e + echo "Checking Nginx Alpine Perl versions..." + CURRENT=$(grep -A3 "nginx:" values.yaml | grep "tag:" | head -1 | sed 's/.*tag: *"\([^"]*\)".*/\1/' | tr -d ' ') + echo "Current Nginx: $CURRENT" + + LATEST=$(curl -s "https://hub.docker.com/v2/repositories/library/nginx/tags?page_size=100&name=alpine-perl" | \ + jq -r '.results[].name' | \ + grep -E '^[0-9]+\.[0-9]+\.[0-9]+-alpine-perl$' | \ + grep -v 'rc' | \ + grep -v 'beta' | \ + grep -v 'alpha' | \ + sort -V | \ + tail -n 1) + + if [ -z "$LATEST" ]; then + echo "Warning: Could not fetch latest Nginx version, using current" + LATEST="$CURRENT" + fi + echo "Latest Nginx: $LATEST" + echo "current=$CURRENT" >> $GITHUB_OUTPUT + echo "latest=$LATEST" >> $GITHUB_OUTPUT + - name: Determine if update is needed id: check_update run: | set -e - NGINX_CURRENT="${{ steps.nginx.outputs.current }}" - NGINX_LATEST="${{ steps.nginx.outputs.latest }}" YOURLS_CURRENT="${{ steps.yourls.outputs.current }}" YOURLS_LATEST="${{ steps.yourls.outputs.latest }}" - - echo "Nginx: $NGINX_CURRENT vs $NGINX_LATEST" + NGINX_CURRENT="${{ steps.nginx.outputs.current }}" + NGINX_LATEST="${{ steps.nginx.outputs.latest }}" + echo "YOURLS: $YOURLS_CURRENT vs $YOURLS_LATEST" - - if [ "$NGINX_CURRENT" != "$NGINX_LATEST" ] || [ "$YOURLS_CURRENT" != "$YOURLS_LATEST" ]; then - echo "update_needed=true" >> $GITHUB_OUTPUT + echo "Nginx: $NGINX_CURRENT vs $NGINX_LATEST" + + UPDATE_NEEDED=false + YOURLS_UPDATED=false + + if [ "$YOURLS_CURRENT" != "$YOURLS_LATEST" ]; then + UPDATE_NEEDED=true + YOURLS_UPDATED=true + echo "YOURLS update detected" + fi + + if [ "$NGINX_CURRENT" != "$NGINX_LATEST" ]; then + UPDATE_NEEDED=true + echo "Nginx update detected" + fi + + echo "update_needed=$UPDATE_NEEDED" >> $GITHUB_OUTPUT + echo "yourls_updated=$YOURLS_UPDATED" >> $GITHUB_OUTPUT + + if [ "$UPDATE_NEEDED" = "true" ]; then echo "Update is needed" else - echo "update_needed=false" >> $GITHUB_OUTPUT - echo "Already up to date" + echo "Already up to date - no action required" fi - name: Update values.yaml @@ -99,37 +116,30 @@ jobs: run: | set -e echo "Updating values.yaml..." - - # YOURLSバージョンを取得 - APP_VERSION="${{ steps.yourls.outputs.app_version }}" - - # Nginx更新 - NGINX_OLD="${{ steps.nginx.outputs.current }}" - NGINX_NEW="${{ steps.nginx.outputs.latest }}" - if [ "$NGINX_OLD" != "$NGINX_NEW" ]; then - # nginxセクションのtagのみを更新(1つ目のタグ) - sed -i "0,/tag: \"${NGINX_OLD}\"/s//tag: \"${NGINX_NEW}\"/" values.yaml - echo "Nginx updated: $NGINX_OLD -> $NGINX_NEW" - fi - - # YOURLS更新 + YOURLS_OLD="${{ steps.yourls.outputs.current }}" YOURLS_NEW="${{ steps.yourls.outputs.latest }}" if [ "$YOURLS_OLD" != "$YOURLS_NEW" ]; then - # yourlsセクションのtagのみを更新(2つ目のタグ) - sed -i "0,/tag: \"${NGINX_NEW}\"/b; s/tag: \"${YOURLS_OLD}\"/tag: \"${YOURLS_NEW}\"/" values.yaml + sed -i "s|tag: \"${YOURLS_OLD}\"|tag: \"${YOURLS_NEW}\"|g" values.yaml echo "YOURLS updated: $YOURLS_OLD -> $YOURLS_NEW" fi - + + NGINX_OLD="${{ steps.nginx.outputs.current }}" + NGINX_NEW="${{ steps.nginx.outputs.latest }}" + if [ "$NGINX_OLD" != "$NGINX_NEW" ]; then + sed -i "s|tag: \"${NGINX_OLD}\"|tag: \"${NGINX_NEW}\"|g" values.yaml + echo "Nginx updated: $NGINX_OLD -> $NGINX_NEW" + fi + echo "values.yaml updated" git diff values.yaml - name: Update Chart.yaml version - if: steps.check_update.outputs.update_needed == 'true' + if: steps.check_update.outputs.yourls_updated == 'true' run: | set -e APP_VERSION="${{ steps.yourls.outputs.app_version }}" - sed -i "s/^version: .*/version: \"$APP_VERSION\"/" Chart.yaml + sed -i "s/^version: .*/version: $APP_VERSION/" Chart.yaml sed -i "s/^appVersion: .*/appVersion: \"$APP_VERSION\"/" Chart.yaml echo "Chart.yaml updated to version $APP_VERSION" cat Chart.yaml @@ -138,105 +148,128 @@ jobs: if: steps.check_update.outputs.update_needed == 'true' run: | git config user.name "Gitea Actions Bot" - git config user.email "actions@gitea.local" - git add values.yaml Chart.yaml - git commit -m "chore: update to YOURLS ${{ steps.yourls.outputs.app_version }}, nginx ${{ steps.nginx.outputs.latest }}" - git push origin main + git config user.email "actions@git.cafepieters.com" + + if [ "${{ steps.check_update.outputs.yourls_updated }}" = "true" ]; then + git add values.yaml Chart.yaml + git commit -m "chore: update to YOURLS ${{ steps.yourls.outputs.app_version }}, nginx ${{ steps.nginx.outputs.latest }}" + else + git add values.yaml + git commit -m "chore: update nginx to ${{ steps.nginx.outputs.latest }} (no release)" + fi + + MAX_RETRIES=3 + RETRY_COUNT=0 + until git push origin main || [ $RETRY_COUNT -eq $MAX_RETRIES ]; do + RETRY_COUNT=$((RETRY_COUNT+1)) + echo "Push failed, retrying ($RETRY_COUNT/$MAX_RETRIES)..." + sleep 5 + git pull --rebase origin main + done + + if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo "ERROR: Failed to push after $MAX_RETRIES attempts" + exit 1 + fi + + echo "Successfully pushed changes to main branch" + + - name: Install Helm + if: steps.check_update.outputs.yourls_updated == 'true' + uses: azure/setup-helm@v3 + with: + version: 'latest' - name: Package Helm Chart - if: steps.check_update.outputs.update_needed == 'true' + if: steps.check_update.outputs.yourls_updated == 'true' run: | helm package . echo "Helm chart packaged" - name: Create Git Tag - if: steps.check_update.outputs.update_needed == 'true' + if: steps.check_update.outputs.yourls_updated == 'true' run: | APP_VERSION="${{ steps.yourls.outputs.app_version }}" - git tag -a "v$APP_VERSION" -m "Release YOURLS $APP_VERSION" - git push origin "v$APP_VERSION" - echo "Git tag v$APP_VERSION created" + + if git rev-parse "v$APP_VERSION" >/dev/null 2>&1; then + echo "Tag v$APP_VERSION already exists, skipping tag creation" + else + git tag -a "v$APP_VERSION" -m "Release YOURLS $APP_VERSION" + git push origin "v$APP_VERSION" + echo "Git tag v$APP_VERSION created" + fi - name: Create Gitea Release - if: steps.check_update.outputs.update_needed == 'true' + if: steps.check_update.outputs.yourls_updated == 'true' env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | APP_VERSION="${{ steps.yourls.outputs.app_version }}" CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}') PACKAGE_FILE="${CHART_NAME}-${APP_VERSION}.tgz" - RELEASE_BODY="YOURLS Helm Chart v${APP_VERSION} - YOURLS: ${{ steps.yourls.outputs.latest }}, Nginx: ${{ steps.nginx.outputs.latest }}" - - # リリースを作成 - curl -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"tag_name\":\"v${APP_VERSION}\",\"name\":\"v${APP_VERSION}\",\"body\":\"${RELEASE_BODY}\"}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" - - # リリースIDを取得 - RELEASE_ID=$(curl -s \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/v${APP_VERSION}" | jq -r '.id') - - # アセットをアップロード - curl -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/gzip" \ - --data-binary "@${PACKAGE_FILE}" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${PACKAGE_FILE}" - - echo "Release created with asset: ${PACKAGE_FILE}" + RELEASE_BODY="YOURLS Helm Chart v${APP_VERSION} - Automated release" - - name: Checkout Helm Repository - if: steps.check_update.outputs.update_needed == 'true' - uses: actions/checkout@v3 - with: - repository: helmchart/repo - token: ${{ secrets.GITEA_TOKEN }} - path: helm-repo - fetch-depth: 0 + if curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/v${APP_VERSION}" | jq -e '.id' >/dev/null 2>&1; then + echo "Release v$APP_VERSION already exists, skipping release creation" + else + curl -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"v${APP_VERSION}\",\"name\":\"v${APP_VERSION}\",\"body\":\"${RELEASE_BODY}\"}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" - - name: Update Helm Repository Index - if: steps.check_update.outputs.update_needed == 'true' + RELEASE_ID=$(curl -s \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/v${APP_VERSION}" | jq -r '.id') + + curl -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/gzip" \ + --data-binary "@${PACKAGE_FILE}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${PACKAGE_FILE}" + + echo "Release v${APP_VERSION} created with asset ${PACKAGE_FILE}" + fi + + - name: Publish to Gitea Package Registry + if: steps.check_update.outputs.yourls_updated == 'true' run: | set -e APP_VERSION="${{ steps.yourls.outputs.app_version }}" CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}') PACKAGE_FILE="${CHART_NAME}-${APP_VERSION}.tgz" - - echo "Updating Helm repository..." - - # パッケージファイルをHelmリポジトリにコピー - cp "${PACKAGE_FILE}" helm-repo/ - - cd helm-repo - - # index.yamlを生成/更新 - helm repo index . --url "https://git.cafepieters.com/helmchart/repo/raw/branch/main" - - # コミットしてプッシュ - git config user.name "Gitea Actions Bot" - git config user.email "actions@gitea.local" - git add "${PACKAGE_FILE}" index.yaml - git commit -m "chore: add ${CHART_NAME} v${APP_VERSION}" || echo "No changes to commit" - git push origin main - - echo "Helm repository updated successfully" + + echo "🚀 Publishing ${PACKAGE_FILE} to Gitea Package Registry..." + + curl --fail-with-body \ + -u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \ + -X POST \ + --upload-file "${PACKAGE_FILE}" \ + "https://git.cafepieters.com/api/packages/helmchart/helm/api/charts" + + echo "✅ Chart published successfully to Gitea Package Registry" - name: Summary - if: steps.check_update.outputs.update_needed == 'true' + if: always() run: | APP_VERSION="${{ steps.yourls.outputs.app_version }}" NGINX_VERSION="${{ steps.nginx.outputs.latest }}" - YOURLS_VERSION="${{ steps.yourls.outputs.latest }}" - echo "========================================" - echo "Update completed successfully!" - echo "========================================" - echo "Chart Version: ${APP_VERSION}" - echo "YOURLS: ${YOURLS_VERSION}" - echo "Nginx: ${NGINX_VERSION}" - echo "========================================" - echo "Helm repository updated at:" - echo "https://git.cafepieters.com/helmchart/repo" - echo "========================================" \ No newline at end of file + YOURLS_UPDATED="${{ steps.check_update.outputs.yourls_updated }}" + + echo "================================" + echo "Update completed!" + echo "- YOURLS: ${APP_VERSION}" + echo "- Nginx: ${NGINX_VERSION}" + echo "" + + if [ "$YOURLS_UPDATED" = "true" ]; then + echo "✅ YOURLS version updated - Release created (v${APP_VERSION})" + echo "YOURLS updated: ${{ steps.yourls.outputs.current }} -> ${{ steps.yourls.outputs.latest }}" + echo "Nginx updated: ${{ steps.nginx.outputs.current }} -> ${{ steps.nginx.outputs.latest }}" + elif [ "${{ steps.check_update.outputs.update_needed }}" = "true" ]; then + echo "ℹ️ Nginx only update - No release (waiting for next YOURLS update)" + echo "Nginx updated: ${{ steps.nginx.outputs.current }} -> ${{ steps.nginx.outputs.latest }}" + else + echo "ℹ️ Already up to date - no action required" + fi + echo "================================" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b4cbea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Claude Code local settings +.claude/ diff --git a/.helmignore b/.helmignore new file mode 100644 index 0000000..cdce731 --- /dev/null +++ b/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.gitea/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Helm packages directory (pre-built packages) +packages/ +*.tgz diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c49995c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md — yourls Helm Chart + +> **共通ルール(要約)** — 詳細は `P:\Claude\CLAUDE.md`(全体共通)と `P:\Claude\HelmChart\CLAUDE.md`(Helm チャート共通: リリースフロー・ワークフロー・クラスタ構成)を参照。 +> 作業前に pull / 機能変更時は README 更新 / 作業単位ごとに commit → push まで / コミットに AI クレジットを付けない / pull で入った変更を revert しない / Helm・PHP はローカル実行不可(Python のみ可)。 + +## リポジトリ概要 + +Raspberry Pi などのベアメタルで稼働することを想定した、Kubernetes 上で動作する Alpine Nginx + YOURLS(URL短縮サービス、PHP-FPM)の Helm チャート。 + +- **Gitea リポジトリ**: `ssh://git@192.168.9.65/helmchart/yourls` +- **Helm リポジトリ**: `https://git.cafepieters.com/api/packages/helmchart/helm` +- **公式イメージ**: `docker.io/yourls`(`docker-library`配下の公式イメージ。`yourls/yourls`という別名前空間のイメージは非公式であり誤り) + +## 実行環境について + +YOURLS本体(コアファイル一式)はイメージ内蔵のもの(`docker.io/yourls:-fpm-alpine`)をemptyDirにコピーして使い捨てで起動する(WordPressチャートと同じbitnami方式)。`user/plugins`・`user/pages`・`user/languages`(管理画面経由で追加されるプラグイン・カスタムページ・言語ファイル)のみPVCで永続化する。`user/config.php`はYOURLS公式イメージ同梱の`config-container.php`(環境変数を`getenv()`で読むだけのテンプレート。YOURLS本体はこのファイルに書き込まない)をPod起動のたびにコピーするだけで、永続化はしない。PHPの実行環境はイメージに依存し、本リポジトリにPHPコードは含まれない。 + +データベースは外部MySQL/MariaDBを前提とし、サブチャートは同梱しない(WordPress/phpfpmチャートと同一方針)。 + +## 設計上の注意(このチャート固有) + +- **YOURLS_SITEは固定値**: WordPressの`WP_HOME`/`WP_SITEURL`のように実行時のHTTP Hostヘッダーから自動導出されない。短縮URL生成に使う固定文字列のため、`values.yourls.site`を実際のドメインに必ず設定する必要がある。 +- **Cookie暗号化キー・自動生成パスワードの安定化**: `templates/_helpers.tpl`の`yourls-nginx.existingSecretValue`が`lookup`関数で既存Secretの値を参照し、`helm upgrade`のたびに`cookiekey`や自動生成された`admin-password`がローテーションされない(ログインセッション・既知のパスワードが無効化されるのを防ぐ)。`helm template`(クラスタ未接続)ではlookupが空を返すため、その場合のみ新しい値が生成される(クラスタに適用されないため実害はない)。 +- **liveness/readinessの使い分け**: `livenessProbe`は`/robots.txt`(YOURLSがDB接続前に無条件で200を返す固定応答。`yourls-loader.php`のソースで確認済み)を見ており、DB接続の一時的な断でPodが再起動ループに陥らないようにしている。`readinessProbe`は`/`(DB接続を要する)を見ており、DB未接続時はServiceの振り分けから外れる。 +- **VIP/グローバルIPの希少性**: 組織CLAUDE.mdの「実運用クラスタ構成」を踏まえ、`service.type`の既定は`ClusterIP`(+Ingress共有)。専用LoadBalancer IPが必要な場合のみ明示的に`LoadBalancer`へ変更する運用とする。 +- **非root実行**: `yourls`/`yourls-init`コンテナは`runAsUser/runAsGroup: 82`(www-data)で実行し、Pod全体に`fsGroup: 82`を設定してPVC/emptyDirの書き込み権限を確保する。`nginx`コンテナは公式イメージの挙動(masterはroot、workerは`nginx`ユーザー)に委ね、org内WordPressチャートと同じ方針でsecurityContextの上書きは行わない。 + +## チャート改修履歴 + +### ゼロからの再設計(2026-09-12, v1.10.6) + +**背景**: 旧チャートが正しく動作しないとの報告を受け、スクラッチで設計をやり直した。 + +**旧チャートの問題点**(調査により判明): +- `.gitea/workflows/image-update-and-release.yaml`が存在しない`docker.io/yourls/yourls`(個人名前空間)を追跡しており、正しい公式イメージ`docker.io/yourls`(`library/yourls`)ではなかった。 +- `image.yourls.tag`が`1.10.2-fpm-alpine`と古く(最新は`1.10.6-fpm-alpine`)、追跡ワークフローの参照先が誤っていたため自動更新も機能していなかった。 +- Bitnami MySQLサブチャートに依存しており、org内の他チャート(WordPress/phpfpm)が採用する「外部DB前提・サブチャート非同梱」の方針と乖離していた。ARM(Raspberry Pi)環境でのBitnamiサブチャートの動作も未検証だった。 +- セキュリティコンテキストが一切設定されておらず(root実行)、実IP取得・ヘルスチェックの設計(liveness/readinessの使い分け)も無かった。 +- `.gitea/workflows/helm-release.yaml`が存在せず、mainブランチpush時のチャート公開が自動化されていなかった。 + +**新チャートの設計方針**: [YOURLS/containers](https://github.com/YOURLS/containers)(公式Dockerイメージのソース)の`container-entrypoint.sh`・`Dockerfile.template`・`config-container.php`を実際に確認した上で、この組織で実績のあるWordPressチャートの構成パターン(emptyDir+PVC分離、fsGroup非root実行、real IP取得対応、extraEnvFrom/extraEnvによるsecret-manager連携)を踏襲して再構築した。 + +**対象ファイル**: 全ファイル(スクラッチから再作成)。詳細は README.md を参照。 diff --git a/Chart.yaml b/Chart.yaml index b2e8572..22ecd48 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,21 +1,23 @@ apiVersion: v2 name: yourls-nginx -description: A Helm chart for YOURLS URL shortener with NGINX +description: YOURLS (Your Own URL Shortener) with Nginx and PHP-FPM on Kubernetes type: application -version: 1.0.0 -appVersion: "1.10.2" +version: 1.10.6 +appVersion: "1.10.6" keywords: - yourls - url-shortener - nginx - php-fpm +maintainers: + - name: Pieter + url: https://git.cafepieters.com/helmchart/repo/ home: https://yourls.org/ sources: - https://github.com/YOURLS/YOURLS -maintainers: - - name: Pieter -dependencies: - - name: mysql - version: "9.x.x" - repository: "https://charts.bitnami.com/bitnami" - condition: mysql.enabled \ No newline at end of file + - https://github.com/YOURLS/containers +icon: https://yourls.org/images/yourls-logo.png +kubeVersion: ">=1.19.0-0" +annotations: + category: Networking + licenses: MIT diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..555c584 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,23 @@ +# GEMINI.md — Antigravity (Gemini) 用エントリポイント(HelmChart\yourls) + +このディレクトリのルールはすべて **`CLAUDE.md`** に記載されている。本ファイルにはルール本文を書かず、読むべき CLAUDE.md への参照だけを置く(正本は常に CLAUDE.md)。 + +## 必ず最初に実行すること + +以下のファイルを**上から順に**読み込み、すべての指示に従うこと。上位ほど広い範囲に適用される共通ルールで、下位は上位を前提とした補足・例外である。 + +1. `P:\Claude\CLAUDE.md` — 全組織共通ルール(Git 運用・コミット書式・実行環境・情報記載の原則) +2. `P:\Claude\HelmChart\CLAUDE.md` — HelmChart 組織共通ルール +3. `P:\Claude\HelmChart\yourls\CLAUDE.md` — yourls リポジトリ固有ルール(このディレクトリ) + +> Antigravity はワークスペース(または git ルート)直下のルールしか自動で読まず、親フォルダを遡らない。そのため上位階層のパスをここに明示している。`P:\Claude` はクラウド同期されており、デスクトップ・ノート PC とも同じパスで存在する。 + +## 読み替え + +- CLAUDE.md 中の「Claude」「Claude Code」は、Antigravity で作業する場合は Gemini 自身のことと読み替える。 +- Git のコミット者は Antigravity でも `user.name = Claude` / `user.email = claude@cafepieters.com` のまま使う(変更しない)。コミットメッセージに AI クレジットを付けない。 +- Claude Code 固有の機能(`.claude/` 設定、スキル、メモリ等)に関する記述は無視してよい。 + +## 保守ルール + +- ルールの追加・変更は **CLAUDE.md にのみ**行う。本ファイルと `.agents/rules/claude-md.md` は参照だけを保持し、内容を増やさない。 diff --git a/README.md b/README.md index 040828d..0c39f3b 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,53 @@ -# YOURLS with NGINX Helm Chart +# YOURLS with Nginx Helm Chart -このHelmチャートは、YOURLS (Your Own URL Shortener) をNGINXとPHP-FPMの構成でKubernetesにデプロイします。 +[YOURLS](https://yourls.org/)(Your Own URL Shortener)をNginx + PHP-FPMの構成でKubernetesにデプロイするHelmチャートです。Raspberry Piなどのベアメタル(ARM)クラスタでの運用を想定しています。 -## 特徴 +## 主な機能 -- NGINX (1.29.3-alpine-perl) + YOURLS (1.10.2-fpm-alpine) のマルチコンテナPod構成 -- MySQL/MariaDBのサブチャートによるデータベース管理 -- 永続ボリュームによるデータ保存 -- Ingressサポート (オプション) -- セキュアな環境変数管理 -- リソース制限とヘルスチェック +- Nginx(alpine-perl) + YOURLS公式イメージ(fpm-alpine)のマルチコンテナPod構成 +- 外部MySQL/MariaDBを利用(サブチャート非同梱。WordPress/phpfpmチャートと同一方針) +- YOURLS本体はemptyDirに展開する使い捨て構成(bitnami方式)。管理画面から追加した + プラグイン・カスタムページ・言語ファイルのみPVCで永続化 +- 非root実行(uid/gid 82 = www-data)+ 読み取り専用ルートファイルシステムマウントによる多層防御 +- Ingress/LoadBalancer両対応、ベアメタル環境での実クライアントIP取得に対応 +- secret-manager等の外部Secretからのパスワード参照(`existingSecret`)、`extraEnvFrom`/`extraEnv`による環境変数の追加注入 +- helm upgradeを跨いでもCookie暗号化キー・自動生成パスワードがローテーションしない設計 + +## アーキテクチャとセキュリティ + +### ストレージ構成 + +``` +/var/www/html/ ← emptyDir (Pod再起動で消える。YOURLS本体を都度展開) +├── admin/, includes/, css/, js/, images/, yourls-*.php ... (イメージから展開) +└── user/ + ├── config.php ← 環境変数から都度生成(永続化しない) + ├── plugins/ ┐ + ├── pages/ ├─ PVCの該当パスをsubPathマウント(永続化) + └── languages/ ┘ +``` + +`user/config.php`はYOURLS公式イメージ同梱の`config-container.php`をコピーしただけのもので、中身は`getenv()`で環境変数を読むだけのテンプレートです。YOURLS本体がこのファイルに書き込むことはないため、Pod起動のたびに生成し直しても情報は失われません。これにより`values.yaml`の変更が再起動のたびに確実に反映されます。 + +### セキュリティ上の利点 + +- YOURLS本体のコードはPodごとに公式イメージからクリーンに展開され、実行時に改ざんされても再起動で元に戻る +- `yourls`/`yourls-init`コンテナは非root(uid/gid 82)・`allowPrivilegeEscalation: false`・全capability drop +- `yourls`コンテナの`/var/www/html`マウントは読み取り専用(`user/plugins`等の永続領域のみ書き込み可) +- DB接続情報を保持する`user/config.php`・内部処理用の`includes/`ディレクトリへの直接アクセスをNginx側で拒否(多層防御。通常はfastcgi経由でPHPとして実行されるため到達しない) +- `automountServiceAccountToken: false`(不要なKubernetes APIトークンをPodにマウントしない) ## 前提条件 - Kubernetes 1.19+ - Helm 3.0+ -- PersistentVolumeプロビジョナーのサポート (永続化を有効にする場合) +- 到達可能な外部MySQL/MariaDB(YOURLS用データベースを別途用意) +- PersistentVolumeプロビジョナー(永続化を有効にする場合) ## ディレクトリ構造 ``` -yourls-nginx/ +yourls/ ├── Chart.yaml ├── values.yaml ├── templates/ @@ -35,100 +62,163 @@ yourls-nginx/ └── README.md ``` -## インストール +## インストール方法 -### 1. 依存関係の更新 +### 1. データベースの準備 -```bash -helm dependency update +外部MySQL/MariaDB上に、YOURLS用のデータベースとユーザーを作成してください(テーブルは初回admin画面アクセス時に自動作成されるため、当該ユーザーに`CREATE`/`ALTER`権限が必要です)。 + +```sql +CREATE DATABASE yourls CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'yourls'@'%' IDENTIFIED BY 'securepassword'; +GRANT ALL PRIVILEGES ON yourls.* TO 'yourls'@'%'; +FLUSH PRIVILEGES; ``` -### 2. 基本インストール +### 2. インストール ```bash -helm install my-yourls ./yourls-nginx +helm install my-yourls . \ + --set yourls.site="https://sho.rt" \ + --set yourls.admin.username=admin \ + --set yourls.admin.password='SecureAdminPassword' \ + --set database.host=mysql.example.svc.cluster.local \ + --set database.user=yourls \ + --set database.password='securepassword' ``` -### 3. カスタム設定でインストール +または `values.yaml` をカスタマイズしてインストール: ```bash -helm install my-yourls ./yourls-nginx \ - --set yourls.config.site=https://短縮.example.com \ - --set yourls.config.username=admin \ - --set yourls.config.password=SecurePassword123 \ - --set mysql.auth.password=SecureMySQLPassword +helm install my-yourls . -f values.yaml ``` -### 4. values.yamlをカスタマイズしてインストール - -```bash -# values.yamlを編集 -vim values.yaml - -# インストール -helm install my-yourls ./yourls-nginx -f values.yaml -``` - -## 設定項目 +## 設定パラメータ ### イメージ設定 | パラメータ | 説明 | デフォルト値 | |-----------|------|-------------| -| `image.nginx.registry` | NGINXイメージのレジストリ | `docker.io` | -| `image.nginx.repository` | NGINXイメージのリポジトリ | `nginx` | -| `image.nginx.tag` | NGINXイメージのタグ | `1.29.3-alpine-perl` | -| `image.yourls.registry` | YOURLSイメージのレジストリ | `docker.io` | -| `image.yourls.repository` | YOURLSイメージのリポジトリ | `yourls` | -| `image.yourls.tag` | YOURLSイメージのタグ | `1.10.2-fpm-alpine` | +| `image.nginx.tag` | Nginxイメージのタグ | `1.31.5-alpine-perl` | +| `image.yourls.tag` | YOURLSイメージのタグ(`docker.io/yourls`) | `1.10.6-fpm-alpine` | ### YOURLS設定 | パラメータ | 説明 | デフォルト値 | |-----------|------|-------------| -| `yourls.config.site` | YOURLSのサイトURL | `https://yourls.example.com` | -| `yourls.config.username` | 管理者ユーザー名 | `admin` | -| `yourls.config.password` | 管理者パスワード | `admin` | -| `yourls.config.cookiekey` | Cookieの暗号化キー | `modify_this_random_string` | -| `yourls.persistence.enabled` | データの永続化 | `true` | -| `yourls.persistence.size` | PVCのサイズ | `1Gi` | +| `yourls.site` | YOURLSインスタンスのURL(固定値。末尾スラッシュなし) | `https://your-own-domain-here.com` | +| `yourls.admin.username` | 管理者ユーザー名 | `admin` | +| `yourls.admin.password` | 管理者パスワード(空の場合は自動生成・Secretに保存) | `""` | +| `yourls.admin.existingSecret` | 管理者パスワードを既存Secretから取得する場合の名前 | `""` | +| `yourls.admin.existingSecretKey` | 上記Secret内のキー名 | `password` | +| `yourls.cookiekey` | Cookie暗号化キー(空の場合は自動生成・以後のupgradeでも保持) | `""` | +| `yourls.privateMode` | `true`: 管理画面ログイン必須(推奨) / `false`: 統計等を公開 | `true` | +| `yourls.hoursOffset` | サーバーのタイムゾーンオフセット(時間) | `0` | +| `yourls.lang` | 管理画面の表示言語 | `""` | -### MySQL設定 +### データベース設定 | パラメータ | 説明 | デフォルト値 | |-----------|------|-------------| -| `mysql.enabled` | MySQLサブチャートの有効化 | `true` | -| `mysql.auth.database` | データベース名 | `yourls` | -| `mysql.auth.username` | データベースユーザー名 | `yourls` | -| `mysql.auth.password` | データベースパスワード | `yourls` | -| `mysql.primary.persistence.size` | PVCのサイズ | `8Gi` | +| `database.host` | MySQL/MariaDBのホスト名 | `mysql.example.svc.cluster.local` | +| `database.port` | ポート番号 | `3306` | +| `database.name` | データベース名 | `yourls` | +| `database.user` | DBユーザー名 | `yourls` | +| `database.password` | DBパスワード(本番はexistingSecret推奨) | `changeme` | +| `database.existingSecret` | DBパスワードを既存Secretから取得する場合の名前 | `""` | +| `database.existingSecretKey` | 上記Secret内のキー名 | `password` | +| `database.tablePrefix` | テーブル接頭辞 | `yourls_` | -### Ingress設定 +### 外部Secret連携・追加環境変数 | パラメータ | 説明 | デフォルト値 | |-----------|------|-------------| +| `extraEnvFrom` | 既存Secret/ConfigMapを丸ごとenvFromで注入(`cred-*`等) | `[]` | +| `extraEnv` | 個別の環境変数を追加(value/valueFrom使用可)。`YOURLS_DEBUG`等、values化していない項目はこちら | `[]` | + +### Service / Ingress設定 + +| パラメータ | 説明 | デフォルト値 | +|-----------|------|-------------| +| `service.type` | Serviceタイプ(VIPが希少なため既定はClusterIP) | `ClusterIP` | +| `service.externalTrafficPolicy` | LoadBalancer/NodePort時のみ有効。`Local`で実IPを透過 | `Local` | | `ingress.enabled` | Ingressの有効化 | `false` | -| `ingress.className` | Ingress class名 | `nginx` | -| `ingress.hosts[0].host` | ホスト名 | `yourls.example.com` | +| `ingress.hostname` | ホスト名 | `yourls.example.com` | +| `ingress.tls` | `true`でTLSを自動設定(hostnameを使用) | `false` | + +### 永続化設定 + +| パラメータ | 説明 | デフォルト値 | +|-----------|------|-------------| +| `persistence.enabled` | `user/plugins`,`user/pages`,`user/languages`の永続化 | `true` | +| `persistence.size` | PVCのサイズ | `1Gi` | +| `persistence.storageClass` | StorageClass名 | `""`(クラスタ既定) | + +## 使用例 + +### Ingressでの公開(TLS付き) + +```bash +helm install my-yourls . \ + --set yourls.site="https://sho.rt" \ + --set database.host=mysql.example.svc.cluster.local \ + --set database.password='securepassword' \ + --set ingress.enabled=true \ + --set ingress.hostname=sho.rt \ + --set ingress.className=nginx \ + --set ingress.tls=true \ + --set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-issuer +``` + +### secret-managerチャートが配布するSecretをDBパスワードに使う + +```bash +helm install my-yourls . \ + --set database.existingSecret=cred-mysql \ + --set database.existingSecretKey=MYSQL_PASSWORD +``` + +### 管理者パスワードの確認(自動生成時) + +```bash +kubectl get secret my-yourls-yourls-nginx-secret \ + -o jsonpath="{.data.admin-password}" | base64 -d; echo +``` + +## 初期化の動作 + +### 新規インストール時 + +1. `yourls-init` initContainerがYOURLS本体のコアファイルをemptyDirへ展開 +2. `user/plugins`,`user/pages`,`user/languages`のPVCが空であれば、YOURLS同梱のデフォルト内容(サンプルプラグイン等)を投入 +3. `user/config.php`を環境変数駆動のテンプレートから生成 +4. Pod起動後、`{yourls.site}/admin/`へ初回アクセスするとYOURLSが自動的にテーブルを作成 + +### 再起動時 + +- コアファイルは毎回クリーンに再展開される(emptyDirは使い捨てのため) +- `user/plugins`等のPVCは既に中身があればそのまま保持され、デフォルト内容で上書きされない +- `user/config.php`も毎回再生成されるため、`values.yaml`のDB接続情報等の変更が確実に反映される ## アップグレード ```bash -helm upgrade my-yourls ./yourls-nginx -f values.yaml +helm upgrade my-yourls . -f values.yaml ``` +`yourls.cookiekey`や自動生成された`yourls.admin.password`は、Secretの既存値を参照して引き継がれるため、明示的に固定していなくても`helm upgrade`のたびに変わることはありません。 + ## アンインストール ```bash helm uninstall my-yourls ``` -## セキュリティに関する注意事項 +PVCは`helm uninstall`では削除されません。完全にデータを削除する場合は手動で削除してください。 -1. **デフォルトパスワードの変更**: インストール後すぐにデフォルトの管理者パスワードを変更してください -2. **COOKIEKEY**: `yourls.config.cookiekey`をランダムな文字列に変更してください -3. **データベースパスワード**: 本番環境では強力なパスワードを設定してください -4. **HTTPS**: 本番環境では必ずIngressでTLS/HTTPSを有効にしてください +```bash +kubectl delete pvc my-yourls-yourls-nginx-pvc +``` ## トラブルシューティング @@ -136,6 +226,7 @@ helm uninstall my-yourls ```bash kubectl get pods -l app.kubernetes.io/name=yourls-nginx +kubectl logs -c yourls-init # 初期化ログ(初回のみ) kubectl logs -c nginx kubectl logs -c yourls ``` @@ -146,9 +237,20 @@ kubectl logs -c yourls kubectl exec -it -c yourls -- env | grep YOURLS_DB ``` +### 管理画面にアクセスしてもテーブルが作成されない + +`database.user`に指定したDBユーザーに`CREATE`/`ALTER`権限があるか確認してください(初回アクセス時の自動テーブル作成に必要です)。 + ### 永続ボリュームの確認 ```bash kubectl get pvc kubectl describe pvc ``` + +## セキュリティに関する注意事項 + +1. **デフォルトの管理者パスワード・DBパスワードは必ず変更してください**(`yourls.admin.password`/`database.password`を明示指定するか、`existingSecret`を利用) +2. **`yourls.site`を実際のドメインに設定してください**(短縮URL生成に使う固定値です) +3. **本番環境では必ずHTTPSを有効化してください**(`ingress.tls: true`、または前段のLB/CDNでTLS終端) +4. **`yourls.privateMode`は既定で`true`です**(管理画面ログインなしでの短縮URL作成・統計閲覧を許可する場合のみ`false`に変更してください。詳細: https://yourls.org/privatepublic) diff --git a/templates/NOTES.txt b/templates/NOTES.txt index 0ef1940..5c534fa 100644 --- a/templates/NOTES.txt +++ b/templates/NOTES.txt @@ -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 \ No newline at end of file +- 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. diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 8646725..38d7fca 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -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 }} \ No newline at end of file +{{- 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 }} diff --git a/templates/configmap.yaml b/templates/configmap.yaml index f25aef9..2f05b2b 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -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; - } - } \ No newline at end of file + {{- with .Values.nginx.extraConfig }} + {{- . | nindent 8 }} + {{- end }} + } diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 75cb335..3f659dc 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -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 }} \ No newline at end of file + {{- end }} diff --git a/templates/ingress.yaml b/templates/ingress.yaml index 18be06d..89023f6 100644 --- a/templates/ingress.yaml +++ b/templates/ingress.yaml @@ -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 }} \ No newline at end of file +{{- end }} diff --git a/templates/pvc.yaml b/templates/pvc.yaml index 74da2a2..97fa3b8 100644 --- a/templates/pvc.yaml +++ b/templates/pvc.yaml @@ -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 }} \ No newline at end of file + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/templates/secret.yaml b/templates/secret.yaml index 4c489fe..b6f662d 100644 --- a/templates/secret.yaml +++ b/templates/secret.yaml @@ -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 }} \ No newline at end of file +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 }} diff --git a/templates/service.yaml b/templates/service.yaml index fe3eba9..f23ecb0 100644 --- a/templates/service.yaml +++ b/templates/service.yaml @@ -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 }} \ No newline at end of file + {{- include "yourls-nginx.selectorLabels" . | nindent 4 }} diff --git a/values.yaml b/values.yaml index 7001d3b..289878a 100644 --- a/values.yaml +++ b/values.yaml @@ -1,99 +1,199 @@ -# Default values for yourls-nginx chart -# This is a YAML-formatted file. +# デフォルト値設定 +replicaCount: 1 -# Image configurations image: nginx: registry: docker.io repository: nginx - tag: "1.29.3-alpine-perl" + tag: "1.31.5-alpine-perl" pullPolicy: IfNotPresent yourls: + # 公式イメージ(docker-library/YOURLS)。fpm-alpineバリアントはPHP-FPMのみで + # リバースプロキシ(nginx)が別途必要な構成。 registry: docker.io repository: yourls - tag: "1.10.2-fpm-alpine" + tag: "1.10.6-fpm-alpine" pullPolicy: IfNotPresent -# Replica count -replicaCount: 1 +# YOURLS設定 +yourls: + # YOURLSインスタンスのURL。末尾スラッシュなし、小文字。 + # 必ず実際のドメインに変更してください(短縮URL生成に使用される固定値のため、 + # WordPressのように実行時のHTTP Hostヘッダーから自動判定されません)。 + site: "https://your-own-domain-here.com" -# Service configuration + admin: + username: "admin" + # 空の場合はSecret作成時にランダム生成されます(helm upgradeを跨いで値は保持されます)。 + # 本番環境では明示的に設定するか、existingSecretの使用を推奨します。 + password: "" + # 【外部Secret連携】secret-managerチャート等、既存のSecretからパスワードを取得する場合に指定。 + # 指定時は上記 password は無視されます。 + existingSecret: "" + existingSecretKey: "password" + + # Cookie暗号化キー。空の場合はSecret作成時にランダム生成され、以後のhelm upgradeでも + # 同じ値が再利用されます(lookup関数でSecretの既存値を参照するため、ログインセッションが + # アップグレードのたびに無効化されることはありません)。 + cookiekey: "" + + # true(デフォルト): 管理画面へのアクセスにログインが必須(YOURLS推奨の安全な既定値)。 + # false: 統計ページ等を公開する場合に変更してください。詳細: https://yourls.org/privatepublic + privateMode: true + + # サーバーのタイムゾーンオフセット(時間) + hoursOffset: 0 + + # 管理画面の表示言語。user/languages に該当する.moファイルがある場合のみ有効。 + lang: "" + +# データベース設定(外部MySQL/MariaDBを利用する構成。サブチャートは同梱しません) +database: + host: "mysql.example.svc.cluster.local" + port: 3306 + name: "yourls" + user: "yourls" + # 本番環境ではexistingSecretの使用を推奨します + password: "changeme" + # 【外部Secret連携】secret-managerチャート等、既存のSecretからパスワードを取得する場合に指定。 + # 指定時は上記 password は無視されます。 + existingSecret: "" + existingSecretKey: "password" + tablePrefix: "yourls_" + +# 既存の Secret / ConfigMap をまるごと環境変数として yourls コンテナへ注入する(envFrom)。 +# 別リポジトリの secret-manager チャートが各ネームスペースへ配布する +# cred-<名前> Secret(API キー等)をそのまま読み込む用途を想定。 +extraEnvFrom: [] +# - secretRef: +# name: cred-gemini + +# 個別の環境変数を追加する(生の env エントリ配列。value / valueFrom(secretKeyRef 等) 使用可)。 +# YOURLS_DEBUG, YOURLS_UNIQUE_URLS, YOURLS_URL_CONVERT, YOURLS_NOSTATS 等、 +# 本チャートが直接values化していないYOURLSの環境変数はこちらで指定してください。 +extraEnv: [] +# - name: YOURLS_UNIQUE_URLS +# value: "true" + +nginx: + # Ingress ControllerやCDN(Cloudflare等)、外部LB等、X-Forwarded-Forヘッダーを + # 付与するリバースプロキシを前段に置く場合にのみ有効にしてください。 + # Service直下にPodがぶら下がるベアメタル/MetalLB構成では、代わりに下記の + # service.externalTrafficPolicy: Local を使用してください。 + forwardRealIP: + enabled: false + trustedProxies: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + # CloudflareやAWS ALB等を使っている場合は以下のIPレンジも追加してください + # - 173.245.48.0/20 + + extraConfig: | + +# Service設定 service: + # VIP/グローバルIPが希少な環境を想定し、既定はClusterIP + Ingress(共有)としています。 + # 専用のLoadBalancer IPを払い出す場合のみ type: LoadBalancer に変更してください。 type: ClusterIP port: 80 targetPort: 80 + # ベアメタルクラスター(MetalLB等)でクライアントの実IPを取得するための設定。 + # kube-proxyはデフォルト(Cluster)だと送信元IPをノードIPにSNATしてしまうため、 + # Local にすることでSNATを回避し、Nginxの$remote_addrに実IPがそのまま渡ります。 + # LoadBalancer / NodePort でのみ有効(ClusterIPでは無視されます)。 + externalTrafficPolicy: Local -# Ingress configuration +# Ingress設定 ingress: enabled: false - className: "nginx" + className: nginx annotations: {} - # { - # acme.cert-manager.io/http01-ingress-class: "nginx", - # cert-manager.io/cluster-issuer: "letsencrypt-issuer", - # nginx.ingress.kubernetes.io/from-to-www-redirect: "true", - # nginx.ingress.kubernetes.io/proxy-body-size: "100m" - # } - hosts: - - host: example.tld - paths: - - path: / - pathType: Prefix - tls: [] - # - secretName: yourls-tls - # hosts: - # - example.tld + # コメントを外すだけで有効化できる設定例 + # cert-manager.io/cluster-issuer: "letsencrypt-issuer", + # nginx.ingress.kubernetes.io/ssl-redirect: "true", + # nginx.ingress.kubernetes.io/force-ssl-redirect: "true", + # nginx.ingress.kubernetes.io/proxy-body-size: "10m", + # IP取得用(Nginx Ingress Controllerの場合) + # nginx.ingress.kubernetes.io/use-forwarded-headers: "true", + # nginx.ingress.kubernetes.io/compute-full-forwarded-for: "true", + hostname: yourls.example.com + path: / + pathType: Prefix + # TLS設定(true にするだけで hostname を使用して自動設定) + tls: false + # tlsを有効にすると自動的に以下が設定されます: + # - hosts: [hostname] + # - secretName: {hostname}-tls -# MySQL/MariaDB configuration -mysql: + # 追加のホスト設定 + extraHosts: [] + # - name: go.example.com + # path: / + + # 追加のTLS設定 + extraTls: [] + # - hosts: + # - go.example.com + # secretName: go-example-com-tls + +# PersistentVolume設定 +# YOURLS本体(コアファイル)はemptyDirに配置(使い捨て)。 +# user/plugins, user/pages, user/languages(管理画面から追加したプラグイン・ページ・ +# 言語ファイル)のみPVCに永続化します。user/config.phpは環境変数から都度生成されるため +# 永続化しません(値を変更した場合に確実に反映されるようにするため)。 +persistence: enabled: true - auth: - rootPassword: "changeme" - database: "yourls" - username: "yourls" - password: "yourls" - primary: - persistence: - enabled: true - size: 8Gi + storageClass: "" + accessMode: ReadWriteOnce + size: 1Gi -# YOURLS configuration -yourls: - config: - site: "https://yourls.example.com" - username: "admin" - password: "admin" - cookiekey: "modify_this_random_string" - hours_offset: 0 - lang: "en" - persistence: - enabled: true - storageClass: "" - accessMode: ReadWriteOnce - size: 1Gi - -# Resources +# リソース設定 resources: nginx: limits: cpu: 200m - memory: 256Mi - requests: - cpu: 100m memory: 128Mi + requests: + cpu: 50m + memory: 32Mi yourls: limits: cpu: 500m - memory: 512Mi - requests: - cpu: 250m memory: 256Mi + requests: + cpu: 100m + memory: 64Mi -# Node selector +# Health Check +# livenessProbeは/robots.txt(YOURLSがDB接続前に無条件で200を返す固定応答)を見ており、 +# DB接続の一時的な断でコンテナが再起動ループに陥らないようにしています。 +# readinessProbeは/(DB接続を要する通常ページ)を見ており、DBに接続できない間は +# Serviceの振り分け対象から外れるようにしています。 +healthCheck: + enabled: true + livenessProbe: + httpGet: + path: /robots.txt + port: 80 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +# Node Selector nodeSelector: {} # Tolerations tolerations: [] # Affinity -affinity: {} \ No newline at end of file +affinity: {}