feat: Alpine ベースの Claude Code リモート実行 Helm チャート初版 (v2.1.259)
gateway(nginx) + manager(python 標準ライブラリ) + セッションごとの使い捨て Pod + reaper CronJob の構成。ブラウザ(PC/スマホ)からアクセスし、セッション単位で 独立した Alpine コンテナに Git リポジトリ(GitHub/Gitea など)をチェックアウトして 編集・検証・プッシュする。無操作が続いたセッションは Pod ごと自動破棄。 - セキュリティ: セッション NS 分離 / restricted PodSecurity / RBAC はセッション NS の pods,services のみの Role / セッション Pod は SA トークン非マウント・非 root・ readOnlyRootFilesystem・drop ALL caps・seccomp / NetworkPolicy で egress を DNS+HTTPS+SSH に限定 / Git ホスト allow-list / ResourceQuota+LimitRange - ライフサイクル: idleTimeout(Pod 内ウォッチドッグ + manager 回収) / maxLifetime (activeDeadlineSeconds) / maxSessions / 予備の孤児掃除 CronJob - 認証: token(既定) / basic / forward(外部SSO) / none。WebSocket は Cookie トークン - Claude 認証: ANTHROPIC_API_KEY または CLAUDE_CODE_OAUTH_TOKEN - CI: helm-release.yaml(未リリース版のみ tag+release+publish) / image-update-and-release.yaml(週次 npm 追従) / session-image.yaml(multi-arch build) - files/ 実体を .Files.Get で ConfigMap 化。helm lint / template 検証済み
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Helm チャートのテキストは常に LF(CRLF はブロックスカラー等で問題を起こしうる)
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.tgz binary
|
||||
@@ -0,0 +1,131 @@
|
||||
# .gitea/workflows/helm-release.yaml
|
||||
# プロジェクトへの変更(main への push)時に実行。
|
||||
# Chart.yaml の version が「まだリリースされていない」場合のみ、
|
||||
# タグ作成 → Gitea リリース → Helm チャート publish を行う。
|
||||
# 既にリリース済みのバージョンなら lint のみで終了する(再 publish しない)。
|
||||
|
||||
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
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: 'v3.16.4'
|
||||
|
||||
- name: Ensure jq
|
||||
run: command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
- name: Get chart info
|
||||
id: info
|
||||
run: |
|
||||
set -e
|
||||
CHART_NAME=$(grep '^name:' Chart.yaml | awk '{print $2}')
|
||||
CHART_VERSION=$(grep '^version:' Chart.yaml | awk '{print $2}' | tr -d '"')
|
||||
echo "name=${CHART_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "version=${CHART_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Chart: ${CHART_NAME} ${CHART_VERSION}"
|
||||
|
||||
- name: Lint
|
||||
run: helm lint .
|
||||
|
||||
- name: Check whether this version is already released
|
||||
id: check
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
V="v${{ steps.info.outputs.version }}"
|
||||
# 1) Gitea リリースの有無
|
||||
REL_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${V}" | jq -r '.id // empty')
|
||||
# 2) Package Registry 上の有無
|
||||
PKG_HTTP=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
-u "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"${REGISTRY_URL}/api/packages/${OWNER}/helm/${{ steps.info.outputs.name }}/${{ steps.info.outputs.version }}")
|
||||
|
||||
if [ -n "$REL_ID" ] || [ "$PKG_HTTP" = "200" ]; then
|
||||
echo "release_needed=false" >> $GITHUB_OUTPUT
|
||||
echo "既に ${V} はリリース済み(release_id=${REL_ID:-none}, pkg_http=${PKG_HTTP})。publish をスキップします。"
|
||||
else
|
||||
echo "release_needed=true" >> $GITHUB_OUTPUT
|
||||
echo "${V} は未リリース。リリースを実行します。"
|
||||
fi
|
||||
|
||||
- name: Package
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
helm package .
|
||||
echo "CHART_FILE=$(ls ${{ steps.info.outputs.name }}-*.tgz)" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Git tag
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
git config user.name "Claude"
|
||||
git config user.email "claude@cafepieters.com"
|
||||
V="v${{ steps.info.outputs.version }}"
|
||||
if git rev-parse "$V" >/dev/null 2>&1; then
|
||||
echo "tag $V exists, skip"
|
||||
else
|
||||
git tag -a "$V" -m "Release $V"
|
||||
git push origin "$V"
|
||||
fi
|
||||
|
||||
- name: Create Gitea release
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
V="v${{ steps.info.outputs.version }}"
|
||||
BODY="${{ steps.info.outputs.name }} Helm Chart ${V}"
|
||||
RID=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${V}\",\"name\":\"${V}\",\"body\":\"${BODY}\"}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" | jq -r '.id // empty')
|
||||
if [ -z "$RID" ]; then
|
||||
RID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${V}" | jq -r '.id // empty')
|
||||
fi
|
||||
curl --fail-with-body -X POST -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/gzip" --data-binary "@${CHART_FILE}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RID}/assets?name=${CHART_FILE}"
|
||||
echo "Gitea release ${V} 作成完了"
|
||||
|
||||
- name: Publish to Gitea Package Registry
|
||||
if: steps.check.outputs.release_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
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_FILE} を publish しました"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "================================"
|
||||
echo "Chart : ${{ steps.info.outputs.name }} ${{ steps.info.outputs.version }}"
|
||||
echo "Released : ${{ steps.check.outputs.release_needed }}"
|
||||
echo "================================"
|
||||
echo "helm repo add cafepieters ${REGISTRY_URL}/api/packages/${OWNER}/helm"
|
||||
echo "helm repo update && helm install my-${{ steps.info.outputs.name }} cafepieters/${{ steps.info.outputs.name }}"
|
||||
@@ -0,0 +1,188 @@
|
||||
name: Update Versions and Release Helm Chart
|
||||
|
||||
# 毎週月曜 3:00 AM JST(日曜 18:00 UTC)に実行。
|
||||
# - 主アプリ = Claude Code CLI(npm: @anthropic-ai/claude-code)の最新版を確認
|
||||
# - base image = gateway(nginx) / manager(python) の最新 Alpine タグを確認
|
||||
# - 主アプリが更新: values.yaml / Chart.yaml / README 履歴を更新しコミット → タグ + Gitea リリース + チャート publish
|
||||
# - base image のみ更新: values.yaml を更新してコミットのみ(リリースなし)
|
||||
#
|
||||
# セッションイメージのビルドは session-image.yaml が担当する(このワークフローが
|
||||
# Chart.yaml を push した結果として発火する)。
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 18 * * 0'
|
||||
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.16.4'
|
||||
|
||||
- name: Ensure jq
|
||||
run: command -v jq >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y -qq jq)
|
||||
|
||||
- name: Check Claude Code (npm) version
|
||||
id: cc
|
||||
run: |
|
||||
set -e
|
||||
CURRENT=$(grep '^appVersion:' Chart.yaml | sed 's/.*"\(.*\)".*/\1/')
|
||||
LATEST=$(curl -s --retry 3 --retry-delay 5 \
|
||||
"https://registry.npmjs.org/@anthropic-ai/claude-code/latest" | jq -r '.version // empty')
|
||||
echo "Current: $CURRENT / npm latest: ${LATEST:-not found}"
|
||||
|
||||
if [ -n "$LATEST" ]; then
|
||||
HIGHER=$(printf '%s\n%s' "$CURRENT" "$LATEST" | sort -V | tail -1)
|
||||
if [ "$HIGHER" != "$LATEST" ] && [ "$CURRENT" != "$LATEST" ]; then
|
||||
echo "Warning: npm latest ($LATEST) < current ($CURRENT). Skipping."
|
||||
LATEST="$CURRENT"
|
||||
fi
|
||||
else
|
||||
LATEST="$CURRENT"
|
||||
fi
|
||||
echo "current=$CURRENT" >> $GITHUB_OUTPUT
|
||||
echo "latest=$LATEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check base image (nginx / python) tags
|
||||
id: base
|
||||
run: |
|
||||
set -e
|
||||
NGINX_CUR=$(grep -A3 'gateway:' values.yaml | grep 'tag:' | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
PY_CUR=$(grep -A3 'manager:' values.yaml | grep 'tag:' | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
|
||||
NGINX_LATEST=$(curl -s --retry 3 "https://registry.hub.docker.com/v2/repositories/nginxinc/nginx-unprivileged/tags?page_size=100" \
|
||||
| jq -r '.results[].name' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-alpine$' | sort -V | tail -1)
|
||||
PY_LATEST=$(curl -s --retry 3 "https://registry.hub.docker.com/v2/repositories/library/python/tags?page_size=100&name=alpine" \
|
||||
| jq -r '.results[].name' | grep -E '^3\.[0-9]+-alpine$' | sort -V | tail -1)
|
||||
|
||||
[ -n "$NGINX_LATEST" ] || NGINX_LATEST="$NGINX_CUR"
|
||||
[ -n "$PY_LATEST" ] || PY_LATEST="$PY_CUR"
|
||||
echo "nginx: $NGINX_CUR -> $NGINX_LATEST ; python: $PY_CUR -> $PY_LATEST"
|
||||
echo "nginx_cur=$NGINX_CUR" >> $GITHUB_OUTPUT
|
||||
echo "nginx_latest=$NGINX_LATEST" >> $GITHUB_OUTPUT
|
||||
echo "py_cur=$PY_CUR" >> $GITHUB_OUTPUT
|
||||
echo "py_latest=$PY_LATEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Decide
|
||||
id: decide
|
||||
run: |
|
||||
set -e
|
||||
CC=false; BASE=false
|
||||
[ "${{ steps.cc.outputs.current }}" != "${{ steps.cc.outputs.latest }}" ] && CC=true
|
||||
[ "${{ steps.base.outputs.nginx_cur }}" != "${{ steps.base.outputs.nginx_latest }}" ] && BASE=true
|
||||
[ "${{ steps.base.outputs.py_cur }}" != "${{ steps.base.outputs.py_latest }}" ] && BASE=true
|
||||
echo "cc=$CC" >> $GITHUB_OUTPUT
|
||||
echo "base=$BASE" >> $GITHUB_OUTPUT
|
||||
echo "update_needed=$([ "$CC" = true ] || [ "$BASE" = true ] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update values.yaml (image tags)
|
||||
if: steps.decide.outputs.update_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
sed -i "s|tag: \"${{ steps.base.outputs.nginx_cur }}\"|tag: \"${{ steps.base.outputs.nginx_latest }}\"|g" values.yaml
|
||||
sed -i "s|tag: \"${{ steps.base.outputs.py_cur }}\"|tag: \"${{ steps.base.outputs.py_latest }}\"|g" values.yaml
|
||||
if [ "${{ steps.decide.outputs.cc }}" = "true" ]; then
|
||||
sed -i "s|tag: \"${{ steps.cc.outputs.current }}\"|tag: \"${{ steps.cc.outputs.latest }}\"|g" values.yaml
|
||||
fi
|
||||
git diff --stat values.yaml || true
|
||||
|
||||
- name: Update Chart.yaml (version == Claude Code version)
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
NEW="${{ steps.cc.outputs.latest }}"
|
||||
sed -i "s/^version: .*/version: ${NEW}/" Chart.yaml
|
||||
sed -i "s/^appVersion: .*/appVersion: \"${NEW}\"/" Chart.yaml
|
||||
cat Chart.yaml
|
||||
|
||||
- name: Update README version history
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
NEW="${{ steps.cc.outputs.latest }}"
|
||||
sed -i "/^| バージョン | Claude Code | 変更内容 |/{
|
||||
n
|
||||
a\| ${NEW} | ${NEW} | 自動更新 |
|
||||
}" README.md
|
||||
|
||||
- name: Lint
|
||||
if: steps.decide.outputs.update_needed == 'true'
|
||||
run: helm lint .
|
||||
|
||||
- name: Commit
|
||||
if: steps.decide.outputs.update_needed == 'true'
|
||||
run: |
|
||||
set -e
|
||||
git config user.name "Claude"
|
||||
git config user.email "claude@cafepieters.com"
|
||||
if [ "${{ steps.decide.outputs.cc }}" = "true" ]; then
|
||||
git add values.yaml Chart.yaml README.md
|
||||
git commit -m "chore: update Claude Code to ${{ steps.cc.outputs.latest }} (nginx ${{ steps.base.outputs.nginx_latest }}, python ${{ steps.base.outputs.py_latest }})"
|
||||
else
|
||||
git add values.yaml
|
||||
git commit -m "chore: bump base images — nginx ${{ steps.base.outputs.nginx_latest }}, python ${{ steps.base.outputs.py_latest }} (no release)"
|
||||
fi
|
||||
git push origin "${GITHUB_REF_NAME:-main}"
|
||||
|
||||
- name: Package
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
helm package .
|
||||
echo "CHART_FILE=$(ls claude-code-*.tgz)" >> $GITHUB_ENV
|
||||
|
||||
- name: Git tag
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
V="v${{ steps.cc.outputs.latest }}"
|
||||
git rev-parse "$V" >/dev/null 2>&1 && { echo "tag $V exists"; exit 0; }
|
||||
git tag -a "$V" -m "Release $V"
|
||||
git push origin "$V"
|
||||
|
||||
- name: Gitea Release
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
NEW="${{ steps.cc.outputs.latest }}"; V="v${NEW}"
|
||||
if curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${V}" | jq -e '.id' >/dev/null 2>&1; then
|
||||
echo "release ${V} exists, skip"
|
||||
else
|
||||
RID=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${V}\",\"name\":\"${V}\",\"body\":\"claude-code Helm Chart ${V} — Claude Code CLI ${NEW}\"}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" | jq -r '.id')
|
||||
curl --fail-with-body -X POST -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/gzip" \
|
||||
--data-binary "@${CHART_FILE}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RID}/assets?name=${CHART_FILE}"
|
||||
fi
|
||||
|
||||
- name: Publish chart to Gitea Package Registry
|
||||
if: steps.decide.outputs.cc == 'true'
|
||||
run: |
|
||||
set -e
|
||||
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"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "Claude Code : ${{ steps.cc.outputs.current }} -> ${{ steps.cc.outputs.latest }} (updated: ${{ steps.decide.outputs.cc }})"
|
||||
echo "nginx : ${{ steps.base.outputs.nginx_cur }} -> ${{ steps.base.outputs.nginx_latest }}"
|
||||
echo "python : ${{ steps.base.outputs.py_cur }} -> ${{ steps.base.outputs.py_latest }}"
|
||||
echo "release : ${{ steps.decide.outputs.cc }}"
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Build Session Image
|
||||
|
||||
# セッション用の事前ビルドイメージ(git + openssh + ttyd + node + @anthropic-ai/claude-code)を
|
||||
# linux/amd64 + linux/arm64 でビルドし、Gitea Container Registry へ push する。
|
||||
#
|
||||
# トリガー:
|
||||
# - files/Dockerfile.session / files/session-entrypoint.sh / Chart.yaml の変更 push
|
||||
# (image-update-and-release.yaml が Chart.yaml を更新した後にも発火する)
|
||||
# - 手動実行(tag を任意指定可能)
|
||||
#
|
||||
# タグ: Chart.yaml の appVersion(= Claude Code CLI バージョン) と latest。
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- 'files/Dockerfile.session'
|
||||
- 'files/session-entrypoint.sh'
|
||||
- 'Chart.yaml'
|
||||
- '.gitea/workflows/session-image.yaml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'イメージタグ(未指定なら Chart.yaml の appVersion)'
|
||||
required: false
|
||||
|
||||
env:
|
||||
REGISTRY_HOST: git.cafepieters.com
|
||||
IMAGE: git.cafepieters.com/helmchart/claude-code/session
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Resolve tag
|
||||
id: t
|
||||
run: |
|
||||
set -e
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
[ -n "$TAG" ] || TAG=$(grep '^appVersion:' Chart.yaml | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "Session image tag: $TAG"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_HOST }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build & push (multi-arch)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: files/Dockerfile.session
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
CLAUDE_CODE_VERSION=${{ steps.t.outputs.tag }}
|
||||
tags: |
|
||||
${{ env.IMAGE }}:${{ steps.t.outputs.tag }}
|
||||
${{ env.IMAGE }}:latest
|
||||
push: true
|
||||
provenance: false
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "pushed ${IMAGE}:${{ steps.t.outputs.tag }} (+ latest) for linux/amd64,linux/arm64"
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Helm パッケージ成果物
|
||||
*.tgz
|
||||
charts/
|
||||
|
||||
# 秘密情報を含む values ファイル(絶対にコミットしない)
|
||||
secrets.values.yaml
|
||||
secrets.*.yaml
|
||||
*.secret.yaml
|
||||
*.local.yaml
|
||||
values.local.yaml
|
||||
|
||||
# SSH 鍵・トークンの一時ファイル
|
||||
*.pem
|
||||
id_*
|
||||
!examples/**
|
||||
*.dec
|
||||
*.dec.yaml
|
||||
|
||||
# OS / エディタ
|
||||
.DS_Store
|
||||
*.swp
|
||||
@@ -0,0 +1,41 @@
|
||||
# CHANGELOG 2.1.259 — 初版
|
||||
|
||||
claude-code Helm チャートの新規作成。
|
||||
|
||||
## 追加
|
||||
|
||||
- **制御プレーン(Deployment)**
|
||||
- `gateway`(nginx-unprivileged / Alpine): 単一入口。`auth.mode` に応じて
|
||||
token / basic / forward / none の認証、`/s/<id>/` のセッションへの WebSocket
|
||||
リバースプロキシ、`auth_request` によるアクセス時刻の更新。
|
||||
- `manager`(python / Alpine, 標準ライブラリのみ): セッション CRUD API、
|
||||
モバイル対応の Web UI(ビルド不要)、アイドル / 最大寿命の回収ループ。
|
||||
in-cluster ServiceAccount で Kubernetes API を直接操作。
|
||||
- **セッション(使い捨て Pod)**
|
||||
- `files/session-entrypoint.sh`: git 認証(SSH 鍵 / HTTPS トークン)、許可済み
|
||||
リポジトリの clone、`claude` 用意、`ttyd` 起動、アイドルウォッチドッグ。
|
||||
- `claude-verify` / `claude-push`(保護ブランチ・`--force` 禁止の安全 push)。
|
||||
- `files/Dockerfile.session`: git + ttyd + claude-code 同梱の事前ビルドイメージ
|
||||
(CI で multi-arch publish、コールドスタート短縮)。
|
||||
- **ライフサイクル**
|
||||
- `session.idleTimeoutSeconds` 無通信で Pod 自動破棄(Pod 内ウォッチドッグ + manager 回収)。
|
||||
- `session.maxLifetimeSeconds` を `activeDeadlineSeconds` として強制。
|
||||
- `session.maxSessions` で同時セッション数を制限。
|
||||
- 予備の孤児掃除 CronJob(`manager --reap-once`)。
|
||||
- **セキュリティ**
|
||||
- セッション NS を制御プレーンから分離(`sessionsNamespace`, PodSecurity `restricted`)。
|
||||
- manager RBAC はセッション NS の `pods` / `services` に限定した Role のみ。
|
||||
- セッション Pod は SA トークン非マウント(API サーバー到達不可)、非 root /
|
||||
`readOnlyRootFilesystem` / drop ALL caps / seccomp `RuntimeDefault`。
|
||||
- NetworkPolicy: セッションの ingress は gateway からのみ、egress は DNS + HTTPS(443)
|
||||
+ SSH(22)、`blockedEgressCIDRs`(既定でクラウドメタデータ IP)を except。
|
||||
- Git ホスト allow-list(`git.allowedHosts`)。既定は allow-list 外を拒否。
|
||||
- `ResourceQuota` / `LimitRange` でセッション NS のブラスト半径を制限。
|
||||
- **CI**
|
||||
- `helm-release.yaml`(main push で Package Registry へ publish、全リポジトリ共通)。
|
||||
- `image-update-and-release.yaml`(毎週月曜 3:00 JST: npm の Claude Code 最新版に追従、
|
||||
主アプリ更新時のみ セッションイメージ build/push + タグ + リリース + publish)。
|
||||
|
||||
## バージョン
|
||||
|
||||
`version` / `appVersion` = `2.1.259`(`@anthropic-ai/claude-code` の npm 最新版に一致)。
|
||||
@@ -0,0 +1,92 @@
|
||||
# CLAUDE.md — claude-code Helm Chart
|
||||
|
||||
## リポジトリ概要
|
||||
|
||||
Alpine Linux ベースの軽量コンテナで **Claude Code をリモート実行** するための Helm チャート。
|
||||
ブラウザ(PC / スマートフォン)からアクセスし、**セッションごとに独立した使い捨て Pod** で
|
||||
Git リポジトリ(GitHub / Gitea など互換 Git 全般)をチェックアウト・編集・検証・プッシュする。
|
||||
一定時間アイドルなセッション Pod は自動破棄される。Raspberry Pi などの ARM ベアメタル運用を想定。
|
||||
|
||||
- **Gitea リポジトリ**: `ssh://git@192.168.9.65/helmchart/claude-code`
|
||||
- **Helm リポジトリ**: `https://git.cafepieters.com/api/packages/helmchart/helm`
|
||||
- **セッションイメージ**: `git.cafepieters.com/helmchart/claude-code/session:<appVersion>`(CI で publish)
|
||||
|
||||
## アーキテクチャ(要点)
|
||||
|
||||
| コンポーネント | 実体 | 役割 |
|
||||
|---|---|---|
|
||||
| `gateway` | `nginxinc/nginx-unprivileged:alpine` | 単一入口。認証(token/basic/forward)、WebSocket リバースプロキシ、アクティビティ計測 |
|
||||
| `manager` | `python:3.x-alpine`(標準ライブラリのみ) | セッション CRUD API、Web UI、アイドル/寿命回収ループ。in-cluster SA で K8s API を直接操作 |
|
||||
| `session` | Alpine + node + git + ttyd + claude-code | セッションごとに作られる使い捨て Pod。`git clone` → `claude`(ttyd) → `claude-verify`/`claude-push` |
|
||||
| `reaper` CronJob | `manager` イメージ `--reap-once` | manager 障害時の保険となる孤児掃除 |
|
||||
|
||||
- 制御プレーンはリリース NS、セッション Pod は専用 NS(`sessionsNamespace.name`)に分離。
|
||||
- manager の RBAC はセッション NS の `pods` / `services` に限定した Role のみ(ClusterRole 不使用)。
|
||||
- セッション Pod は **API サーバー到達不可**(`automountServiceAccountToken: false`)、
|
||||
非 root / `readOnlyRootFilesystem` / drop ALL caps / seccomp RuntimeDefault、
|
||||
egress は DNS + git ホスト + `api.anthropic.com` のみ(NetworkPolicy)。
|
||||
- ファイル実体は `files/`(`.Files.Get` で ConfigMap 化): `manager.py` / `session-entrypoint.sh`
|
||||
/ `gateway.conf.template` / `gateway-entrypoint.sh` / `Dockerfile.session`。
|
||||
|
||||
## 実行環境について
|
||||
|
||||
**Helm はこのマシンにインストールされていない。** ローカルで `helm lint` / `template` は実行できない。
|
||||
テンプレートの検証はステージングのクラスタか CI(`helm-release.yaml` の lint ステップ)で行う。
|
||||
|
||||
## Git 運用ルール(このリポジトリで厳守)
|
||||
|
||||
1. **機能を追加・変更したら、必ず `README.md` を更新してから作業完了とする。**
|
||||
- values の新設キー、挙動変更、セキュリティ影響は README の該当セクションと
|
||||
values リファレンス表へ反映する。
|
||||
2. **変更は必ず Git Commit / Push する。**
|
||||
- 作業単位ごとにコミットし、`origin/main` へ push するまでが作業完了。
|
||||
3. **コミットメッセージに Claude クレジット(`Co-Authored-By` 等)を付けない。**
|
||||
- `user.name = Claude` でコミットされるため、ユーザー名で判別可能。
|
||||
4. `git pull` で取り込んだ変更は常に尊重する。revert して push することは禁止。
|
||||
不適切と思われる場合はユーザーに改善提案を提示し、承認を得てから対応する。
|
||||
|
||||
### Git Commit 情報
|
||||
|
||||
| 項目 | 値 |
|
||||
|------|-----|
|
||||
| user.name | `Claude` |
|
||||
| user.email | `claude@cafepieters.com` |
|
||||
| SSH キー | `P:/Claude/.ssh/id_claude` |
|
||||
| core.sshCommand | `ssh -i P:/Claude/.ssh/id_claude -F P:/Claude/.ssh/config -o StrictHostKeyChecking=no` |
|
||||
|
||||
## リリースフロー
|
||||
|
||||
- **バージョン番号**: `Chart.yaml` の `version` / `appVersion` は
|
||||
**主アプリ = Claude Code CLI(`@anthropic-ai/claude-code` の npm 最新版)と同一**にする。
|
||||
- **ワークフロー(3 本、他 Helm チャートプロジェクトと同じ思想)**
|
||||
- `helm-release.yaml` — `main` への push で発火。`helm lint` の後、`Chart.yaml` の version が
|
||||
未リリース(Gitea リリース・Package Registry 双方に存在しない)なら タグ + Gitea リリース +
|
||||
チャート publish。リリース済みなら lint のみで終了(再 publish しない)。
|
||||
- `image-update-and-release.yaml` — 毎週月曜 3:00 JST。npm の Claude Code 最新版と
|
||||
base image(nginx / python)の最新 Alpine タグを確認。Claude Code 更新時は
|
||||
`values.yaml` / `Chart.yaml` / README 履歴を更新しコミット → タグ + リリース + publish。
|
||||
base image のみ更新時は `values.yaml` を更新してコミットのみ(**リリースなし**)。
|
||||
- `session-image.yaml` — `files/Dockerfile.session` 等の変更、または上記による `Chart.yaml`
|
||||
更新で発火。セッションイメージを `linux/amd64,linux/arm64` でビルドして
|
||||
Gitea Container Registry(`helmchart/claude-code/session:<appVersion>` + `latest`)へ push。
|
||||
- **手動リリース(臨時)**: 改修作業などで必要な場合はバージョン末尾にアルファベットを付与する
|
||||
(例 `2.1.259` → `2.1.259-a` → `2.1.259-b`)。対応する `CHANGELOG-<version>.md` を追加し、
|
||||
README の「バージョン履歴」にも 1 行追加する。
|
||||
|
||||
## 必要な Gitea Secrets
|
||||
|
||||
| シークレット名 | 用途 |
|
||||
|---|---|
|
||||
| `GITEA_TOKEN` | Gitea API(リリース作成・タグ操作) |
|
||||
| `REGISTRY_USER` | Gitea Package / Container Registry ユーザー名 |
|
||||
| `REGISTRY_TOKEN` | Gitea Package / Container Registry トークン(`docker login` にも使用) |
|
||||
|
||||
## 改修履歴
|
||||
|
||||
### 初版(2026-09-03, v2.1.259)
|
||||
|
||||
チャート新規作成。gateway + manager + 使い捨て session Pod + reaper CronJob の構成で、
|
||||
ブラウザからの Claude Code リモート実行、GitHub/Gitea 両対応、アイドル自動破棄、
|
||||
NetworkPolicy / RBAC 最小化 / restricted PodSecurity を実装。
|
||||
|
||||
**対象ファイル**: 全ファイル(初期コミット)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
apiVersion: v2
|
||||
name: claude-code
|
||||
description: >-
|
||||
Alpine ベースの軽量コンテナで Claude Code をリモート実行する Helm チャート。
|
||||
ブラウザ(PC / スマートフォン)からアクセスし、セッションごとに独立した
|
||||
使い捨てコンテナで Git リポジトリ(GitHub / Gitea など)をチェックアウト・編集・
|
||||
検証・プッシュする。一定時間アイドルなセッションは自動破棄される。
|
||||
type: application
|
||||
version: 2.1.259
|
||||
appVersion: "2.1.259"
|
||||
keywords:
|
||||
- claude-code
|
||||
- claude
|
||||
- anthropic
|
||||
- remote-development
|
||||
- ephemeral-environments
|
||||
- git
|
||||
- gitea
|
||||
- github
|
||||
- alpine
|
||||
- arm
|
||||
home: https://git.cafepieters.com/helmchart/claude-code
|
||||
sources:
|
||||
- https://git.cafepieters.com/helmchart/claude-code
|
||||
maintainers:
|
||||
- name: Claude
|
||||
email: claude@cafepieters.com
|
||||
icon: https://www.anthropic.com/images/icons/apple-touch-icon.png
|
||||
annotations:
|
||||
# 主アプリ = Claude Code CLI (@anthropic-ai/claude-code) の npm 最新版に追従する。
|
||||
# version / appVersion はこの CLI バージョンと同一に保つ(CLAUDE.md のリリースルール)。
|
||||
cafepieters.com/upstream-npm: "@anthropic-ai/claude-code"
|
||||
@@ -1,3 +1,371 @@
|
||||
# claude-code
|
||||
|
||||
Alpine based container designed for running Claude Code remotely.
|
||||
**Alpine ベースの軽量コンテナで Claude Code をリモート実行する Helm チャート。**
|
||||
|
||||
ブラウザ(PC / スマートフォン)からアクセスし、**セッションごとに独立した使い捨て Pod** の中で
|
||||
Git リポジトリ(GitHub / Gitea など互換 Git 全般)をチェックアウト → 編集 → 検証 → プッシュします。
|
||||
一定時間アイドルなセッションは Pod ごと自動破棄され、リソースを占有し続けません。
|
||||
Raspberry Pi などの ARM ベアメタル Kubernetes を主な対象にしています。
|
||||
|
||||
> **設計目標** — Claude Code は本来ローカルで動かすものだが、これをクラスタにデプロイすることで、
|
||||
> Git 管理された複数プロジェクトを PC からもスマホからも横断的に、安全に触れるようにする。
|
||||
|
||||
- Gitea リポジトリ: `ssh://git@192.168.9.65/helmchart/claude-code`
|
||||
- Helm リポジトリ: `https://git.cafepieters.com/api/packages/helmchart/helm`
|
||||
- セッションイメージ: `git.cafepieters.com/helmchart/claude-code/session:<appVersion>`
|
||||
|
||||
---
|
||||
|
||||
## 目次
|
||||
|
||||
- [アーキテクチャ](#アーキテクチャ)
|
||||
- [セッションのライフサイクル](#セッションのライフサイクル)
|
||||
- [セキュリティモデル](#セキュリティモデル)
|
||||
- [インストール](#インストール)
|
||||
- [使い方](#使い方)
|
||||
- [values リファレンス](#values-リファレンス)
|
||||
- [image](#image)
|
||||
- [gateway / manager](#gateway--manager)
|
||||
- [sessionsNamespace](#sessionsnamespace)
|
||||
- [auth — アクセス認証](#auth--アクセス認証)
|
||||
- [anthropic — Claude Code 認証情報](#anthropic--claude-code-認証情報)
|
||||
- [git — 接続先と認証アイデンティティ](#git--接続先と認証アイデンティティ)
|
||||
- [session — 使い捨てセッション](#session--使い捨てセッション)
|
||||
- [networkPolicy](#networkpolicy)
|
||||
- [ingress / service](#ingress--service)
|
||||
- [セッションイメージ](#セッションイメージ)
|
||||
- [運用](#運用)
|
||||
- [トラブルシューティング](#トラブルシューティング)
|
||||
- [リリースフロー](#リリースフロー)
|
||||
- [バージョン履歴](#バージョン履歴)
|
||||
|
||||
---
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
```
|
||||
┌──────────── リリース NS ────────────┐
|
||||
ブラウザ (PC/スマホ) │ Deployment: claude-code │
|
||||
│ HTTPS/WSS │ ┌──────────┐ ┌───────────────┐ │
|
||||
▼ │ │ gateway │──▶│ manager │ │
|
||||
Ingress / LB ──────────▶│ │ (nginx) │ │ (python stdlib)│ │
|
||||
│ │ 認証 │ │ - Web UI │ │
|
||||
│ │ WS proxy │ │ - Session API │ │
|
||||
│ └────┬─────┘ │ - 回収ループ │ │
|
||||
│ │ └───────┬────────┘ │
|
||||
│ │ ttyd(WS) │ K8s API │
|
||||
└───────┼─────────────────┼───────────┘
|
||||
▼ ▼ (Role: pods/services)
|
||||
┌───────── セッション NS (restricted PSA) ──────────┐
|
||||
│ Pod claude-sess-ab12cd34ef Pod claude-sess-… │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ alpine + node + git │ git clone │
|
||||
│ │ + ttyd + claude-code │ → claude (ttyd) │
|
||||
│ │ 非root / roFS / netpol│ → claude-verify/push │
|
||||
│ └───────────────────────┘ アイドルで自壊 │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| コンポーネント | イメージ | 役割 |
|
||||
|---|---|---|
|
||||
| **gateway** | `nginxinc/nginx-unprivileged:*-alpine` | 単一入口。`auth.mode` に応じた認証、`/s/<id>/` をセッション Pod の `ttyd` へ WebSocket リバースプロキシ、リクエスト毎にアクティビティ時刻を更新 |
|
||||
| **manager** | `python:3.x-alpine`(標準ライブラリのみ・追加 pip 不要) | モバイル対応 Web UI、セッション作成/一覧/破棄 API、アイドル/寿命の回収ループ。in-cluster ServiceAccount で Kubernetes API を直接操作 |
|
||||
| **session** | `git.cafepieters.com/helmchart/claude-code/session`(既定)または任意の node:alpine 系 | セッションごとに作られる **使い捨て Pod**。`git clone` → `claude` を `ttyd` でブラウザに提供 |
|
||||
| **reaper** | manager と同じイメージ(`--reap-once`) | manager 障害時の保険となる孤児掃除 CronJob |
|
||||
|
||||
manager / gateway / session-entrypoint の実体は `files/` にあり、`.Files.Get` で ConfigMap 化されます。
|
||||
|
||||
## セッションのライフサイクル
|
||||
|
||||
1. **作成** — UI か `POST /api/sessions` に `repo` / `ref` / `identity` を渡す。manager は
|
||||
- リポジトリ URL の形式と **Git ホスト allow-list**(`git.allowedHosts`)を検証
|
||||
- 同時セッション数(`session.maxSessions`)を確認
|
||||
- セッション NS に Pod(`claude-sess-<10hex>`)と Service を作成
|
||||
2. **ブート**(`files/session-entrypoint.sh`, 非 root)
|
||||
- マウントされた SSH 鍵 / HTTPS トークンで git 認証を構成(`known_hosts` 必須、TOFU 無効)
|
||||
- リポジトリを `session.workspaceDir` に `clone`(`--filter=blob:none`)
|
||||
- `claude` が無ければ `$HOME` に非 root で npm インストール
|
||||
- `ttyd` を起動(`session.autoStartClaude: true` なら `claude` を自動起動)
|
||||
3. **利用** — `/s/<id>/` 経由でブラウザ内ターミナルに接続。`claude-verify` / `claude-push` が使える
|
||||
4. **破棄** — 次のいずれかで Pod ごと消滅(Service も回収)
|
||||
- **アイドル**: `ttyd` への接続が `session.idleTimeoutSeconds` 秒間ゼロ → Pod 内ウォッチドッグが自壊
|
||||
- **アイドル(保険)**: manager の回収ループが `last-activity` アノテーションを見て削除
|
||||
- **最大寿命**: `session.maxLifetimeSeconds`(Pod の `activeDeadlineSeconds`)到達で強制終了
|
||||
- **手動**: UI の「破棄」/ `DELETE /api/sessions/<id>`
|
||||
- **孤児掃除**: 予備 CronJob(`session.reaper.cronjob`)
|
||||
|
||||
## セキュリティモデル
|
||||
|
||||
| 層 | 対策 |
|
||||
|---|---|
|
||||
| **入口** | すべて gateway 経由。`auth.mode`= `token`(既定)/ `basic` / `forward`(外部 SSO へ委譲)/ `none`。WebSocket は Cookie トークンで認証(ブラウザは WS に Authorization ヘッダを付けられないため) |
|
||||
| **制御プレーン RBAC** | manager はセッション NS の `pods` / `services` のみ操作する **Role**(ClusterRole 不使用)。他 NS・他リソースには触れない |
|
||||
| **セッション Pod** | `automountServiceAccountToken: false`(**API サーバー到達不可**)、`runAsNonRoot` / `readOnlyRootFilesystem` / `capabilities.drop: [ALL]` / `seccompProfile: RuntimeDefault`。書き込み可能なのは emptyDir(`/workspace`, `$HOME`, `/tmp`)のみ |
|
||||
| **ネットワーク** | NetworkPolicy でセッションの ingress は gateway からのみ。egress は DNS + HTTPS(443, git/Anthropic) + SSH(22)。`networkPolicy.blockedEgressCIDRs`(既定でクラウドメタデータ IP `169.254.169.254/32`)を egress の `except` に指定。自クラスタの Pod/Service CIDR を追加すると内部 SSRF も遮断できる |
|
||||
| **Git 到達先** | `git.allowedHosts` の allow-list。既定は allow-list 外を拒否(`allowArbitraryRepos: true` で解除) |
|
||||
| **認証情報** | `ANTHROPIC_API_KEY` / SSH 鍵 / トークンは Kubernetes Secret。`values.yaml` に平文で書かず `--set` / `--set-file` / `existingSecret` で注入。git HTTPS トークンはセッション内で `credential.helper store` の 0600 ファイルに限定し、プロセス環境に残さない |
|
||||
| **PodSecurity** | セッション NS に `pod-security.kubernetes.io/enforce: restricted` を付与(`sessionsNamespace.create: true` 時) |
|
||||
| **ブラスト半径** | `ResourceQuota`(Pod / Service 数)+ `LimitRange`(コンテナ CPU/メモリ上限)+ `session.maxSessions` |
|
||||
| **自動 push** | 既定 **無効**。有効時もデフォルト/保護ブランチ(`autoPush.protectedBranches`)への push と `--force` は不可。`branchPrefix` 配下の作業ブランチにのみ push |
|
||||
|
||||
> **注意** — `values.yaml` に API キーや秘密鍵を書いてコミットしないでください。`helm get values` /
|
||||
> CI ログ / Git 履歴に残ります。`existingSecret` か CI からの `--set` を使ってください。
|
||||
|
||||
## インストール
|
||||
|
||||
### 前提
|
||||
|
||||
- Kubernetes 1.25+(PodSecurity admission)、NetworkPolicy 対応 CNI(Calico / Cilium など)
|
||||
- Ingress コントローラー(外部公開する場合。WebSocket を通すため read/send timeout を延長)
|
||||
- `ANTHROPIC_API_KEY`(または `claude setup-token` で発行した OAuth トークン)
|
||||
|
||||
### Helm リポジトリから
|
||||
|
||||
```bash
|
||||
helm repo add cafepieters https://git.cafepieters.com/api/packages/helmchart/helm
|
||||
helm repo update
|
||||
helm upgrade --install claude-code cafepieters/claude-code \
|
||||
-n claude-code --create-namespace \
|
||||
-f my.values.yaml \
|
||||
--set anthropic.apiKey="$ANTHROPIC_API_KEY"
|
||||
```
|
||||
|
||||
### ローカルチャートから(GitHub 公開リポジトリだけ触る最小構成)
|
||||
|
||||
```bash
|
||||
helm upgrade --install claude-code ./claude-code \
|
||||
-n claude-code --create-namespace \
|
||||
-f examples/minimal.values.yaml \
|
||||
--set anthropic.apiKey="$ANTHROPIC_API_KEY"
|
||||
```
|
||||
|
||||
### Gitea(SSH) + GitHub(HTTPS) 両対応
|
||||
|
||||
```bash
|
||||
helm upgrade --install claude-code ./claude-code \
|
||||
-n claude-code --create-namespace \
|
||||
-f examples/gitea-and-github.values.yaml \
|
||||
--set anthropic.apiKey="$ANTHROPIC_API_KEY" \
|
||||
--set-file git.identities.gitea-ssh.sshPrivateKey=P:/Claude/.ssh/id_claude \
|
||||
--set-file git.identities.gitea-ssh.knownHosts=./known_hosts.gitea \
|
||||
--set git.identities.github-https.token="$GITHUB_PAT"
|
||||
```
|
||||
|
||||
`known_hosts` は事前に取得します:
|
||||
|
||||
```bash
|
||||
ssh-keyscan 192.168.9.65 > known_hosts.gitea
|
||||
```
|
||||
|
||||
## 使い方
|
||||
|
||||
1. アクセス(Ingress 無効ならポートフォワード):
|
||||
|
||||
```bash
|
||||
kubectl -n claude-code port-forward svc/claude-code 8080:80
|
||||
```
|
||||
|
||||
2. `auth.mode: token` の場合、トークンを取得して UI に貼り付け(または `?token=` で開く):
|
||||
|
||||
```bash
|
||||
kubectl -n claude-code get secret claude-code-auth \
|
||||
-o jsonpath='{.data.token}' | base64 -d ; echo
|
||||
```
|
||||
|
||||
3. UI で **リポジトリ URL** / **ブランチ** / **認証 identity** を指定して「セッション作成」。
|
||||
4. 一覧の「開く」でブラウザ内ターミナル(Claude Code)に接続。
|
||||
5. セッション内の補助コマンド:
|
||||
- `claude-verify` … `session.verifyCommand`(例 `npm test`)を実行
|
||||
- `claude-push` … 検証 → `claude/<branch>` へ push(保護ブランチ / `--force` 不可)
|
||||
6. 無操作 `session.idleTimeoutSeconds` 秒でセッション(Pod)は自動的に消えます。
|
||||
|
||||
## values リファレンス
|
||||
|
||||
### image
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `image.gateway.*` | `nginxinc/nginx-unprivileged:1.29.1-alpine` | 受付 nginx(非 root 版) |
|
||||
| `image.manager.*` | `library/python:3.13-alpine` | 制御プレーン(標準ライブラリのみ) |
|
||||
| `image.session.*` | `library/node:22-alpine` | セッション。既定はブート時に `apk`/`npm` を実行するため初回起動が遅い。[セッションイメージ](#セッションイメージ)参照 |
|
||||
| `imagePullSecrets` | `[]` | プライベートレジストリ用 |
|
||||
|
||||
### gateway / manager
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `gateway.replicaCount` | `1` | 制御プレーン Deployment のレプリカ数 |
|
||||
| `gateway.port` | `8080` | gateway コンテナ待受ポート(Service / Ingress バックエンド) |
|
||||
| `manager.logLevel` | `info` | `debug` / `info` / `warning` / `error` |
|
||||
| `gateway.resources` / `manager.resources` | 控えめ | それぞれの resources |
|
||||
|
||||
### sessionsNamespace
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `sessionsNamespace.name` | `claude-code-sessions` | セッション Pod を隔離する NS(空 = リリース NS) |
|
||||
| `sessionsNamespace.create` | `true` | この NS を作成し PodSecurity ラベルを付与するか。既存 NS を使うなら `false` にして手動でラベル付与 |
|
||||
| `sessionsNamespace.podSecurityLevel` | `restricted` | 付与する PodSecurity `enforce` レベル |
|
||||
| `sessionsNamespace.resourceQuota.enabled` | `true` | セッション NS の `ResourceQuota`(既定は Pod/Service 数の抑制のみ。compute 集計上限は `hard` で明示) |
|
||||
| `sessionsNamespace.resourceQuota.hard` | `{}` | 指定するとそのまま `spec.hard` |
|
||||
| `sessionsNamespace.limitRange.*` | max 4CPU/4Gi | セッション NS のコンテナ既定値・上限(`LimitRange`) |
|
||||
|
||||
### auth — アクセス認証
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `auth.mode` | `token` | `token` / `basic` / `forward` / `none` |
|
||||
| `auth.token.value` | `""` | 空なら install 時に自動生成(`lookup` で再 install 間も安定)。運用では `existingSecret` 推奨 |
|
||||
| `auth.token.existingSecret` / `existingSecretKey` | `""` / `token` | 既存 Secret からトークンを読む |
|
||||
| `auth.basic.users` | `[]` | `htpasswd -nbB user pass` の出力を 1 行以上 |
|
||||
| `auth.basic.existingSecret` / `existingSecretKey` | `""` / `htpasswd` | 既存 htpasswd Secret |
|
||||
| `auth.forward.url` | `""` | `auth_request` の転送先(例: oauth2-proxy の `/oauth2/auth`) |
|
||||
| `auth.forward.signInUrl` | `""` | 未認証時のサインイン URL(任意) |
|
||||
|
||||
`token` モードでは UI のログインフォームで一度トークンを入力すると、`HttpOnly` Cookie に保存され
|
||||
以降の `/s/<id>/`(WebSocket 含む)に自動的に付与されます。
|
||||
|
||||
### anthropic — Claude Code 認証情報
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `anthropic.authMode` | `apiKey` | `apiKey`(`ANTHROPIC_API_KEY`)/ `oauth`(`CLAUDE_CODE_OAUTH_TOKEN`) |
|
||||
| `anthropic.apiKey` / `anthropic.oauthToken` | `""` | `--set` / `--set-file` で注入 |
|
||||
| `anthropic.existingSecret` | `""` | 既存 Secret を使う(キー名は `existingSecretKeyApiKey` / `existingSecretKeyOauthToken`) |
|
||||
| `anthropic.extraEnv` | `{}` | 全セッションへ渡す追加 env(例 `ANTHROPIC_BASE_URL`、Bedrock/Vertex 切替など) |
|
||||
|
||||
Secret は **セッション NS** に作成され、セッション Pod へ `envFrom` で注入されます。
|
||||
|
||||
### git — 接続先と認証アイデンティティ
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `git.allowedHosts` | GitHub/GitLab/Gitea | clone を許可するホスト allow-list |
|
||||
| `git.allowArbitraryRepos` | `false` | allow-list を無効化し任意 URL を許可(非推奨) |
|
||||
| `git.defaultAuthorName` / `Email` | `Claude` / `claude@cafepieters.com` | コミット author 既定 |
|
||||
| `git.identities.<name>.type` | — | `ssh` / `https-token` |
|
||||
| `…ssh.sshPrivateKey` / `knownHosts` | — | PEM 秘密鍵 / `ssh-keyscan` 出力(**`knownHosts` 必須**) |
|
||||
| `…https-token.username` / `token` | `x-access-token` / — | HTTPS ベーシック(GitHub PAT は username 任意) |
|
||||
| `…existingSecret` ほか | — | 既存 Secret 参照(キー名は `existingSecretKey*` で調整) |
|
||||
|
||||
GitHub でも Gitea でも GitLab でも、SSH か HTTPS トークンで認証できる Git ホストなら同じ仕組みで扱えます。
|
||||
セッション作成時に identity 名を選ぶと、その Secret だけがそのセッション Pod にマウントされます。
|
||||
|
||||
### session — 使い捨てセッション
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `session.autoStartClaude` | `true` | ブート後に `claude` を自動起動(`false` はログインシェル) |
|
||||
| `session.workspaceDir` | `/workspace` | clone 先 |
|
||||
| `session.ttydPort` | `7681` | `ttyd` 待受(Pod 内のみ) |
|
||||
| `session.extraApkPackages` | `[]` | 追加 apk(事前ビルドイメージなど root 権限がある場合のみ有効) |
|
||||
| `session.claudeCodeVersion` | `""` | インストールする `@anthropic-ai/claude-code` バージョン(空 = チャート `appVersion`) |
|
||||
| `session.resources` | 100m/256Mi 〜 2CPU/2Gi/4Gi(eph) | セッション Pod の resources |
|
||||
| `session.idleTimeoutSeconds` | `1800` | 無通信での破棄しきい値 |
|
||||
| `session.maxLifetimeSeconds` | `43200` | Pod の `activeDeadlineSeconds` |
|
||||
| `session.reaper.intervalSeconds` | `60` | manager 回収ループ間隔 |
|
||||
| `session.reaper.cronjob.enabled` / `schedule` | `true` / `*/15 * * * *` | 予備の孤児掃除 CronJob |
|
||||
| `session.maxSessions` | `10` | 同時セッション数上限(manager が enforce) |
|
||||
| `session.verifyCommand` | `""` | `claude-verify` / `claude-push` が実行するコマンド |
|
||||
| `session.autoPush.enabled` | `false` | `claude-push` を許可するか |
|
||||
| `session.autoPush.branchPrefix` | `claude/` | push 先ブランチのプレフィックス |
|
||||
| `session.autoPush.protectedBranches` | `[main, master, develop, release]` | push 禁止ブランチ |
|
||||
| `session.podLabels` / `podAnnotations` / `nodeSelector` / `tolerations` / `priorityClassName` | — | セッション Pod へのメタデータ / スケジューリング |
|
||||
|
||||
### networkPolicy
|
||||
|
||||
| キー | 既定 | 説明 |
|
||||
|---|---|---|
|
||||
| `networkPolicy.enabled` | `true` | NetworkPolicy を作成するか(CNI が対応必須) |
|
||||
| `networkPolicy.dnsNamespaceSelector` | `kube-system` | DNS を許可する NS セレクタ |
|
||||
| `networkPolicy.allowInternetHttps` | `true` | セッションの 443/tcp 外向きを許可(git HTTPS + Anthropic API) |
|
||||
| `networkPolicy.allowSshGit` | `true` | セッションの 22/tcp 外向きを許可(SSH git) |
|
||||
| `networkPolicy.blockedEgressCIDRs` | `[169.254.169.254/32]` | egress `except`。自クラスタの Pod/Service CIDR を足すと内部 SSRF を遮断 |
|
||||
| `networkPolicy.sessionEgressExtra` | `[]` | 追加許可(社内ミラー / プロキシ)。`{cidr, ports[]}` |
|
||||
|
||||
完全にインターネットを遮断したい場合は `allowInternetHttps: false` にして egress プロキシ
|
||||
(例: 許可リスト付き Squid)を `sessionEgressExtra` で通してください。
|
||||
|
||||
### ingress / service
|
||||
|
||||
`ingress.enabled: true` で Ingress を作成。WebSocket 用に `proxy-read-timeout` /
|
||||
`proxy-send-timeout` を既定 `3600` に設定します(`ingress.annotations` で上書き可)。
|
||||
`service.type` は `ClusterIP` 既定。
|
||||
|
||||
## セッションイメージ
|
||||
|
||||
既定の `image.session` は CI(`session-image.yaml`)がビルドする **事前ビルドイメージ**
|
||||
`git.cafepieters.com/helmchart/claude-code/session:<appVersion>` です。
|
||||
`git` / `openssh-client` / `ttyd` / `bash` / `node` / `@anthropic-ai/claude-code` を同梱し、
|
||||
非 root(uid 10001)で動作するため、`restricted` PodSecurity / `readOnlyRootFilesystem` 下でも
|
||||
確実に起動し、コールドスタートも数秒です。
|
||||
|
||||
`session-image.yaml` は `files/Dockerfile.session` 等の変更 push、または
|
||||
`image-update-and-release.yaml` による `Chart.yaml` 更新で発火し、`linux/amd64,linux/arm64` の
|
||||
2 アーキテクチャでビルドして Gitea Container Registry へ push します。手動ビルドは:
|
||||
|
||||
```bash
|
||||
docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
--build-arg CLAUDE_CODE_VERSION=2.1.259 \
|
||||
-f files/Dockerfile.session \
|
||||
-t git.cafepieters.com/helmchart/claude-code/session:2.1.259 --push .
|
||||
```
|
||||
|
||||
### 事前ビルドを使わない場合
|
||||
|
||||
`image.session` に素の `node:22-alpine` などを指定することもできます。この場合ブート
|
||||
スクリプトが `$HOME` に非 root で `@anthropic-ai/claude-code` を npm インストールします
|
||||
(初回数十秒)。ただし **`ttyd` は非 root では `apk add` できない** ため、`ttyd` を含まない
|
||||
イメージでは起動に失敗します。`ttyd` 入りの独自イメージを用意するか、事前ビルドイメージを
|
||||
使ってください。
|
||||
|
||||
## 運用
|
||||
|
||||
- **セッション一覧**: `kubectl -n <sessionsNamespace> get pods -l app.kubernetes.io/component=session`
|
||||
- **セッションのログ**: `kubectl -n <sessionsNamespace> logs claude-sess-<id>`
|
||||
- **強制破棄**: UI から、または `kubectl -n <sessionsNamespace> delete pod,svc claude-sess-<id>`
|
||||
- **回収の確認**: manager のログに `reaping session <id> (idle|max-lifetime|phase=...)`
|
||||
- **トークンのローテーション**: `auth.token.existingSecret` の値を更新し、Deployment を再起動
|
||||
- **アップグレード**: `helm upgrade`。`files/*` の変更は checksum アノテーションで自動ローリング
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
| 症状 | 原因と対処 |
|
||||
|---|---|
|
||||
| UI が 502 / セッションを開くと 502 | セッション Pod がまだ Ready でない(初回は `npm i` に時間)。数十秒待つ。恒久対策は事前ビルドイメージ |
|
||||
| セッション作成が `許可されていない Git ホスト` | `git.allowedHosts` に対象ホストを追加(IP と FQDN は別物) |
|
||||
| clone が `Host key verification failed` | identity の `knownHosts` が対象ホストの鍵と不一致。`ssh-keyscan <host>` で取り直す |
|
||||
| clone が `terminated ... could not read Username` | HTTPS の場合 `git.identities.<name>` 未指定 or token 無効。SSH の場合は identity の type/秘密鍵を確認 |
|
||||
| WebSocket がすぐ切れる | Ingress の `proxy-read-timeout` を延長。`auth.mode: token` で Cookie 未設定なら一度 UI からログイン |
|
||||
| セッションが破棄されない | manager の RBAC / ログを確認。予備 CronJob(`session.reaper.cronjob`)が有効か |
|
||||
| `apk` エラーがログに出る | 既定イメージ + `readOnlyRootFilesystem` の制約。事前ビルドイメージを使う |
|
||||
| manager が全リクエストを 401 | `auth.mode: token` でトークンが空。`auth.token.value` か `existingSecret` を設定 |
|
||||
|
||||
## リリースフロー
|
||||
|
||||
`Chart.yaml` の `version` / `appVersion` は **Claude Code CLI(`@anthropic-ai/claude-code` npm 最新版)と同一**。
|
||||
|
||||
- **`helm-release.yaml`** — `main` push 時に Gitea Package Registry へチャートを publish(全リポジトリ共通)。
|
||||
- **`image-update-and-release.yaml`** — 毎週月曜 3:00 JST。
|
||||
- npm の Claude Code 最新版に追従。**主アプリ更新時のみ** `Chart.yaml` 更新 → セッションイメージ
|
||||
multi-arch ビルド/push → Git タグ `v<ver>` → Gitea リリース → チャート publish。
|
||||
- `nginx` / `python` の Alpine タグのみ更新の場合は `values.yaml` を更新してコミットするだけ
|
||||
(**リリースなし**)。
|
||||
- **手動リリース(臨時)** — バージョン末尾にアルファベットを付与(`2.1.259` → `2.1.259-a`)。
|
||||
対応する `CHANGELOG-<version>.md` を追加する。
|
||||
|
||||
必要な Gitea Secrets: `GITEA_TOKEN` / `REGISTRY_USER` / `REGISTRY_TOKEN`。
|
||||
|
||||
### ワークフロー一覧
|
||||
|
||||
| ファイル | トリガー | 動作 |
|
||||
|---|---|---|
|
||||
| `helm-release.yaml` | `main` への push / 手動 | `helm lint`。`Chart.yaml` の version が未リリース(Gitea リリース・Package Registry 双方に無い)なら タグ + Gitea リリース + チャート publish。リリース済みなら lint のみ |
|
||||
| `image-update-and-release.yaml` | 毎週月曜 3:00 JST / 手動 | npm の `@anthropic-ai/claude-code` 最新版と base image(nginx / python)の最新 Alpine タグを確認。Claude Code の更新があれば `values.yaml` / `Chart.yaml` / README 履歴を更新してコミット → タグ + リリース + publish。base image のみの更新は `values.yaml` を更新してコミットのみ(リリースなし) |
|
||||
| `session-image.yaml` | `files/Dockerfile.session` ほかの変更 / 手動 | セッションイメージ(`.../claude-code/session`)を `linux/amd64,linux/arm64` でビルドし、`Chart.yaml` の `appVersion` タグ + `latest` で Gitea Container Registry へ push |
|
||||
|
||||
## バージョン履歴
|
||||
|
||||
| バージョン | Claude Code | 変更内容 |
|
||||
|---|---|---|
|
||||
| 2.1.259 | 2.1.259 | 初版。gateway + manager + 使い捨て session Pod + reaper、GitHub/Gitea 対応、アイドル自動破棄、NetworkPolicy / RBAC 最小化 / restricted PodSecurity |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Gitea(SSH) + GitHub(HTTPS トークン) の両対応構成 + Ingress + 事前ビルド image
|
||||
#
|
||||
# 実行例:
|
||||
# helm upgrade --install claude-code ./claude-code \
|
||||
# -n claude-code --create-namespace \
|
||||
# -f examples/gitea-and-github.values.yaml \
|
||||
# --set anthropic.apiKey="$ANTHROPIC_API_KEY" \
|
||||
# --set-file git.identities.gitea-ssh.sshPrivateKey=/path/to/id_claude \
|
||||
# --set-file git.identities.gitea-ssh.knownHosts=/path/to/known_hosts \
|
||||
# --set git.identities.github-https.token="$GITHUB_PAT"
|
||||
|
||||
image:
|
||||
session:
|
||||
# CI で publish した事前ビルドイメージ(git + ttyd + claude-code 同梱)
|
||||
registry: git.cafepieters.com
|
||||
repository: helmchart/claude-code/session
|
||||
tag: "2.1.259"
|
||||
|
||||
auth:
|
||||
mode: token
|
||||
token:
|
||||
existingSecret: claude-code-access # 事前に kubectl create secret generic で作成
|
||||
|
||||
anthropic:
|
||||
authMode: apiKey
|
||||
|
||||
git:
|
||||
allowedHosts:
|
||||
- github.com
|
||||
- 192.168.9.65
|
||||
- git.cafepieters.com
|
||||
defaultAuthorName: "Claude"
|
||||
defaultAuthorEmail: "claude@cafepieters.com"
|
||||
identities:
|
||||
gitea-ssh:
|
||||
type: ssh
|
||||
# sshPrivateKey / knownHosts は --set-file で注入
|
||||
# knownHosts は `ssh-keyscan 192.168.9.65` の出力
|
||||
github-https:
|
||||
type: https-token
|
||||
username: "x-access-token"
|
||||
# token は --set で注入
|
||||
|
||||
session:
|
||||
autoStartClaude: true
|
||||
idleTimeoutSeconds: 1800
|
||||
maxLifetimeSeconds: 28800
|
||||
maxSessions: 8
|
||||
verifyCommand: "npm test --silent"
|
||||
autoPush:
|
||||
enabled: true
|
||||
branchPrefix: "claude/"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
hosts:
|
||||
- host: claude-code.cafepieters.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: claude-code-tls
|
||||
hosts:
|
||||
- claude-code.cafepieters.com
|
||||
@@ -0,0 +1,26 @@
|
||||
# 最小構成: 公開リポジトリのみ・token 認証・Ingress なし
|
||||
# 実行例:
|
||||
# helm upgrade --install claude-code ./claude-code \
|
||||
# -n claude-code --create-namespace \
|
||||
# -f examples/minimal.values.yaml \
|
||||
# --set anthropic.apiKey="$ANTHROPIC_API_KEY"
|
||||
#
|
||||
# アクセス:
|
||||
# kubectl -n claude-code port-forward svc/claude-code-claude-code 8080:80
|
||||
# トークン: kubectl -n claude-code get secret claude-code-claude-code-auth -o jsonpath='{.data.token}' | base64 -d
|
||||
|
||||
auth:
|
||||
mode: token # 空 value → install 時に自動生成
|
||||
|
||||
anthropic:
|
||||
authMode: apiKey
|
||||
# apiKey は --set か existingSecret で注入する
|
||||
|
||||
git:
|
||||
allowedHosts:
|
||||
- github.com
|
||||
- gitlab.com
|
||||
|
||||
session:
|
||||
idleTimeoutSeconds: 900
|
||||
maxSessions: 3
|
||||
@@ -0,0 +1,41 @@
|
||||
# 外部 SSO(oauth2-proxy)へ認証を委譲する構成。
|
||||
# ブラウザ → Ingress → (oauth2-proxy) → claude-code gateway。
|
||||
# gateway 自身も auth_request で二重チェックする。
|
||||
#
|
||||
# 前提: auth ネームスペースに oauth2-proxy がデプロイ済みで、
|
||||
# http://oauth2-proxy.auth.svc.cluster.local:4180/oauth2/auth が使えること。
|
||||
|
||||
auth:
|
||||
mode: forward
|
||||
forward:
|
||||
url: "http://oauth2-proxy.auth.svc.cluster.local:4180/oauth2/auth"
|
||||
signInUrl: "https://auth.example.com/oauth2/start?rd=$escaped_request_uri"
|
||||
|
||||
anthropic:
|
||||
authMode: oauth # `claude setup-token` で発行したトークンを使う
|
||||
# oauthToken は --set / existingSecret で注入
|
||||
|
||||
git:
|
||||
allowedHosts:
|
||||
- github.com
|
||||
- git.cafepieters.com
|
||||
|
||||
session:
|
||||
maxSessions: 15
|
||||
idleTimeoutSeconds: 3600
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: "https://auth.example.com/oauth2/auth"
|
||||
nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com/oauth2/start?rd=$escaped_request_uri"
|
||||
hosts:
|
||||
- host: claude-code.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: claude-code-tls
|
||||
hosts:
|
||||
- claude-code.example.com
|
||||
@@ -0,0 +1,37 @@
|
||||
# ==========================================================================
|
||||
# claude-code セッションイメージ(事前ビルド版・推奨)
|
||||
#
|
||||
# 既定の values.image.session(node:22-alpine + ブート時インストール)でも
|
||||
# 動作するが、初回起動が遅い。このイメージを CI でビルド・publish し
|
||||
# values.image.session に指定すると、セッションのコールドスタートが数秒に短縮される。
|
||||
#
|
||||
# ビルド:
|
||||
# docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
# --build-arg CLAUDE_CODE_VERSION=2.1.259 \
|
||||
# -f files/Dockerfile.session \
|
||||
# -t git.cafepieters.com/helmchart/claude-code/session:2.1.259 --push .
|
||||
# ==========================================================================
|
||||
ARG NODE_VERSION=22-alpine
|
||||
FROM node:${NODE_VERSION}
|
||||
|
||||
ARG CLAUDE_CODE_VERSION=""
|
||||
|
||||
# git(SSH 込み) / ttyd / bash / 基本ツール。community に ttyd がある。
|
||||
RUN apk add --no-cache \
|
||||
bash git openssh-client ca-certificates curl jq less tini \
|
||||
ttyd \
|
||||
&& update-ca-certificates
|
||||
|
||||
# Claude Code CLI をグローバル導入(ビルド時のみ root、実行時は read-only でOK)
|
||||
RUN npm install -g --no-fund --no-audit \
|
||||
"@anthropic-ai/claude-code${CLAUDE_CODE_VERSION:+@${CLAUDE_CODE_VERSION}}" \
|
||||
&& npm cache clean --force
|
||||
|
||||
# 非 root 実行ユーザー(Helm 側の runAsUser と一致させる: 既定 10001)
|
||||
ARG RUN_UID=10001
|
||||
RUN adduser -D -u ${RUN_UID} -h /home/claude claude
|
||||
USER ${RUN_UID}
|
||||
ENV HOME=/home/claude
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/bin/sh", "/bootstrap/entrypoint.sh"]
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
# claude-code gateway 起動スクリプト
|
||||
# - /etc/resolv.conf から nginx 用 resolver を導出
|
||||
# - auth.mode に応じて認証インクルードを生成
|
||||
# - envsubst で nginx.conf を展開して起動
|
||||
set -eu
|
||||
|
||||
TPL="/etc/gateway/gateway.conf.template"
|
||||
OUT_DIR="/tmp/nginx"
|
||||
mkdir -p "$OUT_DIR/body" "$OUT_DIR/proxy" "$OUT_DIR/fastcgi" "$OUT_DIR/uwsgi" "$OUT_DIR/scgi"
|
||||
|
||||
: "${GATEWAY_PORT:=8080}"
|
||||
: "${MANAGER_PORT:=8081}"
|
||||
: "${TTYD_PORT:=7681}"
|
||||
: "${SESSIONS_NS:=default}"
|
||||
: "${AUTH_MODE:=token}"
|
||||
: "${NGINX_ERROR_LEVEL:=warn}"
|
||||
|
||||
# ---- resolver(CoreDNS)を resolv.conf から取得 ----
|
||||
NGINX_RESOLVER="$(awk '/^nameserver/ { print $2; exit }' /etc/resolv.conf 2>/dev/null || true)"
|
||||
[ -n "$NGINX_RESOLVER" ] || NGINX_RESOLVER="kube-dns.kube-system.svc.cluster.local"
|
||||
export NGINX_RESOLVER
|
||||
|
||||
export GATEWAY_PORT MANAGER_PORT TTYD_PORT SESSIONS_NS NGINX_ERROR_LEVEL
|
||||
|
||||
# ---- 認証インクルードの生成 ----
|
||||
: > "$OUT_DIR/auth_basic.conf"
|
||||
: > "$OUT_DIR/auth_request.conf"
|
||||
: > "$OUT_DIR/auth_backend.conf"
|
||||
|
||||
case "$AUTH_MODE" in
|
||||
token)
|
||||
echo 'auth_request /_auth;' > "$OUT_DIR/auth_request.conf"
|
||||
echo "proxy_pass http://127.0.0.1:${MANAGER_PORT}/auth;" > "$OUT_DIR/auth_backend.conf"
|
||||
;;
|
||||
forward)
|
||||
if [ -z "${AUTH_FORWARD_URL:-}" ]; then
|
||||
echo "AUTH_MODE=forward だが AUTH_FORWARD_URL が空です" >&2; exit 1
|
||||
fi
|
||||
echo 'auth_request /_auth;' > "$OUT_DIR/auth_request.conf"
|
||||
{
|
||||
echo "proxy_pass ${AUTH_FORWARD_URL};"
|
||||
echo 'proxy_set_header X-Original-URL $scheme://$http_host$request_uri;'
|
||||
} > "$OUT_DIR/auth_backend.conf"
|
||||
;;
|
||||
basic)
|
||||
if [ ! -r /etc/gateway-secret/htpasswd ]; then
|
||||
echo "AUTH_MODE=basic だが htpasswd がマウントされていません" >&2; exit 1
|
||||
fi
|
||||
{
|
||||
echo 'auth_basic "claude-code";'
|
||||
echo 'auth_basic_user_file /etc/gateway-secret/htpasswd;'
|
||||
} > "$OUT_DIR/auth_basic.conf"
|
||||
;;
|
||||
none)
|
||||
echo "WARNING: AUTH_MODE=none — 認証なしで公開されます" >&2
|
||||
;;
|
||||
*)
|
||||
echo "未知の AUTH_MODE: $AUTH_MODE" >&2; exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---- nginx.conf 展開 ----
|
||||
envsubst '${GATEWAY_PORT} ${MANAGER_PORT} ${TTYD_PORT} ${SESSIONS_NS} ${NGINX_RESOLVER} ${NGINX_ERROR_LEVEL}' \
|
||||
< "$TPL" > "$OUT_DIR/nginx.conf"
|
||||
|
||||
echo "[gateway] auth_mode=$AUTH_MODE resolver=$NGINX_RESOLVER sessions_ns=$SESSIONS_NS"
|
||||
exec nginx -c "$OUT_DIR/nginx.conf" -g 'daemon off;'
|
||||
@@ -0,0 +1,100 @@
|
||||
# nginx.conf (envsubst テンプレート) — claude-code gateway
|
||||
# 受付・認証・WebSocket プロキシの単一入口。read-only rootfs / 非 root で動作する。
|
||||
worker_processes auto;
|
||||
pid /tmp/nginx/nginx.pid;
|
||||
error_log /dev/stderr ${NGINX_ERROR_LEVEL};
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /dev/stdout;
|
||||
|
||||
client_body_temp_path /tmp/nginx/body;
|
||||
proxy_temp_path /tmp/nginx/proxy;
|
||||
fastcgi_temp_path /tmp/nginx/fastcgi;
|
||||
uwsgi_temp_path /tmp/nginx/uwsgi;
|
||||
scgi_temp_path /tmp/nginx/scgi;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
server_tokens off;
|
||||
client_max_body_size 25m;
|
||||
|
||||
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
|
||||
|
||||
resolver ${NGINX_RESOLVER} valid=10s ipv6=off;
|
||||
|
||||
server {
|
||||
listen ${GATEWAY_PORT};
|
||||
listen [::]:${GATEWAY_PORT};
|
||||
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
|
||||
# ---- HTTP Basic(auth.mode=basic のときのみ内容を持つ)----
|
||||
include /tmp/nginx/auth_basic.conf;
|
||||
|
||||
location = /healthz {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:${MANAGER_PORT}/healthz;
|
||||
}
|
||||
|
||||
# ---- auth_request の実体(auth.mode=token|forward)----
|
||||
location = /_auth {
|
||||
internal;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
include /tmp/nginx/auth_backend.conf;
|
||||
}
|
||||
|
||||
# ---- 制御プレーン(UI / API)----
|
||||
location / {
|
||||
include /tmp/nginx/auth_request.conf;
|
||||
proxy_pass http://127.0.0.1:${MANAGER_PORT};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# UI とログインは常に到達可能にする(token モードのログイン導線のため)
|
||||
location = / { proxy_pass http://127.0.0.1:${MANAGER_PORT}; proxy_set_header Host $host; }
|
||||
location = /index.html { proxy_pass http://127.0.0.1:${MANAGER_PORT}; proxy_set_header Host $host; }
|
||||
location = /login { proxy_pass http://127.0.0.1:${MANAGER_PORT}; proxy_set_header Host $host; }
|
||||
location = /api/config {
|
||||
include /tmp/nginx/auth_request.conf;
|
||||
proxy_pass http://127.0.0.1:${MANAGER_PORT};
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# ---- セッションへのリバースプロキシ(WebSocket 対応)----
|
||||
location ~ ^/s/(?<sid>[a-f0-9]{10})(?<rest>/.*)?$ {
|
||||
include /tmp/nginx/auth_basic.conf;
|
||||
include /tmp/nginx/auth_request.conf;
|
||||
error_page 401 = @needlogin;
|
||||
|
||||
set $rest_path $rest;
|
||||
if ($rest_path = "") { set $rest_path /; }
|
||||
set $sess_backend "http://claude-sess-$sid.${SESSIONS_NS}.svc.cluster.local:${TTYD_PORT}";
|
||||
|
||||
proxy_pass $sess_backend$rest_path$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location @needlogin {
|
||||
return 302 /?next=$request_uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
claude-code manager — 制御プレーン
|
||||
|
||||
責務:
|
||||
* Web UI(モバイル対応・ビルド不要の素の HTML/JS)
|
||||
* セッションのライフサイクル API(作成 / 一覧 / 破棄 / touch)
|
||||
* gateway(nginx) からの auth_request 検証(token モード)
|
||||
* アイドル / 寿命超過セッションの回収ループ
|
||||
|
||||
依存: Python 標準ライブラリのみ(追加 pip 不要)。
|
||||
Kubernetes API へは in-cluster ServiceAccount トークンで直接アクセスする。
|
||||
セッション NS 内の Pod / Service のみを操作する(RBAC で制限済み)。
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 設定(すべて環境変数から)
|
||||
# --------------------------------------------------------------------------
|
||||
def env(key, default=""):
|
||||
return os.environ.get(key, default)
|
||||
|
||||
def env_json(key, default):
|
||||
raw = os.environ.get(key, "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except ValueError:
|
||||
logging.error("invalid JSON in env %s", key)
|
||||
return default
|
||||
|
||||
LISTEN_PORT = int(env("LISTEN_PORT", "8080"))
|
||||
SESSIONS_NS = env("SESSIONS_NAMESPACE", "default")
|
||||
FULLNAME = env("FULLNAME", "claude-code")
|
||||
SESSION_SA = env("SESSION_SERVICE_ACCOUNT", "claude-code-session")
|
||||
SESSION_IMAGE = env("SESSION_IMAGE", "docker.io/library/node:22-alpine")
|
||||
SESSION_IMAGE_PULL_POLICY = env("SESSION_IMAGE_PULL_POLICY", "IfNotPresent")
|
||||
SESSION_IMAGE_PULL_SECRETS = env_json("SESSION_IMAGE_PULL_SECRETS_JSON", [])
|
||||
BOOTSTRAP_CM = env("SESSION_BOOTSTRAP_CONFIGMAP", FULLNAME + "-session")
|
||||
ANTHROPIC_SECRET = env("ANTHROPIC_SECRET", FULLNAME + "-anthropic")
|
||||
GIT_IDENTITIES = env_json("GIT_IDENTITIES_JSON", {})
|
||||
GIT_ALLOWED_HOSTS = [h.strip().lower() for h in env("GIT_ALLOWED_HOSTS", "").split(",") if h.strip()]
|
||||
GIT_ALLOW_ARBITRARY = env("GIT_ALLOW_ARBITRARY", "false").lower() == "true"
|
||||
GIT_AUTHOR_NAME = env("GIT_DEFAULT_AUTHOR_NAME", "Claude")
|
||||
GIT_AUTHOR_EMAIL = env("GIT_DEFAULT_AUTHOR_EMAIL", "claude@cafepieters.com")
|
||||
IDLE_TIMEOUT = int(env("IDLE_TIMEOUT_SECONDS", "1800"))
|
||||
MAX_LIFETIME = int(env("MAX_LIFETIME_SECONDS", "43200"))
|
||||
REAP_INTERVAL = int(env("REAP_INTERVAL_SECONDS", "60"))
|
||||
MAX_SESSIONS = int(env("MAX_SESSIONS", "10"))
|
||||
TTYD_PORT = int(env("TTYD_PORT", "7681"))
|
||||
SESSION_RESOURCES = env_json("SESSION_RESOURCES_JSON", {})
|
||||
WORKSPACE_DIR = env("WORKSPACE_DIR", "/workspace")
|
||||
AUTOSTART_CLAUDE = env("AUTOSTART_CLAUDE", "true")
|
||||
VERIFY_COMMAND = env("VERIFY_COMMAND", "")
|
||||
AUTOPUSH = env_json("AUTOPUSH_JSON", {"enabled": False})
|
||||
EXTRA_APK = env("EXTRA_APK_PACKAGES", "")
|
||||
CLAUDE_CODE_VERSION = env("CLAUDE_CODE_VERSION", "")
|
||||
ANTHROPIC_EXTRA_ENV = env_json("ANTHROPIC_EXTRA_ENV_JSON", {})
|
||||
POD_LABELS = env_json("SESSION_POD_LABELS_JSON", {})
|
||||
POD_ANNOTATIONS = env_json("SESSION_POD_ANNOTATIONS_JSON", {})
|
||||
NODE_SELECTOR = env_json("SESSION_NODE_SELECTOR_JSON", {})
|
||||
TOLERATIONS = env_json("SESSION_TOLERATIONS_JSON", [])
|
||||
PRIORITY_CLASS = env("SESSION_PRIORITY_CLASS", "")
|
||||
SESSION_RUN_UID = int(env("SESSION_RUN_UID", "10001"))
|
||||
|
||||
AUTH_MODE = env("AUTH_MODE", "token")
|
||||
_auth_token_file = env("AUTH_TOKEN_FILE", "")
|
||||
if _auth_token_file and os.path.exists(_auth_token_file):
|
||||
with open(_auth_token_file, "r", encoding="utf-8") as fh:
|
||||
AUTH_TOKEN = fh.read().strip()
|
||||
else:
|
||||
AUTH_TOKEN = env("AUTH_TOKEN", "")
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, env("LOG_LEVEL", "info").upper(), logging.INFO),
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
)
|
||||
log = logging.getLogger("manager")
|
||||
|
||||
LABEL_APP = "app.kubernetes.io/name"
|
||||
LABEL_COMPONENT = "app.kubernetes.io/component"
|
||||
LABEL_SID = "claude-code.cafepieters.com/session-id"
|
||||
ANN_PREFIX = "claude-code.cafepieters.com/"
|
||||
SESSION_SELECTOR = LABEL_COMPONENT + "=session," + LABEL_APP + "=claude-code"
|
||||
|
||||
SID_RE = re.compile(r"^[a-f0-9]{10}$")
|
||||
# 受け入れる repo URL: https://host/... または ssh://git@host/... または git@host:path
|
||||
REPO_RE = re.compile(
|
||||
r"^(?:https://(?P<h1>[A-Za-z0-9.\-]+)(?::\d+)?/[\w./\-~%]+"
|
||||
r"|ssh://git@(?P<h2>[A-Za-z0-9.\-]+)(?::\d+)?/[\w./\-~%]+"
|
||||
r"|git@(?P<h3>[A-Za-z0-9.\-]+):[\w./\-~%]+)$"
|
||||
)
|
||||
REF_RE = re.compile(r"^[\w./\-]{1,200}$")
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Kubernetes API クライアント(標準ライブラリのみ)
|
||||
# --------------------------------------------------------------------------
|
||||
K8S_HOST = "https://kubernetes.default.svc"
|
||||
_sa_dir = "/var/run/secrets/kubernetes.io/serviceaccount"
|
||||
with open(_sa_dir + "/token", "r", encoding="utf-8") as fh:
|
||||
K8S_TOKEN = fh.read().strip()
|
||||
_ssl_ctx = ssl.create_default_context(cafile=_sa_dir + "/ca.crt")
|
||||
|
||||
|
||||
def k8s(method, path, body=None, ctype="application/json"):
|
||||
url = K8S_HOST + path
|
||||
data = None
|
||||
if body is not None:
|
||||
data = body if isinstance(body, bytes) else json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(url=url, method=method, data=data)
|
||||
req.add_header("Authorization", "Bearer " + K8S_TOKEN)
|
||||
req.add_header("Accept", "application/json")
|
||||
if data is not None:
|
||||
req.add_header("Content-Type", ctype)
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=_ssl_ctx, timeout=30) as resp:
|
||||
raw = resp.read()
|
||||
return resp.status, (json.loads(raw) if raw else {})
|
||||
except urllib.error.HTTPError as exc:
|
||||
raw = exc.read()
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except ValueError:
|
||||
payload = {"message": raw.decode("utf-8", "replace")}
|
||||
return exc.code, payload
|
||||
|
||||
|
||||
def core(path):
|
||||
return "/api/v1/namespaces/" + urllib.parse.quote(SESSIONS_NS) + path
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# セッション操作
|
||||
# --------------------------------------------------------------------------
|
||||
def repo_host(url):
|
||||
m = REPO_RE.match(url)
|
||||
if not m:
|
||||
return None
|
||||
return (m.group("h1") or m.group("h2") or m.group("h3") or "").lower()
|
||||
|
||||
|
||||
def list_sessions():
|
||||
status, payload = k8s("GET", core("/pods?labelSelector=" + urllib.parse.quote(SESSION_SELECTOR)))
|
||||
if status != 200:
|
||||
log.error("list pods failed: %s %s", status, payload)
|
||||
return []
|
||||
out = []
|
||||
for pod in payload.get("items", []):
|
||||
meta = pod.get("metadata", {})
|
||||
ann = meta.get("annotations", {})
|
||||
st = pod.get("status", {})
|
||||
out.append({
|
||||
"id": meta.get("labels", {}).get(LABEL_SID, ""),
|
||||
"phase": st.get("phase", "Unknown"),
|
||||
"repo": ann.get(ANN_PREFIX + "repo", ""),
|
||||
"ref": ann.get(ANN_PREFIX + "ref", ""),
|
||||
"identity": ann.get(ANN_PREFIX + "git-identity", ""),
|
||||
"owner": ann.get(ANN_PREFIX + "owner", ""),
|
||||
"createdAt": ann.get(ANN_PREFIX + "created-at", ""),
|
||||
"lastActivity": ann.get(ANN_PREFIX + "last-activity", ""),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def active_session_count():
|
||||
return sum(1 for s in list_sessions() if s["phase"] in ("Pending", "Running"))
|
||||
|
||||
|
||||
def build_pod(sid, repo, ref, identity_name, owner):
|
||||
now = str(int(time.time()))
|
||||
env_list = [
|
||||
{"name": "SESSION_ID", "value": sid},
|
||||
{"name": "REPO_URL", "value": repo},
|
||||
{"name": "REPO_REF", "value": ref},
|
||||
{"name": "HOME", "value": "/home/claude"},
|
||||
{"name": "WORKSPACE_DIR", "value": WORKSPACE_DIR},
|
||||
{"name": "TTYD_PORT", "value": str(TTYD_PORT)},
|
||||
{"name": "IDLE_TIMEOUT_SECONDS", "value": str(IDLE_TIMEOUT)},
|
||||
{"name": "AUTOSTART_CLAUDE", "value": AUTOSTART_CLAUDE},
|
||||
{"name": "VERIFY_COMMAND", "value": VERIFY_COMMAND},
|
||||
{"name": "AUTOPUSH_ENABLED", "value": str(bool(AUTOPUSH.get("enabled"))).lower()},
|
||||
{"name": "AUTOPUSH_BRANCH_PREFIX", "value": AUTOPUSH.get("branchPrefix", "claude/")},
|
||||
{"name": "AUTOPUSH_PROTECTED", "value": ",".join(AUTOPUSH.get("protectedBranches", []))},
|
||||
{"name": "EXTRA_APK_PACKAGES", "value": EXTRA_APK},
|
||||
{"name": "CLAUDE_CODE_VERSION", "value": CLAUDE_CODE_VERSION},
|
||||
{"name": "GIT_AUTHOR_NAME", "value": GIT_AUTHOR_NAME},
|
||||
{"name": "GIT_AUTHOR_EMAIL", "value": GIT_AUTHOR_EMAIL},
|
||||
{"name": "GIT_COMMITTER_NAME", "value": GIT_AUTHOR_NAME},
|
||||
{"name": "GIT_COMMITTER_EMAIL", "value": GIT_AUTHOR_EMAIL},
|
||||
]
|
||||
for key, val in ANTHROPIC_EXTRA_ENV.items():
|
||||
env_list.append({"name": str(key), "value": str(val)})
|
||||
|
||||
volumes = [
|
||||
{"name": "workspace", "emptyDir": {"sizeLimit": "4Gi"}},
|
||||
{"name": "home", "emptyDir": {"sizeLimit": "1Gi"}},
|
||||
{"name": "tmp", "emptyDir": {"medium": "Memory", "sizeLimit": "64Mi"}},
|
||||
{"name": "npm-cache", "emptyDir": {"sizeLimit": "1Gi"}},
|
||||
{"name": "bootstrap", "configMap": {"name": BOOTSTRAP_CM, "defaultMode": 0o555}},
|
||||
]
|
||||
mounts = [
|
||||
{"name": "workspace", "mountPath": WORKSPACE_DIR},
|
||||
{"name": "home", "mountPath": "/home/claude"},
|
||||
{"name": "tmp", "mountPath": "/tmp"},
|
||||
{"name": "npm-cache", "mountPath": "/home/claude/.npm"},
|
||||
{"name": "bootstrap", "mountPath": "/bootstrap", "readOnly": True},
|
||||
]
|
||||
|
||||
ident = GIT_IDENTITIES.get(identity_name)
|
||||
if ident:
|
||||
env_list.append({"name": "GIT_IDENTITY_TYPE", "value": ident.get("type", "")})
|
||||
if ident.get("type") == "ssh":
|
||||
volumes.append({
|
||||
"name": "git-identity",
|
||||
"secret": {
|
||||
"secretName": ident["secret"],
|
||||
"defaultMode": 0o400,
|
||||
"items": [
|
||||
{"key": ident.get("keyPrivateKey", "id"), "path": "id"},
|
||||
{"key": ident.get("keyKnownHosts", "known_hosts"), "path": "known_hosts"},
|
||||
],
|
||||
},
|
||||
})
|
||||
mounts.append({"name": "git-identity", "mountPath": "/git-identity", "readOnly": True})
|
||||
elif ident.get("type") == "https-token":
|
||||
volumes.append({
|
||||
"name": "git-identity",
|
||||
"secret": {
|
||||
"secretName": ident["secret"],
|
||||
"defaultMode": 0o400,
|
||||
"items": [{"key": ident.get("keyToken", "token"), "path": "token"}],
|
||||
},
|
||||
})
|
||||
mounts.append({"name": "git-identity", "mountPath": "/git-identity", "readOnly": True})
|
||||
env_list.append({"name": "GIT_HTTPS_USERNAME", "value": ident.get("username", "x-access-token")})
|
||||
|
||||
container = {
|
||||
"name": "claude",
|
||||
"image": SESSION_IMAGE,
|
||||
"imagePullPolicy": SESSION_IMAGE_PULL_POLICY,
|
||||
"command": ["/bin/sh", "/bootstrap/entrypoint.sh"],
|
||||
"env": env_list,
|
||||
"envFrom": [{"secretRef": {"name": ANTHROPIC_SECRET}}],
|
||||
"ports": [{"name": "ttyd", "containerPort": TTYD_PORT}],
|
||||
"volumeMounts": mounts,
|
||||
"resources": SESSION_RESOURCES,
|
||||
"securityContext": {
|
||||
"allowPrivilegeEscalation": False,
|
||||
"readOnlyRootFilesystem": True,
|
||||
"runAsNonRoot": True,
|
||||
"runAsUser": SESSION_RUN_UID,
|
||||
"capabilities": {"drop": ["ALL"]},
|
||||
},
|
||||
"readinessProbe": {
|
||||
"tcpSocket": {"port": TTYD_PORT},
|
||||
"initialDelaySeconds": 5,
|
||||
"periodSeconds": 5,
|
||||
"failureThreshold": 60,
|
||||
},
|
||||
}
|
||||
|
||||
labels = {
|
||||
LABEL_APP: "claude-code",
|
||||
LABEL_COMPONENT: "session",
|
||||
"app.kubernetes.io/managed-by": "claude-code-manager",
|
||||
LABEL_SID: sid,
|
||||
}
|
||||
labels.update({str(k): str(v) for k, v in POD_LABELS.items()})
|
||||
annotations = {
|
||||
ANN_PREFIX + "repo": repo,
|
||||
ANN_PREFIX + "ref": ref,
|
||||
ANN_PREFIX + "git-identity": identity_name or "",
|
||||
ANN_PREFIX + "owner": owner or "anon",
|
||||
ANN_PREFIX + "created-at": now,
|
||||
ANN_PREFIX + "last-activity": now,
|
||||
}
|
||||
annotations.update({str(k): str(v) for k, v in POD_ANNOTATIONS.items()})
|
||||
|
||||
spec = {
|
||||
"serviceAccountName": SESSION_SA,
|
||||
"automountServiceAccountToken": False,
|
||||
"restartPolicy": "Never",
|
||||
"activeDeadlineSeconds": MAX_LIFETIME,
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"enableServiceLinks": False,
|
||||
"securityContext": {
|
||||
"runAsNonRoot": True,
|
||||
"runAsUser": SESSION_RUN_UID,
|
||||
"runAsGroup": SESSION_RUN_UID,
|
||||
"fsGroup": SESSION_RUN_UID,
|
||||
"seccompProfile": {"type": "RuntimeDefault"},
|
||||
},
|
||||
"containers": [container],
|
||||
"volumes": volumes,
|
||||
}
|
||||
if SESSION_IMAGE_PULL_SECRETS:
|
||||
spec["imagePullSecrets"] = SESSION_IMAGE_PULL_SECRETS
|
||||
if NODE_SELECTOR:
|
||||
spec["nodeSelector"] = NODE_SELECTOR
|
||||
if TOLERATIONS:
|
||||
spec["tolerations"] = TOLERATIONS
|
||||
if PRIORITY_CLASS:
|
||||
spec["priorityClassName"] = PRIORITY_CLASS
|
||||
|
||||
return {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {"name": "claude-sess-" + sid, "namespace": SESSIONS_NS,
|
||||
"labels": labels, "annotations": annotations},
|
||||
"spec": spec,
|
||||
}
|
||||
|
||||
|
||||
def build_service(sid):
|
||||
return {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "claude-sess-" + sid,
|
||||
"namespace": SESSIONS_NS,
|
||||
"labels": {LABEL_APP: "claude-code", LABEL_COMPONENT: "session", LABEL_SID: sid},
|
||||
},
|
||||
"spec": {
|
||||
"selector": {LABEL_SID: sid},
|
||||
"ports": [{"name": "ttyd", "port": TTYD_PORT, "targetPort": TTYD_PORT}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_session(repo, ref, identity_name, owner):
|
||||
repo = (repo or "").strip()
|
||||
ref = (ref or "main").strip()
|
||||
if not REPO_RE.match(repo):
|
||||
return None, "リポジトリ URL の形式が不正です"
|
||||
if not REF_RE.match(ref):
|
||||
return None, "ref の形式が不正です"
|
||||
host = repo_host(repo)
|
||||
if not GIT_ALLOW_ARBITRARY and host not in GIT_ALLOWED_HOSTS:
|
||||
return None, "許可されていない Git ホストです: " + str(host)
|
||||
if identity_name and identity_name not in GIT_IDENTITIES:
|
||||
return None, "未知の git identity: " + identity_name
|
||||
if active_session_count() >= MAX_SESSIONS:
|
||||
return None, "同時セッション数の上限(%d)に達しています" % MAX_SESSIONS
|
||||
|
||||
sid = secrets.token_hex(5)
|
||||
status, payload = k8s("POST", core("/pods"), build_pod(sid, repo, ref, identity_name, owner))
|
||||
if status not in (200, 201):
|
||||
return None, "Pod 作成に失敗: %s" % payload.get("message", status)
|
||||
status, payload = k8s("POST", core("/services"), build_service(sid))
|
||||
if status not in (200, 201):
|
||||
k8s("DELETE", core("/pods/claude-sess-" + sid))
|
||||
return None, "Service 作成に失敗: %s" % payload.get("message", status)
|
||||
log.info("session %s created repo=%s ref=%s owner=%s", sid, repo, ref, owner)
|
||||
return sid, None
|
||||
|
||||
|
||||
def delete_session(sid):
|
||||
if not SID_RE.match(sid):
|
||||
return False
|
||||
k8s("DELETE", core("/pods/claude-sess-" + sid), {"gracePeriodSeconds": 10})
|
||||
k8s("DELETE", core("/services/claude-sess-" + sid))
|
||||
log.info("session %s deleted", sid)
|
||||
return True
|
||||
|
||||
|
||||
_touch_cache = {}
|
||||
|
||||
|
||||
def touch_session(sid, min_interval=15):
|
||||
if not SID_RE.match(sid):
|
||||
return
|
||||
now = time.time()
|
||||
if now - _touch_cache.get(sid, 0) < min_interval:
|
||||
return
|
||||
_touch_cache[sid] = now
|
||||
patch = {"metadata": {"annotations": {ANN_PREFIX + "last-activity": str(int(now))}}}
|
||||
k8s("PATCH", core("/pods/claude-sess-" + sid), patch,
|
||||
ctype="application/merge-patch+json")
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 回収ループ
|
||||
# --------------------------------------------------------------------------
|
||||
def reaper_loop():
|
||||
while True:
|
||||
time.sleep(REAP_INTERVAL)
|
||||
try:
|
||||
reap_once()
|
||||
except Exception as exc: # noqa: BLE001 - ループを止めない
|
||||
log.exception("reaper error: %s", exc)
|
||||
|
||||
|
||||
def reap_once():
|
||||
now = int(time.time())
|
||||
live_ids = set()
|
||||
for s in list_sessions():
|
||||
sid = s["id"]
|
||||
live_ids.add(sid)
|
||||
reason = None
|
||||
if s["phase"] in ("Succeeded", "Failed"):
|
||||
reason = "phase=" + s["phase"]
|
||||
else:
|
||||
try:
|
||||
created = int(s["createdAt"] or 0)
|
||||
last = int(s["lastActivity"] or 0)
|
||||
except ValueError:
|
||||
created = last = now
|
||||
if created and now - created > MAX_LIFETIME:
|
||||
reason = "max-lifetime"
|
||||
elif last and now - last > IDLE_TIMEOUT:
|
||||
reason = "idle"
|
||||
if reason:
|
||||
log.info("reaping session %s (%s)", sid, reason)
|
||||
delete_session(sid)
|
||||
live_ids.discard(sid)
|
||||
# 孤児 Service の掃除
|
||||
status, payload = k8s("GET", core("/services?labelSelector=" + urllib.parse.quote(SESSION_SELECTOR)))
|
||||
if status == 200:
|
||||
for svc in payload.get("items", []):
|
||||
sid = svc.get("metadata", {}).get("labels", {}).get(LABEL_SID, "")
|
||||
if sid and sid not in live_ids:
|
||||
log.info("reaping orphan service %s", sid)
|
||||
k8s("DELETE", core("/services/claude-sess-" + sid))
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 認証(token モード)
|
||||
# --------------------------------------------------------------------------
|
||||
def extract_token(handler):
|
||||
auth = handler.headers.get("Authorization", "")
|
||||
if auth.startswith("Bearer "):
|
||||
return auth[7:].strip()
|
||||
cookie = handler.headers.get("Cookie", "")
|
||||
for part in cookie.split(";"):
|
||||
part = part.strip()
|
||||
if part.startswith("cc_token="):
|
||||
return urllib.parse.unquote(part[9:])
|
||||
qs = urllib.parse.urlparse(handler.path).query
|
||||
params = urllib.parse.parse_qs(qs)
|
||||
if "token" in params:
|
||||
return params["token"][0]
|
||||
return ""
|
||||
|
||||
|
||||
def token_ok(handler):
|
||||
if AUTH_MODE != "token":
|
||||
return True
|
||||
if not AUTH_TOKEN:
|
||||
return False
|
||||
return hmac.compare_digest(extract_token(handler), AUTH_TOKEN)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# HTTP ハンドラ
|
||||
# --------------------------------------------------------------------------
|
||||
INDEX_HTML = """<!doctype html>
|
||||
<html lang="ja"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Claude Code — Remote Sessions</title>
|
||||
<style>
|
||||
:root{color-scheme:light dark}
|
||||
*{box-sizing:border-box}
|
||||
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;margin:0;padding:1rem;max-width:760px;margin-inline:auto}
|
||||
h1{font-size:1.15rem;margin:.2rem 0 1rem}
|
||||
form,.card{border:1px solid #8884;border-radius:10px;padding:1rem;margin-bottom:1rem}
|
||||
label{display:block;font-weight:600;margin:.5rem 0 .2rem}
|
||||
input,select,button{font:inherit;padding:.55rem .6rem;border-radius:8px;border:1px solid #8886;width:100%}
|
||||
button{background:#4338ca;color:#fff;border:0;font-weight:600;cursor:pointer;margin-top:.8rem}
|
||||
button.sec{background:#8883;color:inherit}
|
||||
.row{display:flex;gap:.5rem;align-items:center}
|
||||
.row>*{width:auto}
|
||||
table{width:100%;border-collapse:collapse;font-size:.92rem}
|
||||
td,th{text-align:left;padding:.45rem .3rem;border-bottom:1px solid #8883;vertical-align:top}
|
||||
a.open{display:inline-block;background:#0891b2;color:#fff;padding:.3rem .7rem;border-radius:6px;text-decoration:none}
|
||||
.muted{opacity:.65;font-size:.85rem}
|
||||
code{background:#8882;padding:.1rem .3rem;border-radius:4px}
|
||||
</style></head><body>
|
||||
<h1>Claude Code — Remote Sessions</h1>
|
||||
<div id="login" hidden>
|
||||
<form onsubmit="return doLogin(event)">
|
||||
<label for="tk">アクセストークン</label>
|
||||
<input id="tk" type="password" autocomplete="current-password" placeholder="Bearer token">
|
||||
<button type="submit">ログイン</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="app" hidden>
|
||||
<form onsubmit="return doCreate(event)">
|
||||
<label for="repo">リポジトリ URL</label>
|
||||
<input id="repo" placeholder="https://github.com/owner/repo.git または git@host:owner/repo.git" required>
|
||||
<div class="row">
|
||||
<div style="flex:1"><label for="ref">ブランチ / タグ</label><input id="ref" value="main"></div>
|
||||
<div style="flex:1"><label for="ident">認証 identity</label><select id="ident"></select></div>
|
||||
</div>
|
||||
<button type="submit">セッション作成</button>
|
||||
</form>
|
||||
<div class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<strong>セッション一覧</strong>
|
||||
<button class="sec" style="width:auto;margin:0" onclick="refresh()">更新</button>
|
||||
</div>
|
||||
<table><thead><tr><th>ID</th><th>状態</th><th>リポジトリ</th><th></th></tr></thead>
|
||||
<tbody id="rows"></tbody></table>
|
||||
<p class="muted">アイドル <span id="idle"></span> 秒で自動破棄されます。</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const $=s=>document.querySelector(s);
|
||||
let identities=[];
|
||||
function api(path,opts){opts=opts||{};opts.credentials='same-origin';return fetch(path,opts);}
|
||||
async function boot(){
|
||||
const r=await api('/api/config');
|
||||
if(r.status===401){$('#login').hidden=false;return;}
|
||||
const c=await r.json();
|
||||
identities=c.identities||[];
|
||||
$('#idle').textContent=c.idleTimeoutSeconds;
|
||||
const sel=$('#ident');
|
||||
sel.innerHTML='<option value="">(認証なし / 公開リポジトリ)</option>'+identities.map(i=>`<option>${i}</option>`).join('');
|
||||
$('#app').hidden=false;
|
||||
refresh();
|
||||
}
|
||||
async function doLogin(e){e.preventDefault();
|
||||
const r=await api('/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:$('#tk').value})});
|
||||
if(r.ok){location.reload();}else{alert('トークンが違います');}
|
||||
return false;
|
||||
}
|
||||
async function doCreate(e){e.preventDefault();
|
||||
const body={repo:$('#repo').value.trim(),ref:$('#ref').value.trim()||'main',identity:$('#ident').value};
|
||||
const r=await api('/api/sessions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const j=await r.json();
|
||||
if(!r.ok){alert(j.error||'作成に失敗しました');return false;}
|
||||
$('#repo').value='';refresh();return false;
|
||||
}
|
||||
async function del(id){if(!confirm('セッション '+id+' を破棄しますか?'))return;
|
||||
await api('/api/sessions/'+id,{method:'DELETE'});refresh();}
|
||||
async function refresh(){
|
||||
const r=await api('/api/sessions');if(!r.ok)return;
|
||||
const list=(await r.json()).sessions||[];
|
||||
$('#rows').innerHTML=list.map(s=>`<tr>
|
||||
<td><code>${s.id}</code></td>
|
||||
<td>${s.phase}</td>
|
||||
<td style="word-break:break-all">${s.repo||''}<div class="muted">${s.ref||''}</div></td>
|
||||
<td><a class="open" href="/s/${s.id}/" target="_blank" rel="noopener">開く</a>
|
||||
<button class="sec" style="width:auto;margin:.3rem 0 0" onclick="del('${s.id}')">破棄</button></td>
|
||||
</tr>`).join('')||'<tr><td colspan=4 class="muted">セッションはありません</td></tr>';
|
||||
}
|
||||
boot();
|
||||
</script></body></html>
|
||||
"""
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "claude-code-manager"
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def _send(self, code, body=b"", ctype="application/json; charset=utf-8", extra=None):
|
||||
if isinstance(body, str):
|
||||
body = body.encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", ctype)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
for key, val in (extra or {}).items():
|
||||
self.send_header(key, val)
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(body)
|
||||
|
||||
def _json(self, code, obj, extra=None):
|
||||
self._send(code, json.dumps(obj), "application/json; charset=utf-8", extra)
|
||||
|
||||
def _body(self):
|
||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
if length <= 0:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(self.rfile.read(length) or b"{}")
|
||||
except ValueError:
|
||||
return {}
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
log.debug("%s - %s", self.address_string(), fmt % args)
|
||||
|
||||
# ---- ルーティング ----
|
||||
def do_GET(self):
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
if path == "/healthz":
|
||||
return self._send(200, b'{"ok":true}')
|
||||
if path == "/auth":
|
||||
# nginx auth_request 用(token モードのみ意味を持つ)
|
||||
if token_ok(self):
|
||||
orig = self.headers.get("X-Original-URI", "")
|
||||
m = re.search(r"/s/([a-f0-9]{10})/", orig)
|
||||
if m:
|
||||
try:
|
||||
touch_session(m.group(1))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return self._send(200, b"ok", "text/plain")
|
||||
return self._send(401, b"unauthorized", "text/plain")
|
||||
if path == "/" or path == "/index.html":
|
||||
return self._send(200, INDEX_HTML, "text/html; charset=utf-8")
|
||||
if path == "/api/config":
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
return self._json(200, {
|
||||
"identities": sorted(GIT_IDENTITIES.keys()),
|
||||
"idleTimeoutSeconds": IDLE_TIMEOUT,
|
||||
"maxLifetimeSeconds": MAX_LIFETIME,
|
||||
"maxSessions": MAX_SESSIONS,
|
||||
"authMode": AUTH_MODE,
|
||||
})
|
||||
if path == "/api/sessions":
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
return self._json(200, {"sessions": list_sessions()})
|
||||
return self._send(404, b'{"error":"not found"}')
|
||||
|
||||
def do_POST(self):
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
if path == "/login":
|
||||
body = self._body()
|
||||
token = str(body.get("token", ""))
|
||||
if AUTH_MODE == "token" and AUTH_TOKEN and hmac.compare_digest(token, AUTH_TOKEN):
|
||||
cookie = ("cc_token=" + urllib.parse.quote(token) +
|
||||
"; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000")
|
||||
return self._json(200, {"ok": True}, {"Set-Cookie": cookie})
|
||||
return self._json(401, {"error": "invalid token"})
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
if path == "/api/sessions":
|
||||
body = self._body()
|
||||
owner = self.headers.get("X-Forwarded-User", "") or "anon"
|
||||
sid, err = create_session(body.get("repo", ""), body.get("ref", "main"),
|
||||
body.get("identity", ""), owner)
|
||||
if err:
|
||||
return self._json(400, {"error": err})
|
||||
return self._json(201, {"id": sid, "url": "/s/" + sid + "/"})
|
||||
m = re.match(r"^/api/sessions/([a-f0-9]{10})/touch$", path)
|
||||
if m:
|
||||
touch_session(m.group(1), min_interval=0)
|
||||
return self._json(200, {"ok": True})
|
||||
return self._send(404, b'{"error":"not found"}')
|
||||
|
||||
def do_DELETE(self):
|
||||
if not token_ok(self):
|
||||
return self._json(401, {"error": "unauthorized"})
|
||||
m = re.match(r"^/api/sessions/([a-f0-9]{10})$", urllib.parse.urlparse(self.path).path)
|
||||
if not m:
|
||||
return self._send(404, b'{"error":"not found"}')
|
||||
delete_session(m.group(1))
|
||||
return self._json(200, {"ok": True})
|
||||
|
||||
|
||||
def main():
|
||||
log.info("claude-code manager starting: ns=%s image=%s idle=%ds maxLife=%ds maxSessions=%d authMode=%s",
|
||||
SESSIONS_NS, SESSION_IMAGE, IDLE_TIMEOUT, MAX_LIFETIME, MAX_SESSIONS, AUTH_MODE)
|
||||
if AUTH_MODE == "token" and not AUTH_TOKEN:
|
||||
log.warning("AUTH_MODE=token だがトークンが空。全リクエストを拒否します。")
|
||||
threading.Thread(target=reaper_loop, name="reaper", daemon=True).start()
|
||||
httpd = ThreadingHTTPServer(("0.0.0.0", LISTEN_PORT), Handler)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if "--reap-once" in sys.argv[1:]:
|
||||
# 予備の CronJob から呼ばれる一回きりの回収。manager 障害時の保険。
|
||||
log.info("one-shot reap starting (ns=%s)", SESSIONS_NS)
|
||||
reap_once()
|
||||
log.info("one-shot reap done")
|
||||
else:
|
||||
main()
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/bin/sh
|
||||
# ==========================================================================
|
||||
# claude-code セッションブート(使い捨てコンテナ内で1回だけ実行)
|
||||
#
|
||||
# 1. git 認証(SSH 鍵 / HTTPS トークン)を $HOME にセットアップ
|
||||
# 2. 許可済みリポジトリを $WORKSPACE_DIR へ clone
|
||||
# 3. `claude` を PATH に用意(無ければ非 root で $HOME にインストール)
|
||||
# 4. ttyd でブラウザにシェル(+ 任意で claude 自動起動)を提供
|
||||
# 5. アイドルウォッチドッグ:無通信が続いたら自身を終了 → Pod 破棄
|
||||
#
|
||||
# 前提: 非 root(uid=10001) / readOnlyRootFilesystem。
|
||||
# 書き込み可能なのは $HOME, $WORKSPACE_DIR, /tmp のみ。
|
||||
# git / ttyd / node は事前ビルドイメージに同梱されている想定
|
||||
# (files/Dockerfile.session)。未同梱時は下記フォールバックが走る。
|
||||
# ==========================================================================
|
||||
set -eu
|
||||
|
||||
log() { printf '\033[36m[boot]\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[31m[boot:ERROR]\033[0m %s\n' "$*" >&2; }
|
||||
|
||||
: "${HOME:=/home/claude}"
|
||||
: "${WORKSPACE_DIR:=/workspace}"
|
||||
: "${TTYD_PORT:=7681}"
|
||||
: "${IDLE_TIMEOUT_SECONDS:=1800}"
|
||||
: "${AUTOSTART_CLAUDE:=true}"
|
||||
: "${REPO_REF:=main}"
|
||||
export HOME
|
||||
export NPM_CONFIG_CACHE="$HOME/.npm"
|
||||
export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH"
|
||||
|
||||
mkdir -p "$HOME/.ssh" "$WORKSPACE_DIR" "$HOME/.npm-global"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 1. git 認証セットアップ
|
||||
# --------------------------------------------------------------------------
|
||||
git config --global user.name "${GIT_AUTHOR_NAME:-Claude}"
|
||||
git config --global user.email "${GIT_AUTHOR_EMAIL:-claude@cafepieters.com}"
|
||||
git config --global safe.directory '*'
|
||||
git config --global advice.detachedHead false
|
||||
git config --global credential.helper ''
|
||||
|
||||
ASKPASS_DISABLE() { export GIT_TERMINAL_PROMPT=0; }
|
||||
ASKPASS_DISABLE
|
||||
|
||||
case "${GIT_IDENTITY_TYPE:-}" in
|
||||
ssh)
|
||||
if [ ! -r /git-identity/id ]; then err "SSH 鍵がマウントされていません"; exit 1; fi
|
||||
install -m 600 /git-identity/id "$HOME/.ssh/id_session"
|
||||
# known_hosts を必須にし、ホスト鍵の TOFU を無効化する
|
||||
if [ -r /git-identity/known_hosts ]; then
|
||||
install -m 644 /git-identity/known_hosts "$HOME/.ssh/known_hosts"
|
||||
else
|
||||
err "known_hosts がありません(StrictHostKeyChecking を維持するため必須)"; exit 1
|
||||
fi
|
||||
cat > "$HOME/.ssh/config" <<EOF
|
||||
Host *
|
||||
IdentityFile $HOME/.ssh/id_session
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking yes
|
||||
UserKnownHostsFile $HOME/.ssh/known_hosts
|
||||
PasswordAuthentication no
|
||||
EOF
|
||||
chmod 600 "$HOME/.ssh/config"
|
||||
export GIT_SSH_COMMAND="ssh -F $HOME/.ssh/config"
|
||||
log "SSH 認証を構成しました"
|
||||
;;
|
||||
https-token)
|
||||
if [ ! -r /git-identity/token ]; then err "HTTPS トークンがマウントされていません"; exit 1; fi
|
||||
_u="${GIT_HTTPS_USERNAME:-x-access-token}"
|
||||
_t="$(cat /git-identity/token)"
|
||||
# トークンをプロセス環境にもファイルにも平文で残さず、credential helper で都度供給
|
||||
cat > "$HOME/.git-credentials" <<EOF
|
||||
https://$(printf '%s' "$_u" | sed 's/@/%40/g'):$(printf '%s' "$_t" | sed 's/@/%40/g')@$(printf '%s\n' "$REPO_URL" | sed -E 's#^https://([^/]+)/.*#\1#')
|
||||
EOF
|
||||
chmod 600 "$HOME/.git-credentials"
|
||||
git config --global credential.helper "store --file=$HOME/.git-credentials"
|
||||
unset _t
|
||||
log "HTTPS トークン認証を構成しました"
|
||||
;;
|
||||
*)
|
||||
log "git 認証なし(公開リポジトリのみ clone 可能)"
|
||||
;;
|
||||
esac
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 2. clone
|
||||
# --------------------------------------------------------------------------
|
||||
REPO_DIR="$WORKSPACE_DIR/$(basename "${REPO_URL%.git}")"
|
||||
if [ -d "$REPO_DIR/.git" ]; then
|
||||
log "既存チェックアウトを再利用: $REPO_DIR"
|
||||
else
|
||||
log "clone 中: $REPO_URL ($REPO_REF)"
|
||||
if ! git clone --filter=blob:none --no-tags "$REPO_URL" "$REPO_DIR"; then
|
||||
err "clone に失敗しました"; exit 1
|
||||
fi
|
||||
fi
|
||||
cd "$REPO_DIR"
|
||||
git checkout "$REPO_REF" 2>/dev/null || git checkout -b "$REPO_REF" 2>/dev/null || true
|
||||
log "作業ディレクトリ: $REPO_DIR ($(git rev-parse --short HEAD 2>/dev/null || echo '?'))"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 3. claude CLI を用意
|
||||
# --------------------------------------------------------------------------
|
||||
if ! command -v claude >/dev/null 2>&1; then
|
||||
log "claude が未同梱のため $HOME にインストールします(初回は時間がかかります)"
|
||||
npm config set prefix "$HOME/.npm-global" >/dev/null 2>&1 || true
|
||||
_ver="${CLAUDE_CODE_VERSION:-}"
|
||||
_pkg="@anthropic-ai/claude-code${_ver:+@$_ver}"
|
||||
if ! npm install -g --no-fund --no-audit "$_pkg"; then
|
||||
err "claude-code のインストールに失敗しました。事前ビルドイメージの利用を推奨します。"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log "claude: $(claude --version 2>/dev/null || echo 'unknown')"
|
||||
|
||||
# 追加 apk(root 権限がある事前ビルドイメージのみ有効。非 root では警告のみ)
|
||||
if [ -n "${EXTRA_APK_PACKAGES:-}" ]; then
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
apk add --no-cache $EXTRA_APK_PACKAGES || err "apk add に一部失敗"
|
||||
else
|
||||
err "EXTRA_APK_PACKAGES は非 root では無視されます: $EXTRA_APK_PACKAGES"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 4. 補助コマンド(claude-verify / claude-push)
|
||||
# --------------------------------------------------------------------------
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
cat > "$HOME/.local/bin/claude-verify" <<'EOF'
|
||||
#!/bin/sh
|
||||
# セッション設定の検証コマンドを実行する
|
||||
if [ -z "${VERIFY_COMMAND:-}" ]; then echo "verifyCommand は未設定です"; exit 0; fi
|
||||
echo "+ $VERIFY_COMMAND"
|
||||
exec sh -c "$VERIFY_COMMAND"
|
||||
EOF
|
||||
chmod +x "$HOME/.local/bin/claude-verify"
|
||||
|
||||
cat > "$HOME/.local/bin/claude-push" <<'EOF'
|
||||
#!/bin/sh
|
||||
# 検証 → 安全なブランチへ push(デフォルト/保護ブランチ・--force は禁止)
|
||||
set -eu
|
||||
if [ "${AUTOPUSH_ENABLED:-false}" != "true" ]; then
|
||||
echo "autoPush は無効です(values: session.autoPush.enabled)。手動 push は 'git push' を使ってください。" >&2
|
||||
exit 1
|
||||
fi
|
||||
cur="$(git rev-parse --abbrev-ref HEAD)"
|
||||
prefix="${AUTOPUSH_BRANCH_PREFIX:-claude/}"
|
||||
protected="${AUTOPUSH_PROTECTED:-main,master,develop,release}"
|
||||
case ",$protected," in *",$cur,"*)
|
||||
echo "現在のブランチ '$cur' は保護対象です。作業ブランチへ切り替えてください。" >&2; exit 1;;
|
||||
esac
|
||||
case "$cur" in "$prefix"*) target="$cur" ;; *) target="${prefix}${cur}" ;; esac
|
||||
if [ -n "${VERIFY_COMMAND:-}" ]; then
|
||||
echo "== verify: $VERIFY_COMMAND"
|
||||
sh -c "$VERIFY_COMMAND" || { echo "検証に失敗したため push を中止します。" >&2; exit 1; }
|
||||
fi
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add -A
|
||||
git commit -m "${1:-chore: work from claude-code session $SESSION_ID}"
|
||||
fi
|
||||
[ "$target" = "$cur" ] || git branch -f "$target" "$cur"
|
||||
echo "== push origin $target(--force なし)"
|
||||
exec git push origin "HEAD:$target"
|
||||
EOF
|
||||
chmod +x "$HOME/.local/bin/claude-push"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 5. アイドルウォッチドッグ
|
||||
# --------------------------------------------------------------------------
|
||||
idle_watchdog() {
|
||||
# ttyd への ESTABLISHED 接続が IDLE_TIMEOUT_SECONDS の間ゼロなら Pod を終了させる
|
||||
last_seen="$(date +%s)"
|
||||
while true; do
|
||||
sleep 30
|
||||
conns=0
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
conns="$(ss -Htn "sport = :$TTYD_PORT" state established 2>/dev/null | wc -l | tr -d ' ')"
|
||||
elif [ -r /proc/net/tcp ]; then
|
||||
hexport="$(printf '%04X' "$TTYD_PORT")"
|
||||
conns="$(awk -v p=":$hexport" '$2 ~ p"$" && $4=="01"{n++} END{print n+0}' /proc/net/tcp)"
|
||||
fi
|
||||
now="$(date +%s)"
|
||||
if [ "${conns:-0}" -gt 0 ]; then
|
||||
last_seen="$now"
|
||||
elif [ $(( now - last_seen )) -ge "$IDLE_TIMEOUT_SECONDS" ]; then
|
||||
err "アイドル ${IDLE_TIMEOUT_SECONDS}s を超過。セッションを終了します。"
|
||||
kill -TERM "$TTYD_PID" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 6. ttyd 起動
|
||||
# --------------------------------------------------------------------------
|
||||
if ! command -v ttyd >/dev/null 2>&1; then
|
||||
err "ttyd が見つかりません。事前ビルドイメージ(files/Dockerfile.session)を利用してください。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SHELL_CMD="/bin/bash"
|
||||
command -v bash >/dev/null 2>&1 || SHELL_CMD="/bin/sh"
|
||||
|
||||
cat > "$HOME/.session-shell" <<EOF
|
||||
#!/bin/sh
|
||||
cd "$REPO_DIR" || cd "$WORKSPACE_DIR"
|
||||
export PATH="$PATH"
|
||||
echo "── Claude Code セッション $SESSION_ID ──────────────────────────"
|
||||
echo "リポジトリ : $REPO_URL ($REPO_REF)"
|
||||
echo "検証コマンド: ${VERIFY_COMMAND:-(未設定)} 補助: claude-verify / claude-push"
|
||||
echo "─────────────────────────────────────────────────────────────"
|
||||
if [ "${AUTOSTART_CLAUDE}" = "true" ] && command -v claude >/dev/null 2>&1; then
|
||||
exec claude
|
||||
fi
|
||||
exec $SHELL_CMD -l
|
||||
EOF
|
||||
chmod +x "$HOME/.session-shell"
|
||||
|
||||
log "ttyd を 0.0.0.0:$TTYD_PORT で起動します"
|
||||
ttyd --port "$TTYD_PORT" --interface 0.0.0.0 --writable \
|
||||
--client-option fontSize=14 --client-option 'theme={"background":"#1e1e1e"}' \
|
||||
--terminal-type xterm-256color \
|
||||
"$HOME/.session-shell" &
|
||||
TTYD_PID=$!
|
||||
|
||||
idle_watchdog &
|
||||
|
||||
wait "$TTYD_PID"
|
||||
@@ -0,0 +1,57 @@
|
||||
claude-code ({{ .Chart.Version }}) をデプロイしました。
|
||||
|
||||
制御プレーン : {{ include "claude-code.fullname" . }} (namespace: {{ .Release.Namespace }})
|
||||
セッション NS : {{ include "claude-code.sessionsNamespace" . }}
|
||||
セッション像 : {{ .Values.image.session.registry }}/{{ .Values.image.session.repository }}:{{ .Values.image.session.tag }}
|
||||
認証モード : {{ .Values.auth.mode }}
|
||||
アイドル破棄 : {{ .Values.session.idleTimeoutSeconds }}s / 最大寿命 {{ .Values.session.maxLifetimeSeconds }}s / 同時 {{ .Values.session.maxSessions }}
|
||||
|
||||
アクセス方法:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
https://{{ .host }}/
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "claude-code.fullname" . }} 8080:{{ .Values.service.port }}
|
||||
→ http://localhost:8080/
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.auth.mode "token" }}
|
||||
|
||||
アクセストークンの取得:
|
||||
kubectl -n {{ .Release.Namespace }} get secret {{ include "claude-code.authSecretName" . }} \
|
||||
-o jsonpath='{.data.{{ .Values.auth.token.existingSecretKey | default "token" }}}' | base64 -d ; echo
|
||||
|
||||
UI の「アクセストークン」欄に貼り付けるか、`?token=<TOKEN>` を付けて開いてください。
|
||||
{{- else if eq .Values.auth.mode "basic" }}
|
||||
|
||||
Basic 認証のユーザーでログインしてください。
|
||||
{{- else if eq .Values.auth.mode "forward" }}
|
||||
|
||||
外部認証プロキシ({{ .Values.auth.forward.url }})経由でログインしてください。
|
||||
{{- else }}
|
||||
|
||||
⚠ auth.mode=none — 認証なしで公開されています。信頼済みネットワーク限定で使用してください。
|
||||
{{- end }}
|
||||
|
||||
{{- if and (not .Values.anthropic.existingSecret) (not .Values.anthropic.apiKey) (not .Values.anthropic.oauthToken) }}
|
||||
|
||||
⚠ Claude Code の認証情報が未設定です(anthropic.apiKey / oauthToken / existingSecret のいずれも空)。
|
||||
セッション内の `claude` は認証に失敗します。`--set anthropic.apiKey=...` で再適用してください。
|
||||
{{- end }}
|
||||
{{- if not .Values.git.identities }}
|
||||
|
||||
⚠ git.identities が未設定です。公開リポジトリの clone のみ可能です。
|
||||
GitHub / Gitea へ push するには values に SSH 鍵か HTTPS トークンを登録してください(README 参照)。
|
||||
{{- end }}
|
||||
|
||||
使い方:
|
||||
1. UI で「リポジトリ URL」「ブランチ」「認証 identity」を指定してセッション作成
|
||||
2. 一覧の「開く」でブラウザ内ターミナル(Claude Code)に接続
|
||||
3. セッション内で `claude-verify` / `claude-push` が利用可能
|
||||
4. 無操作 {{ .Values.session.idleTimeoutSeconds }}s でセッション(Pod)は自動破棄されます
|
||||
|
||||
許可された Git ホスト: {{ join ", " .Values.git.allowedHosts }}
|
||||
{{- if .Values.git.allowArbitraryRepos }}
|
||||
(allowArbitraryRepos=true のため任意ホストを許可)
|
||||
{{- end }}
|
||||
@@ -0,0 +1,112 @@
|
||||
{{/*
|
||||
==========================================================================
|
||||
claude-code — 共通ヘルパー
|
||||
==========================================================================
|
||||
*/}}
|
||||
|
||||
{{- define "claude-code.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "claude-code.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := include "claude-code.name" . -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "claude-code.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* 制御プレーン共通ラベル */}}
|
||||
{{- define "claude-code.labels" -}}
|
||||
helm.sh/chart: {{ include "claude-code.chart" . }}
|
||||
{{ include "claude-code.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: claude-code
|
||||
{{- with .Values.commonLabels }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "claude-code.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "claude-code.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/* セッション Pod を作成するネームスペース */}}
|
||||
{{- define "claude-code.sessionsNamespace" -}}
|
||||
{{- default .Release.Namespace .Values.sessionsNamespace.name -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* 制御プレーン ServiceAccount 名 */}}
|
||||
{{- define "claude-code.managerServiceAccountName" -}}
|
||||
{{- printf "%s-manager" (include "claude-code.fullname" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* セッション ServiceAccount 名 */}}
|
||||
{{- define "claude-code.sessionServiceAccountName" -}}
|
||||
{{- printf "%s-session" (include "claude-code.fullname" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* 認証トークン Secret 名 */}}
|
||||
{{- define "claude-code.authSecretName" -}}
|
||||
{{- if .Values.auth.token.existingSecret -}}
|
||||
{{- .Values.auth.token.existingSecret -}}
|
||||
{{- else if .Values.auth.basic.existingSecret -}}
|
||||
{{- .Values.auth.basic.existingSecret -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-auth" (include "claude-code.fullname" .) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Anthropic 認証情報 Secret 名(セッション NS 側) */}}
|
||||
{{- define "claude-code.anthropicSecretName" -}}
|
||||
{{- if .Values.anthropic.existingSecret -}}
|
||||
{{- .Values.anthropic.existingSecret -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-anthropic" (include "claude-code.fullname" .) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Git アイデンティティ Secret 名(アイデンティティ名を与える) */}}
|
||||
{{- define "claude-code.gitIdentitySecretName" -}}
|
||||
{{- $ctx := .ctx -}}
|
||||
{{- $name := .name -}}
|
||||
{{- $id := index $ctx.Values.git.identities $name -}}
|
||||
{{- if and $id $id.existingSecret -}}
|
||||
{{- $id.existingSecret -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-git-%s" (include "claude-code.fullname" $ctx) $name -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* 自動生成トークン(未指定時)。リリース間で安定させるため lookup を優先。 */}}
|
||||
{{- define "claude-code.resolvedToken" -}}
|
||||
{{- if .Values.auth.token.value -}}
|
||||
{{- .Values.auth.token.value -}}
|
||||
{{- else -}}
|
||||
{{- $sec := (lookup "v1" "Secret" .Release.Namespace (printf "%s-auth" (include "claude-code.fullname" .))) -}}
|
||||
{{- if and $sec $sec.data (index $sec.data "token") -}}
|
||||
{{- index $sec.data "token" | b64dec -}}
|
||||
{{- else -}}
|
||||
{{- randAlphaNum 40 -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* 制御プレーン Pod セキュリティコンテキスト */}}
|
||||
{{- define "claude-code.podSecurityContext" -}}
|
||||
{{- toYaml .Values.podSecurityContext -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "claude-code.containerSecurityContext" -}}
|
||||
{{- toYaml .Values.containerSecurityContext -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-gateway
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
data:
|
||||
gateway.conf.template: |
|
||||
{{ .Files.Get "files/gateway.conf.template" | indent 4 }}
|
||||
entrypoint.sh: |
|
||||
{{ .Files.Get "files/gateway-entrypoint.sh" | indent 4 }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
data:
|
||||
manager.py: |
|
||||
{{ .Files.Get "files/manager.py" | indent 4 }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-session
|
||||
namespace: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
data:
|
||||
entrypoint.sh: |
|
||||
{{ .Files.Get "files/session-entrypoint.sh" | indent 4 }}
|
||||
@@ -0,0 +1,71 @@
|
||||
{{- if .Values.session.reaper.cronjob.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-reaper
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: reaper
|
||||
spec:
|
||||
schedule: {{ .Values.session.reaper.cronjob.schedule | quote }}
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 1
|
||||
failedJobsHistoryLimit: 3
|
||||
startingDeadlineSeconds: 120
|
||||
jobTemplate:
|
||||
spec:
|
||||
activeDeadlineSeconds: 300
|
||||
backoffLimit: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 12 }}
|
||||
app.kubernetes.io/component: reaper
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
serviceAccountName: {{ include "claude-code.managerServiceAccountName" . }}
|
||||
automountServiceAccountToken: true
|
||||
securityContext:
|
||||
{{- include "claude-code.podSecurityContext" . | nindent 12 }}
|
||||
containers:
|
||||
- name: reaper
|
||||
image: "{{ .Values.image.manager.registry }}/{{ .Values.image.manager.repository }}:{{ .Values.image.manager.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.manager.pullPolicy }}
|
||||
command: ["python3", "-u", "/app/manager.py", "--reap-once"]
|
||||
env:
|
||||
- name: PYTHONDONTWRITEBYTECODE
|
||||
value: "1"
|
||||
- name: LOG_LEVEL
|
||||
value: {{ .Values.manager.logLevel | quote }}
|
||||
- name: SESSIONS_NAMESPACE
|
||||
value: {{ include "claude-code.sessionsNamespace" . | quote }}
|
||||
- name: FULLNAME
|
||||
value: {{ include "claude-code.fullname" . | quote }}
|
||||
- name: IDLE_TIMEOUT_SECONDS
|
||||
value: {{ .Values.session.idleTimeoutSeconds | quote }}
|
||||
- name: MAX_LIFETIME_SECONDS
|
||||
value: {{ .Values.session.maxLifetimeSeconds | quote }}
|
||||
- name: TTYD_PORT
|
||||
value: {{ .Values.session.ttydPort | quote }}
|
||||
securityContext:
|
||||
{{- include "claude-code.containerSecurityContext" . | nindent 16 }}
|
||||
resources:
|
||||
requests: { cpu: 20m, memory: 32Mi }
|
||||
limits: { cpu: 200m, memory: 128Mi }
|
||||
volumeMounts:
|
||||
- name: manager-app
|
||||
mountPath: /app
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: manager-app
|
||||
configMap:
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
defaultMode: 0555
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 8Mi
|
||||
{{- end }}
|
||||
@@ -0,0 +1,255 @@
|
||||
{{- $sessNs := include "claude-code.sessionsNamespace" . -}}
|
||||
{{- $ccVersion := .Values.session.claudeCodeVersion | default .Chart.AppVersion -}}
|
||||
{{- /* --- git identities を manager が解釈できる JSON へ変換 --- */ -}}
|
||||
{{- $idents := dict -}}
|
||||
{{- range $name, $id := .Values.git.identities -}}
|
||||
{{- $entry := dict "type" $id.type "secret" (include "claude-code.gitIdentitySecretName" (dict "ctx" $ "name" $name)) -}}
|
||||
{{- if eq $id.type "ssh" -}}
|
||||
{{- $_ := set $entry "keyPrivateKey" ($id.existingSecretKeyPrivateKey | default "id") -}}
|
||||
{{- $_ := set $entry "keyKnownHosts" ($id.existingSecretKeyKnownHosts | default "known_hosts") -}}
|
||||
{{- else if eq $id.type "https-token" -}}
|
||||
{{- $_ := set $entry "keyToken" ($id.existingSecretKeyToken | default "token") -}}
|
||||
{{- $_ := set $entry "username" ($id.username | default "x-access-token") -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $idents $name $entry -}}
|
||||
{{- end -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
spec:
|
||||
replicas: {{ .Values.gateway.replicaCount }}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
annotations:
|
||||
checksum/manager: {{ .Files.Get "files/manager.py" | sha256sum }}
|
||||
checksum/gateway: {{ print (.Files.Get "files/gateway.conf.template") (.Files.Get "files/gateway-entrypoint.sh") | sha256sum }}
|
||||
checksum/session: {{ .Files.Get "files/session-entrypoint.sh" | sha256sum }}
|
||||
{{- with .Values.commonAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "claude-code.managerServiceAccountName" . }}
|
||||
automountServiceAccountToken: true
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "claude-code.podSecurityContext" . | nindent 8 }}
|
||||
containers:
|
||||
# ---- gateway: 受付・認証・WebSocket プロキシ ----
|
||||
- name: gateway
|
||||
image: "{{ .Values.image.gateway.registry }}/{{ .Values.image.gateway.repository }}:{{ .Values.image.gateway.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.gateway.pullPolicy }}
|
||||
command: ["/bin/sh", "/etc/gateway/entrypoint.sh"]
|
||||
env:
|
||||
- name: GATEWAY_PORT
|
||||
value: {{ .Values.gateway.port | quote }}
|
||||
- name: MANAGER_PORT
|
||||
value: "8081"
|
||||
- name: TTYD_PORT
|
||||
value: {{ .Values.session.ttydPort | quote }}
|
||||
- name: SESSIONS_NS
|
||||
value: {{ $sessNs | quote }}
|
||||
- name: AUTH_MODE
|
||||
value: {{ .Values.auth.mode | quote }}
|
||||
{{- if eq .Values.auth.mode "forward" }}
|
||||
- name: AUTH_FORWARD_URL
|
||||
value: {{ required "auth.mode=forward には auth.forward.url が必須です" .Values.auth.forward.url | quote }}
|
||||
{{- end }}
|
||||
- name: NGINX_ERROR_LEVEL
|
||||
value: {{ eq .Values.manager.logLevel "debug" | ternary "info" "warn" | quote }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.gateway.port }}
|
||||
securityContext:
|
||||
{{- include "claude-code.containerSecurityContext" . | nindent 12 }}
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: http }
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: http }
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
{{- toYaml .Values.gateway.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: gateway-config
|
||||
mountPath: /etc/gateway
|
||||
readOnly: true
|
||||
- name: nginx-tmp
|
||||
mountPath: /tmp/nginx
|
||||
{{- if eq .Values.auth.mode "basic" }}
|
||||
- name: gateway-secret
|
||||
mountPath: /etc/gateway-secret
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
# ---- manager: ライフサイクル API / UI / 回収ループ ----
|
||||
- name: manager
|
||||
image: "{{ .Values.image.manager.registry }}/{{ .Values.image.manager.repository }}:{{ .Values.image.manager.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.manager.pullPolicy }}
|
||||
command: ["python3", "-u", "/app/manager.py"]
|
||||
env:
|
||||
- name: LISTEN_PORT
|
||||
value: "8081"
|
||||
- name: PYTHONDONTWRITEBYTECODE
|
||||
value: "1"
|
||||
- name: LOG_LEVEL
|
||||
value: {{ .Values.manager.logLevel | quote }}
|
||||
- name: FULLNAME
|
||||
value: {{ include "claude-code.fullname" . | quote }}
|
||||
- name: SESSIONS_NAMESPACE
|
||||
value: {{ $sessNs | quote }}
|
||||
- name: SESSION_SERVICE_ACCOUNT
|
||||
value: {{ include "claude-code.sessionServiceAccountName" . | quote }}
|
||||
- name: SESSION_IMAGE
|
||||
value: "{{ .Values.image.session.registry }}/{{ .Values.image.session.repository }}:{{ .Values.image.session.tag }}"
|
||||
- name: SESSION_IMAGE_PULL_POLICY
|
||||
value: {{ .Values.image.session.pullPolicy | quote }}
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
- name: SESSION_IMAGE_PULL_SECRETS_JSON
|
||||
value: {{ toJson . | quote }}
|
||||
{{- end }}
|
||||
- name: SESSION_BOOTSTRAP_CONFIGMAP
|
||||
value: {{ include "claude-code.fullname" . }}-session
|
||||
- name: ANTHROPIC_SECRET
|
||||
value: {{ include "claude-code.anthropicSecretName" . | quote }}
|
||||
- name: GIT_IDENTITIES_JSON
|
||||
value: {{ $idents | toJson | quote }}
|
||||
- name: GIT_ALLOWED_HOSTS
|
||||
value: {{ join "," .Values.git.allowedHosts | quote }}
|
||||
- name: GIT_ALLOW_ARBITRARY
|
||||
value: {{ .Values.git.allowArbitraryRepos | quote }}
|
||||
- name: GIT_DEFAULT_AUTHOR_NAME
|
||||
value: {{ .Values.git.defaultAuthorName | quote }}
|
||||
- name: GIT_DEFAULT_AUTHOR_EMAIL
|
||||
value: {{ .Values.git.defaultAuthorEmail | quote }}
|
||||
- name: IDLE_TIMEOUT_SECONDS
|
||||
value: {{ .Values.session.idleTimeoutSeconds | quote }}
|
||||
- name: MAX_LIFETIME_SECONDS
|
||||
value: {{ .Values.session.maxLifetimeSeconds | quote }}
|
||||
- name: REAP_INTERVAL_SECONDS
|
||||
value: {{ .Values.session.reaper.intervalSeconds | quote }}
|
||||
- name: MAX_SESSIONS
|
||||
value: {{ .Values.session.maxSessions | quote }}
|
||||
- name: TTYD_PORT
|
||||
value: {{ .Values.session.ttydPort | quote }}
|
||||
- name: SESSION_RESOURCES_JSON
|
||||
value: {{ toJson .Values.session.resources | quote }}
|
||||
- name: WORKSPACE_DIR
|
||||
value: {{ .Values.session.workspaceDir | quote }}
|
||||
- name: AUTOSTART_CLAUDE
|
||||
value: {{ .Values.session.autoStartClaude | quote }}
|
||||
- name: VERIFY_COMMAND
|
||||
value: {{ .Values.session.verifyCommand | quote }}
|
||||
- name: AUTOPUSH_JSON
|
||||
value: {{ toJson .Values.session.autoPush | quote }}
|
||||
- name: EXTRA_APK_PACKAGES
|
||||
value: {{ join " " .Values.session.extraApkPackages | quote }}
|
||||
- name: CLAUDE_CODE_VERSION
|
||||
value: {{ $ccVersion | quote }}
|
||||
{{- with .Values.anthropic.extraEnv }}
|
||||
- name: ANTHROPIC_EXTRA_ENV_JSON
|
||||
value: {{ toJson . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.session.podLabels }}
|
||||
- name: SESSION_POD_LABELS_JSON
|
||||
value: {{ toJson . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.session.podAnnotations }}
|
||||
- name: SESSION_POD_ANNOTATIONS_JSON
|
||||
value: {{ toJson . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.session.nodeSelector }}
|
||||
- name: SESSION_NODE_SELECTOR_JSON
|
||||
value: {{ toJson . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.session.tolerations }}
|
||||
- name: SESSION_TOLERATIONS_JSON
|
||||
value: {{ toJson . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.session.priorityClassName }}
|
||||
- name: SESSION_PRIORITY_CLASS
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
- name: AUTH_MODE
|
||||
value: {{ .Values.auth.mode | quote }}
|
||||
{{- if eq .Values.auth.mode "token" }}
|
||||
- name: AUTH_TOKEN_FILE
|
||||
value: /etc/manager-secret/token
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: manager
|
||||
containerPort: 8081
|
||||
securityContext:
|
||||
{{- include "claude-code.containerSecurityContext" . | nindent 12 }}
|
||||
readinessProbe:
|
||||
httpGet: { path: /healthz, port: manager }
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: manager }
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
{{- toYaml .Values.manager.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: manager-app
|
||||
mountPath: /app
|
||||
readOnly: true
|
||||
- name: manager-tmp
|
||||
mountPath: /tmp
|
||||
{{- if eq .Values.auth.mode "token" }}
|
||||
- name: manager-secret
|
||||
mountPath: /etc/manager-secret
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: gateway-config
|
||||
configMap:
|
||||
name: {{ include "claude-code.fullname" . }}-gateway
|
||||
defaultMode: 0555
|
||||
- name: nginx-tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 16Mi
|
||||
- name: manager-app
|
||||
configMap:
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
defaultMode: 0555
|
||||
- name: manager-tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 16Mi
|
||||
{{- if eq .Values.auth.mode "token" }}
|
||||
- name: manager-secret
|
||||
secret:
|
||||
secretName: {{ include "claude-code.authSecretName" . }}
|
||||
items:
|
||||
- key: {{ .Values.auth.token.existingSecretKey | default "token" }}
|
||||
path: token
|
||||
{{- end }}
|
||||
{{- if eq .Values.auth.mode "basic" }}
|
||||
- name: gateway-secret
|
||||
secret:
|
||||
secretName: {{ include "claude-code.authSecretName" . }}
|
||||
items:
|
||||
- key: {{ .Values.auth.basic.existingSecretKey | default "htpasswd" }}
|
||||
path: htpasswd
|
||||
{{- end }}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $fullName := include "claude-code.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- /* WebSocket を長時間維持できるよう read/send タイムアウトを既定で延長(明示指定があれば優先) */}}
|
||||
{{- $defaults := dict
|
||||
"nginx.ingress.kubernetes.io/proxy-read-timeout" "3600"
|
||||
"nginx.ingress.kubernetes.io/proxy-send-timeout" "3600" }}
|
||||
{{- toYaml (merge (deepCopy .Values.ingress.annotations) $defaults) | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType | default "Prefix" }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.sessionsNamespace.limitRange.enabled }}
|
||||
{{- $lr := .Values.sessionsNamespace.limitRange -}}
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-sessions
|
||||
namespace: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
spec:
|
||||
limits:
|
||||
- type: Container
|
||||
max:
|
||||
{{- toYaml $lr.max | nindent 8 }}
|
||||
default:
|
||||
{{- toYaml $lr.default | nindent 8 }}
|
||||
defaultRequest:
|
||||
{{- toYaml $lr.defaultRequest | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if and .Values.sessionsNamespace.create (ne (include "claude-code.sessionsNamespace" .) .Release.Namespace) }}
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
{{- with .Values.sessionsNamespace.podSecurityLevel }}
|
||||
pod-security.kubernetes.io/enforce: {{ . }}
|
||||
pod-security.kubernetes.io/enforce-version: latest
|
||||
pod-security.kubernetes.io/audit: {{ . }}
|
||||
pod-security.kubernetes.io/warn: {{ . }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
"helm.sh/resource-policy": keep
|
||||
{{- with .Values.commonAnnotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,51 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# ブラウザ / Ingress コントローラーからの受付ポート
|
||||
- ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.gateway.port }}
|
||||
egress:
|
||||
# DNS
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
{{- toYaml .Values.networkPolicy.dnsNamespaceSelector | nindent 14 }}
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# kube-apiserver(セッション Pod/Service の CRUD)
|
||||
- ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
- protocol: TCP
|
||||
port: 6443
|
||||
# セッション Pod の ttyd へのリバースプロキシ
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ include "claude-code.sessionsNamespace" . }}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: claude-code
|
||||
app.kubernetes.io/component: session
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.session.ttydPort }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,75 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
{{- $np := .Values.networkPolicy -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-session
|
||||
namespace: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: claude-code
|
||||
app.kubernetes.io/component: session
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# 制御プレーン(gateway)からの ttyd 接続のみ許可
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ .Release.Namespace }}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 14 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.session.ttydPort }}
|
||||
egress:
|
||||
# DNS
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
{{- toYaml $np.dnsNamespaceSelector | nindent 14 }}
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
{{- if $np.allowInternetHttps }}
|
||||
# HTTPS(git over HTTPS + api.anthropic.com)。内部レンジは except で遮断。
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
{{- with $np.blockedEgressCIDRs }}
|
||||
except:
|
||||
{{- toYaml . | nindent 14 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
{{- end }}
|
||||
{{- if $np.allowSshGit }}
|
||||
# SSH git(port 22)
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
{{- with $np.blockedEgressCIDRs }}
|
||||
except:
|
||||
{{- toYaml . | nindent 14 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 22
|
||||
{{- end }}
|
||||
{{- range $np.sessionEgressExtra }}
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: {{ .cidr }}
|
||||
ports:
|
||||
{{- toYaml .ports | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
spec:
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
{{- end }}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{- $sessNs := include "claude-code.sessionsNamespace" . -}}
|
||||
---
|
||||
# 制御プレーンがセッション NS 内で必要とする最小権限。
|
||||
# ClusterRole は使わず、セッション NS に閉じた Role のみ。
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
namespace: {{ $sessNs }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch", "create", "delete", "deletecollection"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log", "pods/status"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
namespace: {{ $sessNs }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ include "claude-code.fullname" . }}-manager
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "claude-code.managerServiceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -0,0 +1,22 @@
|
||||
{{- if .Values.sessionsNamespace.resourceQuota.enabled }}
|
||||
{{- $rq := .Values.sessionsNamespace.resourceQuota -}}
|
||||
{{- $max := int .Values.session.maxSessions -}}
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-sessions
|
||||
namespace: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
spec:
|
||||
hard:
|
||||
{{- if $rq.hard }}
|
||||
{{- /* 明示指定された上限をそのまま使う */}}
|
||||
{{- toYaml $rq.hard | nindent 4 }}
|
||||
{{- else }}
|
||||
{{- /* 既定は同時セッション数の抑制のみ。compute 集計上限は hard で明示指定する。 */}}
|
||||
pods: {{ add $max 2 | quote }}
|
||||
count/services: {{ add $max 2 | quote }}
|
||||
count/pods: {{ add $max 2 | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- if not .Values.anthropic.existingSecret }}
|
||||
# Claude Code 認証情報。セッション NS に作成し、セッション Pod へ env として注入する。
|
||||
# 値が空のままインストールするとセッション内の claude が認証に失敗する(NOTES 参照)。
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-anthropic
|
||||
namespace: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- if eq .Values.anthropic.authMode "oauth" }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: {{ .Values.anthropic.oauthToken | quote }}
|
||||
{{- else }}
|
||||
ANTHROPIC_API_KEY: {{ .Values.anthropic.apiKey | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{- if eq .Values.auth.mode "token" }}
|
||||
{{- if not .Values.auth.token.existingSecret }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-auth
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/resource-policy": keep
|
||||
type: Opaque
|
||||
stringData:
|
||||
token: {{ include "claude-code.resolvedToken" . | quote }}
|
||||
{{- end }}
|
||||
{{- else if eq .Values.auth.mode "basic" }}
|
||||
{{- if not .Values.auth.basic.existingSecret }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}-auth
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
htpasswd: |
|
||||
{{- range .Values.auth.basic.users }}
|
||||
{{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{- $ctx := . -}}
|
||||
{{- $sessNs := include "claude-code.sessionsNamespace" . -}}
|
||||
{{- range $name, $id := .Values.git.identities }}
|
||||
{{- if not $id.existingSecret }}
|
||||
---
|
||||
# Git アイデンティティ "{{ $name }}"(type: {{ $id.type }})
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" $ctx }}-git-{{ $name }}
|
||||
namespace: {{ $sessNs }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" $ctx | nindent 4 }}
|
||||
claude-code.cafepieters.com/git-identity: {{ $name | quote }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- if eq $id.type "ssh" }}
|
||||
{{- if not $id.sshPrivateKey }}{{ fail (printf "git.identities.%s: type=ssh には sshPrivateKey か existingSecret が必要です" $name) }}{{- end }}
|
||||
{{- if not $id.knownHosts }}{{ fail (printf "git.identities.%s: type=ssh には knownHosts が必須です(ホスト鍵の TOFU を無効化するため)" $name) }}{{- end }}
|
||||
id: {{ $id.sshPrivateKey | quote }}
|
||||
known_hosts: {{ $id.knownHosts | quote }}
|
||||
{{- else if eq $id.type "https-token" }}
|
||||
{{- if not $id.token }}{{ fail (printf "git.identities.%s: type=https-token には token か existingSecret が必要です" $name) }}{{- end }}
|
||||
username: {{ $id.username | default "x-access-token" | quote }}
|
||||
token: {{ $id.token | quote }}
|
||||
{{- else }}
|
||||
{{ fail (printf "git.identities.%s: type は ssh か https-token を指定してください" $name) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "claude-code.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "claude-code.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# 制御プレーン(manager / reaper)用 ServiceAccount。
|
||||
# セッション NS 内で Pod / Service を作成・削除する権限のみを持つ(rbac.yaml)。
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "claude-code.managerServiceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
automountServiceAccountToken: true
|
||||
---
|
||||
# セッション Pod 用 ServiceAccount。
|
||||
# いかなる RBAC も紐づけず、トークンも配布しない(API サーバーへ到達不能)。
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "claude-code.sessionServiceAccountName" . }}
|
||||
namespace: {{ include "claude-code.sessionsNamespace" . }}
|
||||
labels:
|
||||
{{- include "claude-code.labels" . | nindent 4 }}
|
||||
automountServiceAccountToken: false
|
||||
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "claude-code Helm chart values",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"nameOverride": { "type": "string" },
|
||||
"fullnameOverride": { "type": "string" },
|
||||
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["gateway", "manager", "session"],
|
||||
"properties": {
|
||||
"gateway": { "$ref": "#/definitions/image" },
|
||||
"manager": { "$ref": "#/definitions/image" },
|
||||
"session": { "$ref": "#/definitions/image" }
|
||||
}
|
||||
},
|
||||
"imagePullSecrets": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" } } }
|
||||
},
|
||||
|
||||
"gateway": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicaCount": { "type": "integer", "minimum": 1 },
|
||||
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||
"resources": { "type": "object" }
|
||||
}
|
||||
},
|
||||
|
||||
"manager": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"logLevel": { "type": "string", "enum": ["debug", "info", "warning", "error"] },
|
||||
"resources": { "type": "object" }
|
||||
}
|
||||
},
|
||||
|
||||
"sessionsNamespace": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"create": { "type": "boolean" },
|
||||
"podSecurityLevel": { "type": "string", "enum": ["privileged", "baseline", "restricted"] },
|
||||
"resourceQuota": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"hard": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"limitRange": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"max": { "type": "object" },
|
||||
"default": { "type": "object" },
|
||||
"defaultRequest": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode"],
|
||||
"properties": {
|
||||
"mode": { "type": "string", "enum": ["token", "basic", "forward", "none"] },
|
||||
"token": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": { "type": "string" },
|
||||
"existingSecret": { "type": "string" },
|
||||
"existingSecretKey": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"basic": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"users": { "type": "array", "items": { "type": "string" } },
|
||||
"existingSecret": { "type": "string" },
|
||||
"existingSecretKey": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"forward": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": { "type": "string" },
|
||||
"signInUrl": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"anthropic": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["authMode"],
|
||||
"properties": {
|
||||
"authMode": { "type": "string", "enum": ["apiKey", "oauth"] },
|
||||
"apiKey": { "type": "string" },
|
||||
"oauthToken": { "type": "string" },
|
||||
"existingSecret": { "type": "string" },
|
||||
"existingSecretKeyApiKey": { "type": "string" },
|
||||
"existingSecretKeyOauthToken": { "type": "string" },
|
||||
"extraEnv": { "type": "object" }
|
||||
}
|
||||
},
|
||||
|
||||
"git": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["allowedHosts"],
|
||||
"properties": {
|
||||
"allowedHosts": { "type": "array", "items": { "type": "string" } },
|
||||
"allowArbitraryRepos": { "type": "boolean" },
|
||||
"defaultAuthorName": { "type": "string" },
|
||||
"defaultAuthorEmail": { "type": "string" },
|
||||
"identities": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["ssh", "https-token"] },
|
||||
"sshPrivateKey": { "type": "string" },
|
||||
"knownHosts": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"token": { "type": "string" },
|
||||
"existingSecret": { "type": "string" },
|
||||
"existingSecretKeyPrivateKey": { "type": "string" },
|
||||
"existingSecretKeyKnownHosts": { "type": "string" },
|
||||
"existingSecretKeyToken": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"session": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"autoStartClaude": { "type": "boolean" },
|
||||
"workspaceDir": { "type": "string" },
|
||||
"ttydPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
||||
"extraApkPackages": { "type": "array", "items": { "type": "string" } },
|
||||
"claudeCodeVersion": { "type": "string" },
|
||||
"resources": { "type": "object" },
|
||||
"idleTimeoutSeconds": { "type": "integer", "minimum": 60 },
|
||||
"maxLifetimeSeconds": { "type": "integer", "minimum": 300 },
|
||||
"reaper": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"intervalSeconds": { "type": "integer", "minimum": 10 },
|
||||
"cronjob": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"schedule": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"maxSessions": { "type": "integer", "minimum": 1 },
|
||||
"verifyCommand": { "type": "string" },
|
||||
"autoPush": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"branchPrefix": { "type": "string" },
|
||||
"protectedBranches": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"podLabels": { "type": "object" },
|
||||
"podAnnotations": { "type": "object" },
|
||||
"nodeSelector": { "type": "object" },
|
||||
"tolerations": { "type": "array" },
|
||||
"priorityClassName": { "type": "string" }
|
||||
}
|
||||
},
|
||||
|
||||
"networkPolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"dnsNamespaceSelector": { "type": "object" },
|
||||
"allowInternetHttps": { "type": "boolean" },
|
||||
"allowSshGit": { "type": "boolean" },
|
||||
"blockedEgressCIDRs": { "type": "array", "items": { "type": "string" } },
|
||||
"sessionEgressExtra": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["cidr", "ports"],
|
||||
"properties": {
|
||||
"cidr": { "type": "string" },
|
||||
"ports": { "type": "array" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"className": { "type": "string" },
|
||||
"annotations": { "type": "object" },
|
||||
"hosts": { "type": "array" },
|
||||
"tls": { "type": "array" }
|
||||
}
|
||||
},
|
||||
|
||||
"service": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
|
||||
"port": { "type": "integer", "minimum": 1, "maximum": 65535 }
|
||||
}
|
||||
},
|
||||
|
||||
"podSecurityContext": { "type": "object" },
|
||||
"containerSecurityContext": { "type": "object" },
|
||||
"commonLabels": { "type": "object" },
|
||||
"commonAnnotations": { "type": "object" },
|
||||
"podDisruptionBudget": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"minAvailable": { "type": ["integer", "string"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["registry", "repository", "tag"],
|
||||
"properties": {
|
||||
"registry": { "type": "string" },
|
||||
"repository": { "type": "string" },
|
||||
"tag": { "type": "string" },
|
||||
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
# ==========================================================================
|
||||
# claude-code — Claude Code リモート実行 Helm チャート
|
||||
# --------------------------------------------------------------------------
|
||||
# 構成:
|
||||
# gateway ... 受付 nginx。ブラウザ ↔ セッションの単一入口。認証・WebSocket
|
||||
# プロキシ・アクティビティ計測を担う。
|
||||
# manager ... 制御プレーン(Python 標準ライブラリのみ)。セッションの
|
||||
# 作成/一覧/破棄 API、Web UI、アイドル回収ループ。
|
||||
# session ... セッションごとに作られる使い捨て Pod。Alpine ベースで
|
||||
# git clone → 編集 → 検証 → push を行い、claude を ttyd 経由で
|
||||
# ブラウザに提供する。アイドルで自動破棄。
|
||||
#
|
||||
# セキュリティ上の注意(必読):
|
||||
# - この values.yaml に平文の API キー / SSH 秘密鍵 / トークンを書かない。
|
||||
# Git にコミットしない。実値は CI の --set / --set-file か existingSecret で注入する。
|
||||
# - セッション Pod は API サーバーへアクセスできない(SA トークン非マウント)。
|
||||
# - セッション Pod の egress は Git ホストと api.anthropic.com のみに制限される
|
||||
# (networkPolicy.enabled: true のとき)。
|
||||
# - 詳細は README.md「セキュリティモデル」を参照。
|
||||
# ==========================================================================
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# image: すべて Alpine ベース / linux/arm64 対応イメージ
|
||||
# --------------------------------------------------------------------------
|
||||
image:
|
||||
# 受付 nginx(非 root 版)
|
||||
gateway:
|
||||
registry: docker.io
|
||||
repository: nginxinc/nginx-unprivileged
|
||||
tag: "1.29.1-alpine"
|
||||
pullPolicy: IfNotPresent
|
||||
# 制御プレーン(Python 標準ライブラリのみ・追加 pip 不要)
|
||||
manager:
|
||||
registry: docker.io
|
||||
repository: library/python
|
||||
tag: "3.13-alpine"
|
||||
pullPolicy: IfNotPresent
|
||||
# セッションの使い捨てコンテナ。
|
||||
# 既定は CI(session-image.yaml)がビルドする事前ビルドイメージ
|
||||
# (git + openssh + ttyd + node + @anthropic-ai/claude-code 同梱、非 root 実行)。
|
||||
# restricted PodSecurity / readOnlyRootFilesystem 下で確実に動くのはこの方式。
|
||||
#
|
||||
# 事前ビルドを使わず素の node:alpine 等を指定することも可能だが、その場合
|
||||
# ブート時に $HOME へ claude を npm インストールし、ttyd が同梱されていないと
|
||||
# 起動に失敗する(apk は非 root では不可)。フォールバック例:
|
||||
# registry: docker.io / repository: library/node / tag: "22-alpine"
|
||||
session:
|
||||
registry: git.cafepieters.com
|
||||
repository: helmchart/claude-code/session
|
||||
tag: "2.1.259"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# gateway: 受付・ルーティング
|
||||
# --------------------------------------------------------------------------
|
||||
gateway:
|
||||
replicaCount: 1
|
||||
# gateway コンテナの待受ポート(Service / Ingress のバックエンド)
|
||||
port: 8080
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# manager: 制御プレーン
|
||||
# --------------------------------------------------------------------------
|
||||
manager:
|
||||
# debug | info | warning | error
|
||||
logLevel: info
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# sessionsNamespace: セッション Pod を隔離するネームスペース
|
||||
# --------------------------------------------------------------------------
|
||||
sessionsNamespace:
|
||||
# 空 = リリース先ネームスペースを使う。セキュリティ上、専用ネームスペースを推奨。
|
||||
name: "claude-code-sessions"
|
||||
# true ならこのチャートがネームスペースを作成し PodSecurity ラベルを付与する。
|
||||
# 既存ネームスペースを使う場合は false にし、下記ラベルを手動で付与すること。
|
||||
create: true
|
||||
# PodSecurity admission レベル(restricted 準拠のセッション Pod を強制)
|
||||
podSecurityLevel: restricted
|
||||
|
||||
# セッション NS 全体の使用量上限(暴走・過剰生成のブラスト半径を限定)
|
||||
resourceQuota:
|
||||
enabled: true
|
||||
# 省略値は session.maxSessions と session.resources から算出される。
|
||||
# 明示指定するとそのまま ResourceQuota.spec.hard になる。
|
||||
hard: {}
|
||||
# requests.cpu: "4"
|
||||
# requests.memory: 8Gi
|
||||
# limits.cpu: "16"
|
||||
# limits.memory: 32Gi
|
||||
|
||||
# セッション NS のコンテナ既定値 / 上限
|
||||
limitRange:
|
||||
enabled: true
|
||||
max:
|
||||
cpu: "4"
|
||||
memory: 4Gi
|
||||
default:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
defaultRequest:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# auth: ブラウザからのアクセス認証
|
||||
# --------------------------------------------------------------------------
|
||||
auth:
|
||||
# token : Bearer トークン / Cookie / ?token= で検証(既定)
|
||||
# basic : HTTP Basic 認証(htpasswd)
|
||||
# forward : 外部認証プロキシ(oauth2-proxy 等)へ auth_request を委譲
|
||||
# none : 認証なし(信頼済みネットワーク限定・非推奨)
|
||||
mode: token
|
||||
|
||||
token:
|
||||
# 空なら install 時に自動生成される(helm get manifest で参照可能なため、
|
||||
# 運用では existingSecret を使うのが望ましい)。
|
||||
value: ""
|
||||
existingSecret: ""
|
||||
existingSecretKey: "token"
|
||||
|
||||
basic:
|
||||
# `htpasswd -nbB user pass` の出力を1行以上。existingSecret 優先。
|
||||
users: []
|
||||
existingSecret: ""
|
||||
existingSecretKey: "htpasswd"
|
||||
|
||||
forward:
|
||||
# nginx auth_request の転送先(例: http://oauth2-proxy.auth.svc.cluster.local:4180/oauth2/auth)
|
||||
url: ""
|
||||
# 未認証時のリダイレクト先(任意)
|
||||
signInUrl: ""
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# anthropic: Claude Code の認証情報
|
||||
# --------------------------------------------------------------------------
|
||||
anthropic:
|
||||
# apiKey : ANTHROPIC_API_KEY を使う
|
||||
# oauth : CLAUDE_CODE_OAUTH_TOKEN を使う(`claude setup-token` で発行)
|
||||
authMode: apiKey
|
||||
apiKey: ""
|
||||
oauthToken: ""
|
||||
# 上記の代わりに既存 Secret を参照
|
||||
existingSecret: ""
|
||||
existingSecretKeyApiKey: "ANTHROPIC_API_KEY"
|
||||
existingSecretKeyOauthToken: "CLAUDE_CODE_OAUTH_TOKEN"
|
||||
# 全セッションへ渡す追加環境変数(例: ANTHROPIC_BASE_URL / ANTHROPIC_MODEL)
|
||||
extraEnv: {}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# git: 接続先ホストと認証アイデンティティ
|
||||
# --------------------------------------------------------------------------
|
||||
git:
|
||||
# チェックアウトを許可するホスト allow-list。manager はこれ以外のホストを拒否する。
|
||||
allowedHosts:
|
||||
- github.com
|
||||
- gitlab.com
|
||||
- 192.168.9.65
|
||||
- git.cafepieters.com
|
||||
# allow-list を無効化し任意 URL を許可する(非推奨)
|
||||
allowArbitraryRepos: false
|
||||
|
||||
# コミット時の既定 author(セッション作成時に上書き可)
|
||||
defaultAuthorName: "Claude"
|
||||
defaultAuthorEmail: "claude@cafepieters.com"
|
||||
|
||||
# 認証アイデンティティ。セッション作成時に名前で選択する。
|
||||
# GitHub / Gitea / GitLab など SSH または HTTPS トークンで認証する任意のホストに対応。
|
||||
identities: {}
|
||||
# gitea-ssh:
|
||||
# type: ssh # ssh | https-token
|
||||
# # --- ssh のとき ---
|
||||
# sshPrivateKey: "" # PEM 形式。existingSecret 優先
|
||||
# knownHosts: "" # `ssh-keyscan <host>` の出力(必須/ホスト鍵TOFUを無効化)
|
||||
# # --- 既存 Secret を使う場合 ---
|
||||
# existingSecret: ""
|
||||
# existingSecretKeyPrivateKey: "id"
|
||||
# existingSecretKeyKnownHosts: "known_hosts"
|
||||
# github-https:
|
||||
# type: https-token
|
||||
# username: "x-access-token" # GitHub は任意文字列、Gitea はユーザー名
|
||||
# token: "" # PAT。existingSecret 優先
|
||||
# existingSecret: ""
|
||||
# existingSecretKeyToken: "token"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# session: 使い捨てセッションの挙動とライフサイクル
|
||||
# --------------------------------------------------------------------------
|
||||
session:
|
||||
# ブート完了後に `claude` を自動起動する(false ならログインシェルで待受)
|
||||
autoStartClaude: true
|
||||
# リポジトリを clone する作業ディレクトリ
|
||||
workspaceDir: /workspace
|
||||
# ttyd の待受ポート(Pod 内からのみ到達可能)
|
||||
ttydPort: 7681
|
||||
# ブート時に追加インストールする apk パッケージ(言語ランタイム等)
|
||||
extraApkPackages: []
|
||||
# インストールする @anthropic-ai/claude-code のバージョン(空 = チャート appVersion)
|
||||
claudeCodeVersion: ""
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
ephemeral-storage: 4Gi
|
||||
|
||||
# ---- lifecycle: 破棄条件 ----
|
||||
# 無通信がこの秒数継続したセッションを破棄する(Pod 内ウォッチドッグ + manager 回収)
|
||||
idleTimeoutSeconds: 1800
|
||||
# 生成からの最大寿命(Pod の activeDeadlineSeconds)。到達で強制終了。
|
||||
maxLifetimeSeconds: 43200
|
||||
|
||||
reaper:
|
||||
# manager 内アイドル回収ループの実行間隔
|
||||
intervalSeconds: 60
|
||||
# 予備の孤児掃除 CronJob(manager 障害時の保険)
|
||||
cronjob:
|
||||
enabled: true
|
||||
schedule: "*/15 * * * *"
|
||||
|
||||
# 同時に存在できるセッション数の上限(manager が enforce)
|
||||
maxSessions: 10
|
||||
|
||||
# ---- verify / push ----
|
||||
# セッション内の `claude-verify` が実行するコマンド(例: "npm test" / "make check")
|
||||
verifyCommand: ""
|
||||
autoPush:
|
||||
# true でもデフォルトブランチや protectedBranches への push、--force は禁止。
|
||||
enabled: false
|
||||
branchPrefix: "claude/"
|
||||
protectedBranches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- release
|
||||
|
||||
# セッション Pod への追加メタデータ / スケジューリング
|
||||
podLabels: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
# セッション Pod の優先度クラス(任意)
|
||||
priorityClassName: ""
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# networkPolicy: 通信制限(CNI が NetworkPolicy 対応であること)
|
||||
# --------------------------------------------------------------------------
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
# DNS 解決を許可する kube-dns の namespace セレクタ
|
||||
dnsNamespaceSelector:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
# api.anthropic.com / GitHub は IP 固定できないため、既定でセッションの
|
||||
# 443/tcp(HTTPS git・Claude API)と 22/tcp(SSH git)の外向きを許可する。
|
||||
# 完全に閉じる場合は egress プロキシを用意し false にする。
|
||||
allowInternetHttps: true
|
||||
allowSshGit: true
|
||||
# allowInternetHttps 時に egress から除外する CIDR(SSRF 対策)。
|
||||
# クラウドのメタデータ IP は既定で遮断。自クラスターの Pod/Service CIDR を
|
||||
# 追加すると、セッションから内部サービスへの到達を防げる。
|
||||
blockedEgressCIDRs:
|
||||
- 169.254.169.254/32
|
||||
# 追加で許可する egress(社内ミラー / プロキシ用)
|
||||
sessionEgressExtra: []
|
||||
# - cidr: 10.0.0.0/8
|
||||
# ports:
|
||||
# - protocol: TCP
|
||||
# port: 443
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# ingress / service
|
||||
# --------------------------------------------------------------------------
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
# nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
hosts:
|
||||
- host: claude-code.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: claude-code-tls
|
||||
# hosts:
|
||||
# - claude-code.example.com
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 制御プレーンの Pod / コンテナセキュリティ
|
||||
# --------------------------------------------------------------------------
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
containerSecurityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
# 全リソースへ付与する追加ラベル / アノテーション
|
||||
commonLabels: {}
|
||||
commonAnnotations: {}
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
minAvailable: 1
|
||||
Reference in New Issue
Block a user