diff --git a/.gitea/workflows/helm-release.yaml b/.gitea/workflows/helm-release.yaml new file mode 100644 index 0000000..7184a24 --- /dev/null +++ b/.gitea/workflows/helm-release.yaml @@ -0,0 +1,104 @@ +# .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_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}" + + 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 new file mode 100644 index 0000000..fa96623 --- /dev/null +++ b/.gitea/workflows/image-update-and-release.yaml @@ -0,0 +1,197 @@ +name: Update Docker Image Tags and Release Helm Chart + +on: + schedule: + - cron: '0 3 * * 1' # 毎週月曜日 3:00 AM (JST 12:00 PM) + workflow_dispatch: + +env: + REGISTRY_URL: https://git.cafepieters.com + OWNER: helmchart + +jobs: + update-and-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: 'v3.12.0' + + - name: Check jq availability + run: | + if ! command -v jq &> /dev/null; then + echo "Installing jq..." + apt-get update && apt-get install -y jq + fi + jq --version + + - name: Check for new n8n version + id: n8n + run: | + set -e + echo "Checking n8n versions..." + CURRENT=$(grep "tag:" values.yaml | head -1 | sed 's/.*tag: *"\([^"]*\)".*/\1/' | tr -d ' ') + echo "Current n8n: $CURRENT" + + LATEST=$(curl -s "https://registry.hub.docker.com/v2/repositories/n8nio/n8n/tags?page_size=100" | \ + jq -r '.results[].name' | \ + grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | \ + sort -V | tail -1) + + if [ -z "$LATEST" ]; then + echo "Warning: Could not fetch latest n8n version, using current" + LATEST="$CURRENT" + fi + + echo "Latest n8n: $LATEST" + echo "current=$CURRENT" >> $GITHUB_OUTPUT + echo "latest=$LATEST" >> $GITHUB_OUTPUT + + - name: Determine update and release conditions + id: check_update + run: | + set -e + N8N_CURRENT="${{ steps.n8n.outputs.current }}" + N8N_LATEST="${{ steps.n8n.outputs.latest }}" + + echo "n8n: $N8N_CURRENT vs $N8N_LATEST" + + UPDATE_NEEDED=false + RELEASE_NEEDED=false + + if [ "$N8N_CURRENT" != "$N8N_LATEST" ]; then + UPDATE_NEEDED=true + RELEASE_NEEDED=true + echo "Update and release needed" + else + echo "Already up to date" + fi + + echo "update_needed=$UPDATE_NEEDED" >> $GITHUB_OUTPUT + echo "release_needed=$RELEASE_NEEDED" >> $GITHUB_OUTPUT + + - name: Update values.yaml + if: steps.check_update.outputs.update_needed == 'true' + run: | + set -e + echo "Updating values.yaml..." + N8N_OLD="${{ steps.n8n.outputs.current }}" + N8N_NEW="${{ steps.n8n.outputs.latest }}" + + sed -i "s/tag: \"${N8N_OLD}\"/tag: \"${N8N_NEW}\"/" values.yaml + echo "n8n updated: $N8N_OLD -> $N8N_NEW" + + echo "values.yaml updated" + git diff values.yaml + + - name: Update Chart.yaml version + if: steps.check_update.outputs.release_needed == 'true' + run: | + set -e + APP_VERSION="${{ steps.n8n.outputs.latest }}" + 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 + + - name: Commit changes + if: steps.check_update.outputs.update_needed == 'true' + run: | + git config user.name "Claude" + git config user.email "claude@cafepieters.com" + git add values.yaml Chart.yaml + git commit -m "chore: update n8n to ${{ steps.n8n.outputs.latest }}" + git push origin main + + - name: Package Helm Chart + if: steps.check_update.outputs.release_needed == 'true' + run: | + helm package . + echo "Helm chart packaged" + + - name: Create Git Tag + if: steps.check_update.outputs.release_needed == 'true' + run: | + APP_VERSION="${{ steps.n8n.outputs.latest }}" + if git rev-parse "v$APP_VERSION" >/dev/null 2>&1; then + echo "Tag v$APP_VERSION already exists, skipping" + else + git tag -a "v$APP_VERSION" -m "Release n8n $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.release_needed == 'true' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + APP_VERSION="${{ steps.n8n.outputs.latest }}" + CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}') + PACKAGE_FILE="${CHART_NAME}-${APP_VERSION}.tgz" + RELEASE_BODY="n8n Helm Chart v${APP_VERSION} - n8n: ${{ steps.n8n.outputs.latest }}" + + EXISTING=$(curl -s \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/v${APP_VERSION}" | jq -r '.id // empty') + + if [ -n "$EXISTING" ]; then + echo "Release v$APP_VERSION already exists (id=$EXISTING), skipping" + else + RELEASE_ID=$(curl -s -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" | 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.release_needed == 'true' + run: | + CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}') + APP_VERSION="${{ steps.n8n.outputs.latest }}" + PACKAGE_FILE="${CHART_NAME}-${APP_VERSION}.tgz" + + 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}" \ + "${REGISTRY_URL}/api/packages/${OWNER}/helm/api/charts" + + echo "Chart published to registry successfully" + + - name: Summary + run: | + APP_VERSION="${{ steps.n8n.outputs.latest }}" + UPDATE_NEEDED="${{ steps.check_update.outputs.update_needed }}" + RELEASE_NEEDED="${{ steps.check_update.outputs.release_needed }}" + echo "========================================" + if [ "$UPDATE_NEEDED" = "true" ]; then + echo "Update completed!" + else + echo "Already up to date, no changes." + fi + echo "========================================" + echo "n8n: ${APP_VERSION}" + if [ "$RELEASE_NEEDED" = "true" ]; then + echo "Chart Version: ${APP_VERSION} (released)" + echo "Registry: ${REGISTRY_URL}/api/packages/${OWNER}/helm" + else + echo "No release needed" + fi + echo "========================================" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..938d5b3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# n8n Helm Chart - CLAUDE.md + +## リポジトリ概要 + +n8n ワークフロー自動化ツールを Kubernetes 上にデプロイするHelmチャートです。 +Raspberry Pi などのベアメタル上で動作する Kubernetes クラスタを想定した構成になっています。 +`n8nio/n8n` 公式マルチアーキテクチャイメージ(linux/arm64 対応)を使用します。 + +## ARM 対応について + +`n8nio/n8n` は linux/arm64 に対応したマルチアーキテクチャイメージです。 +Raspberry Pi 4 以降(arm64)で動作します。 + +## Git 情報 + +- **ユーザー名**: Claude +- **メールアドレス**: claude@cafepieters.com +- **リポジトリ**: ssh://git@192.168.9.65/helmchart/n8n.git + +## チャート構成 + +| リソース | 説明 | +|---|---| +| Deployment | n8n 本体(シングルコンテナ) | +| Service | LoadBalancer / ClusterIP(ポート 5678) | +| PVC | n8n データ(ワークフロー・認証情報・SQLite DB)永続化 | +| Secret | 暗号化キー・Basic認証パスワード・DBパスワード | +| Ingress | オプション(nginx ingress controller 対応) | +| HPA | オプション(※SQLiteモード時はスケールアウト非推奨) | +| PDB | Pod Disruption Budget | +| NetworkPolicy | オプション | + +## データ永続化 + +n8n のデータは `/home/node/.n8n` に保存されます。 +`persistence.enabled: true`(デフォルト)で PVC に永続化されます。 +**persistence.enabled: false の場合、Pod 再起動でデータが失われます。** + +## データベース + +- デフォルト: SQLite(`/home/node/.n8n/database.sqlite`) +- 本番推奨: PostgreSQL(`n8n.database.type: postgresdb`) + +## リリースフローのルール + +### バージョン番号の方針 +- Helmチャートのバージョン番号(`Chart.yaml` の `version` / `appVersion`)は、**n8n のバージョン番号と同一**とする。 + +### 自動リリース条件 +- **n8n バージョン更新時**: `values.yaml` と `Chart.yaml` を更新し、Gitタグ・Giteaリリース・Gitea Package Registry への発行まで行う。 + +### 手動リリース(臨時) +- 改修作業などで手動リリースが必要な場合は、バージョン末尾にアルファベットを付与する。 + - 例: `2.19.2` → `2.19.2-a`, `2.19.2-b` + +## ワークフロー構成 + +### `.gitea/workflows/image-update-and-release.yaml` +毎週月曜日 3:00 AM(JST 12:00 PM)に自動実行され、以下を行う: +1. Docker Hub から n8n の最新バージョンを取得 +2. 更新がある場合は `values.yaml` と `Chart.yaml` を更新 +3. Gitタグ・Giteaリリース・Gitea Package Registry への発行を実施 + +### `.gitea/workflows/helm-release.yaml` +`main` ブランチへのプッシュ時に自動実行。Gitea Package Registry にチャートを発行する。 + +## 必要な Gitea Secrets + +| シークレット名 | 用途 | +|---|---| +| `GITEA_TOKEN` | Gitea API(リリース作成・タグ操作) | +| `REGISTRY_USER` | Gitea Package Registry ユーザー名 | +| `REGISTRY_TOKEN` | Gitea Package Registry トークン | diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..ff29832 --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: n8n +description: A Helm chart for n8n workflow automation on Kubernetes (ARM/Raspberry Pi ready) +type: application +version: "2.19.2" +appVersion: "2.19.2" +keywords: + - n8n + - workflow + - automation + - integration +maintainers: + - name: Pieter + url: https://git.cafepieters.com/helmchart/repo/ +home: https://n8n.io/ +sources: + - https://github.com/n8n-io/n8n +icon: https://n8n.io/favicon.ico +kubeVersion: ">=1.19.0-0" +annotations: + category: Automation + licenses: Sustainable Use License diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..f57375a --- /dev/null +++ b/templates/_helpers.tpl @@ -0,0 +1,74 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "n8n.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "n8n.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "n8n.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "n8n.labels" -}} +helm.sh/chart: {{ include "n8n.chart" . }} +{{ include "n8n.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "n8n.selectorLabels" -}} +app.kubernetes.io/name: {{ include "n8n.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "n8n.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "n8n.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Name of the secret holding the encryption key and auth credentials +*/}} +{{- define "n8n.secretName" -}} +{{- .Values.n8n.existingSecret | default (include "n8n.fullname" .) }} +{{- end }} + +{{/* +Name of the PVC for n8n data +*/}} +{{- define "n8n.pvcName" -}} +{{- .Values.persistence.existingClaim | default (include "n8n.fullname" .) }} +{{- end }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml new file mode 100644 index 0000000..3b86575 --- /dev/null +++ b/templates/deployment.yaml @@ -0,0 +1,149 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "n8n.selectorLabels" . | nindent 6 }} + strategy: + type: Recreate + template: + metadata: + annotations: + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "n8n.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "n8n.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: n8n + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 5678 + protocol: TCP + env: + - name: N8N_PORT + value: "5678" + - name: N8N_PROTOCOL + value: {{ .Values.n8n.protocol | quote }} + - name: N8N_HOST + value: {{ .Values.n8n.host | quote }} + {{- if .Values.n8n.webhookUrl }} + - name: WEBHOOK_URL + value: {{ .Values.n8n.webhookUrl | quote }} + {{- end }} + - name: GENERIC_TIMEZONE + value: {{ .Values.n8n.timezone | quote }} + - name: N8N_LOG_LEVEL + value: {{ .Values.n8n.logLevel | quote }} + - name: N8N_USER_FOLDER + value: "/home/node/.n8n" + - name: N8N_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ include "n8n.secretName" . }} + key: encryption-key + {{- if .Values.n8n.basicAuth.enabled }} + - name: N8N_BASIC_AUTH_ACTIVE + value: "true" + - name: N8N_BASIC_AUTH_USER + value: {{ .Values.n8n.basicAuth.user | quote }} + - name: N8N_BASIC_AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.n8n.basicAuth.existingSecret | default (include "n8n.fullname" .) }} + key: {{ .Values.n8n.basicAuth.passwordKey }} + {{- end }} + - name: EXECUTIONS_DATA_PRUNE + value: {{ .Values.n8n.executions.pruneData | quote }} + - name: EXECUTIONS_DATA_MAX_AGE + value: {{ .Values.n8n.executions.pruneDataMaxAge | quote }} + - name: EXECUTIONS_DATA_MAX_COUNT + value: {{ .Values.n8n.executions.pruneDataMaxCount | quote }} + - name: DB_TYPE + value: {{ .Values.n8n.database.type | quote }} + {{- if eq .Values.n8n.database.type "postgresdb" }} + - name: DB_POSTGRESDB_HOST + value: {{ .Values.n8n.database.postgresdb.host | quote }} + - name: DB_POSTGRESDB_PORT + value: {{ .Values.n8n.database.postgresdb.port | quote }} + - name: DB_POSTGRESDB_DATABASE + value: {{ .Values.n8n.database.postgresdb.database | quote }} + - name: DB_POSTGRESDB_USER + value: {{ .Values.n8n.database.postgresdb.user | quote }} + - name: DB_POSTGRESDB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.n8n.database.postgresdb.existingSecret | default (include "n8n.fullname" .) }} + key: {{ .Values.n8n.database.postgresdb.passwordKey }} + {{- end }} + {{- range $key, $val := .Values.n8n.extraEnv }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + successThreshold: {{ .Values.livenessProbe.successThreshold }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + successThreshold: {{ .Values.readinessProbe.successThreshold }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 10 }} + volumeMounts: + - name: data + mountPath: /home/node/.n8n + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "n8n.pvcName" . }} + {{- else }} + emptyDir: {} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/templates/hpa.yaml b/templates/hpa.yaml new file mode 100644 index 0000000..8f39243 --- /dev/null +++ b/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "n8n.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/templates/ingress.yaml b/templates/ingress.yaml new file mode 100644 index 0000000..f702e62 --- /dev/null +++ b/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "n8n.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/templates/networkPolicy.yaml b/templates/networkPolicy.yaml new file mode 100644 index 0000000..fba2860 --- /dev/null +++ b/templates/networkPolicy.yaml @@ -0,0 +1,22 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "n8n.selectorLabels" . | nindent 6 }} + policyTypes: + {{- toYaml .Values.networkPolicy.policyTypes | nindent 4 }} + {{- if .Values.networkPolicy.ingress }} + ingress: + {{- toYaml .Values.networkPolicy.ingress | nindent 4 }} + {{- end }} + {{- if .Values.networkPolicy.egress }} + egress: + {{- toYaml .Values.networkPolicy.egress | nindent 4 }} + {{- end }} +{{- end }} diff --git a/templates/pdb.yaml b/templates/pdb.yaml new file mode 100644 index 0000000..aa7a14c --- /dev/null +++ b/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "n8n.selectorLabels" . | nindent 6 }} + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- end }} + {{- if .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/templates/pvc.yaml b/templates/pvc.yaml new file mode 100644 index 0000000..00bc620 --- /dev/null +++ b/templates/pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.enabled (not (.Values.persistence.existingClaim)) -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/templates/secret.yaml b/templates/secret.yaml new file mode 100644 index 0000000..3667723 --- /dev/null +++ b/templates/secret.yaml @@ -0,0 +1,19 @@ +{{- if not .Values.n8n.existingSecret -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + encryption-key: {{ .Values.n8n.encryptionKey | default (randAlphaNum 32) | b64enc | quote }} + {{- if and .Values.n8n.basicAuth.enabled (not .Values.n8n.basicAuth.existingSecret) }} + basic-auth-password: {{ .Values.n8n.basicAuth.password | default (randAlphaNum 16) | b64enc | quote }} + {{- end }} + {{- if and (eq .Values.n8n.database.type "postgresdb") (not .Values.n8n.database.postgresdb.existingSecret) }} + postgres-password: {{ .Values.n8n.database.postgresdb.password | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/templates/service.yaml b/templates/service.yaml new file mode 100644 index 0000000..d0882c6 --- /dev/null +++ b/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "n8n.fullname" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "n8n.selectorLabels" . | nindent 4 }} diff --git a/templates/serviceaccount.yaml b/templates/serviceaccount.yaml new file mode 100644 index 0000000..5c629a4 --- /dev/null +++ b/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "n8n.serviceAccountName" . }} + labels: + {{- include "n8n.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..5cc50b1 --- /dev/null +++ b/values.yaml @@ -0,0 +1,209 @@ +# Default values for n8n +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + registry: docker.io + repository: n8nio/n8n + tag: "2.19.2" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + annotations: {} + name: "" + +podAnnotations: {} + +podSecurityContext: + fsGroup: 1000 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1000 + +service: + type: LoadBalancer + # type: ClusterIP + port: 5678 + targetPort: 5678 + annotations: {} + +ingress: + enabled: false + 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: n8n.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: n8n-tls + # hosts: + # - n8n.local + +# Resource limits suitable for Raspberry Pi +resources: + limits: + cpu: 1000m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - n8n + topologyKey: kubernetes.io/hostname + +# n8n specific configuration +n8n: + # Encryption key for stored credentials (auto-generated if not provided) + encryptionKey: "" + existingSecret: "" + + # Host and protocol settings (used for webhook URLs) + host: "n8n.local" + protocol: "http" + # webhookUrl: "https://n8n.example.com/" + + # Timezone + timezone: "Asia/Tokyo" + + # Log level: error, warn, info, verbose, debug + logLevel: "info" + + # Basic authentication + basicAuth: + enabled: false + user: "admin" + password: "" + existingSecret: "" + passwordKey: "basic-auth-password" + + # Execution data pruning + executions: + pruneData: true + pruneDataMaxAge: 336 # hours (14 days) + pruneDataMaxCount: 10000 + + # Database configuration + database: + # type: sqlite (default) or postgresdb + type: "sqlite" + # PostgreSQL settings (used when type=postgresdb) + postgresdb: + host: "postgres.default.svc.cluster.local" + port: 5432 + database: "n8n" + user: "" + password: "" + existingSecret: "" + passwordKey: "postgres-password" + + # Extra environment variables + extraEnv: {} + # extraEnv: + # N8N_METRICS: "true" + # N8N_DIAGNOSTICS_ENABLED: "false" + +# Persistent storage for n8n data (workflows, credentials, sqlite DB) +persistence: + enabled: true + storageClass: "" + accessMode: ReadWriteOnce + size: 5Gi + annotations: {} + # existingClaim: "" + +# Liveness and readiness probes +livenessProbe: + enabled: true + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 6 + successThreshold: 1 + +readinessProbe: + enabled: true + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + +# Network Policy +networkPolicy: + enabled: false + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + name: ingress-nginx + ports: + - protocol: TCP + port: 5678 + egress: + - to: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 443 + - protocol: TCP + port: 80 + - to: + - namespaceSelector: + matchLabels: + name: kube-system + ports: + - protocol: UDP + port: 53 + +# Pod Disruption Budget +podDisruptionBudget: + enabled: true + minAvailable: 1