# claude-code **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:` --- ## 目次 - [アーキテクチャ](#アーキテクチャ) - [セッションのライフサイクル](#セッションのライフサイクル) - [セキュリティモデル](#セキュリティモデル) - [Claude 認証(API 課金なし / サブスクリプション)](#claude-認証api-課金なし--サブスクリプション) - [管理画面へのアクセス(LoadBalancer / Ingress)](#管理画面へのアクセスloadbalancer--ingress) - [インストール](#インストール) - [使い方](#使い方) - [values リファレンス](#values-リファレンス) - [image](#image) - [gateway / manager](#gateway--manager) - [sessionsNamespace](#sessionsnamespace) - [auth — アクセス認証](#auth--アクセス認証) - [anthropic — Claude Code 認証情報](#anthropic--claude-code-認証情報) - [git — 接続先と認証アイデンティティ](#git--接続先と認証アイデンティティ) - [session — 使い捨てセッション](#session--使い捨てセッション) - [networkPolicy](#networkpolicy) - [service / ingress](#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//` をセッション 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//` 経由でブラウザ内ターミナルに接続。`claude-verify` / `claude-push` が使える 4. **破棄** — 次のいずれかで Pod ごと消滅(Service も回収) - **アイドル**: `ttyd` への接続が `session.idleTimeoutSeconds` 秒間ゼロ → Pod 内ウォッチドッグが自壊 - **アイドル(保険)**: manager の回収ループが `last-activity` アノテーションを見て削除 - **最大寿命**: `session.maxLifetimeSeconds`(Pod の `activeDeadlineSeconds`)到達で強制終了 - **手動**: UI の「破棄」/ `DELETE /api/sessions/` - **孤児掃除**: 予備 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` を使ってください。 ## Claude 認証(API 課金なし / サブスクリプション) このチャートは **Claude の API(従量課金)を使わずに**、ローカルの Claude Code と同じ **サブスクリプション認証(Pro / Max プラン)** で動かせます。セッション内の `claude` の 挙動・機能はローカル実行と同一です(同じ CLI・同じログイン方式)。 `anthropic.authMode` は 3 つ: | モード | 注入するもの | トークン更新 | 課金 | |---|---|---|---| | **`credentials`(既定・推奨)** | `~/.claude/.credentials.json` 全体(`refreshToken` 込み) | セッション内 `claude` が自己更新 **+** クラスタ内 refresher CronJob が Secret を自動更新 | サブスクリプション枠(API 課金なし) | | `oauth` | `CLAUDE_CODE_OAUTH_TOKEN`(`claude setup-token` の単発トークン) | 自動更新なし(`refreshToken` を含まない)。失効したら手動再投入 | サブスクリプション枠 | | `apiKey` | `ANTHROPIC_API_KEY`(`sk-ant-...`) | — | API 従量課金 | ### `credentials` モードの使い方(推奨) 1. **claude にログイン済みの手元 PC** の認証情報ファイルをそのまま渡します (このファイルには `accessToken` と、それを更新するための `refreshToken` が入っています): ```bash # 場所: Linux/macOS = ~/.claude/.credentials.json # Windows = %USERPROFILE%\.claude\.credentials.json # (macOS でキーチェーン保存の場合は Keychain の "Claude Code" 項目を書き出す) helm upgrade --install claude-code cafepieters/claude-code -n claude-code --create-namespace \ -f examples/loadbalancer-oauth.values.yaml \ --set-file anthropic.credentialsJson="$HOME/.claude/.credentials.json" ``` 2. これだけで **トークンが定期的に変わっても再デプロイは不要** です。仕組み: - セッション Pod は生成のたびに Secret を読むため、常に最新の認証情報で起動します。 - **セッション内の `claude`** は、`accessToken` が失効していれば `refreshToken` で 自動更新します(そのセッションの間だけ有効。`$HOME/.claude/.credentials.json` に書き戻す)。 - **refresher CronJob**(`anthropic.refresher`、既定 30 分ごと)が、Secret 内の `refreshToken` を使ってクラスタ内で `accessToken` を更新し、**Secret 自体を書き戻します**。 これにより手元 PC がオフラインでも、新規セッションは常に有効なトークンで起動できます。 3. 既存 Secret を使う場合: ```bash kubectl -n claude-code-sessions create secret generic claude-creds \ --from-file=.credentials.json="$HOME/.claude/.credentials.json" # values: anthropic.existingSecret=claude-creds # (refresher に patch させるため RBAC はこの Secret 名に限定して付与される) ``` ### 注意 / 制約 - 利用枠はローカルの Claude Code と共有です。同時セッションを増やすとプランの レート上限に早く達します。 - `session.maxLifetimeSeconds`(既定 12h)が `accessToken` の寿命より長いと、長時間 セッションが自分でトークンを更新し、`refreshToken` のローテーションで refresher 側の 値が一時的に古くなることがあります。refresher は短い周期(既定 30 分)で追従します。 - **refresher は Claude Code CLI の OAuth クライアント値(`anthropic.refresher.tokenUrl` / `clientId`)に依存します。これらは非公式で、仕様変更により動かなくなる可能性があります。** その場合でもセッション内 `claude` の自己更新は独立して機能します(新規セッションの 起動時トークンが古い場合のみ影響)。不要なら `anthropic.refresher.enabled: false`。 - refresher の権限は **セッション NS の認証情報 Secret 1 個への `get` / `patch` のみ** (`resourceNames` で限定した Role)。 - `refreshToken` 自体が失効・失効させられた場合は、手元 PC で `claude` に再ログインして `--set-file anthropic.credentialsJson=...` で再投入します。 - Secret の中身は refresher が書き換えます。`helm upgrade` で `--set-file` を付けない限り Helm は Secret の値を上書きしません(`helm.sh/resource-policy: keep` + 3-way merge)。 ## 管理画面へのアクセス(LoadBalancer / Ingress) 管理画面(gateway)への到達経路は 2 つあり、**併用できます**。既定は LoadBalancer です。 ### A. LoadBalancer でローカルネットワークの IP を使う(既定・Ingress 不要) `service.type: LoadBalancer`(既定)にすると、クラスタの LB 実装 (**kube-vip** / MetalLB / Cilium LB-IPAM など)がローカルネットワークの IP を払い出します。 その IP をブラウザで開くだけでアクセスできます。ドメインも Ingress も TLS 終端も不要です。 ```yaml ingress: enabled: false # Ingress は張らない service: type: LoadBalancer port: 80 annotations: kube-vip.io/loadbalancerIPs: "192.168.9.50" # 払い出す VIP(空きアドレス) kube-vip.io/vipHost: "cpi11" loadBalancerSourceRanges: # 接続元をローカルセグメントに限定(第一の防御線) - 192.168.9.0/24 externalTrafficPolicy: Local # 実クライアント IP を保持 ``` ```bash kubectl -n claude-code get svc claude-code # EXTERNAL-IP を確認 # → http:/// をブラウザで開く ``` | キー | 説明 | |---|---| | `service.type` | `LoadBalancer`(既定)/ `ClusterIP` / `NodePort` | | `service.port` | 待受ポート(既定 `80`) | | `service.annotations` | LB 実装へのヒント(kube-vip の VIP 指定、MetalLB のプール指定など) | | `service.loadBalancerIP` | 払い出す IP を直接指定(annotations 指定を推奨) | | `service.loadBalancerSourceRanges` | 接続を許可する CIDR。**Ingress を張らない構成では必ず設定して接続元を絞る** | | `service.loadBalancerClass` | 複数 LB 実装がある場合の指定 | | `service.externalTrafficPolicy` | `Local` で実クライアント IP を保持 | | `service.nodePort` | `type: NodePort` 時の固定ポート | > **セキュリティ** — LoadBalancer は L3/L4 でネットワークに口を開けます。`loadBalancerSourceRanges` > で接続元を絞り、`auth.mode` は `token`(既定)以上にしてください。VIP セグメントは > ルーターの DHCP 範囲と重複させないこと。 ### B. Ingress でドメインアクセス(従来どおり維持) `ingress.enabled: true` で Ingress を作成します。A と同時に有効化しても構いません。 WebSocket 用に `proxy-read-timeout` / `proxy-send-timeout` を既定 `3600` に設定します (`ingress.annotations` で上書き可)。TLS は `ingress.tls` で設定します。 ```yaml ingress: enabled: true className: nginx hosts: - host: claude-code.example.com paths: [{ path: /, pathType: Prefix }] tls: - secretName: claude-code-tls hosts: [claude-code.example.com] ``` ## インストール ### 前提 - Kubernetes 1.25+(PodSecurity admission)、NetworkPolicy 対応 CNI(Calico / Cilium など) - 管理画面の公開方法に応じて **どちらか**: - LoadBalancer 実装(kube-vip / MetalLB / Cilium LB-IPAM など) … 既定 - Ingress コントローラー … `ingress.enabled: true` の場合 - どちらも無い場合は `service.type: ClusterIP` + `kubectl port-forward` で利用可 - Claude の認証情報 … `~/.claude/.credentials.json`(既定 `credentials` モード・推奨) または `claude setup-token` の OAuth トークン、`ANTHROPIC_API_KEY` ### 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" ``` ### ローカル LB の IP + サブスクリプション認証(API 不要・推奨構成) ```bash # 手元の PC は claude にログイン済みであること(~/.claude/.credentials.json が存在) helm upgrade --install claude-code cafepieters/claude-code \ -n claude-code --create-namespace \ -f examples/loadbalancer-oauth.values.yaml \ --set-file anthropic.credentialsJson="$HOME/.claude/.credentials.json" \ --set-file git.identities.gitea-ssh.sshPrivateKey=P:/Claude/.ssh/id_claude \ --set-file git.identities.gitea-ssh.knownHosts=./known_hosts.gitea ``` 以降トークンが変わっても再デプロイは不要です(refresher CronJob が Secret を追従更新)。 `examples/loadbalancer-oauth.values.yaml` は Ingress 無効・`service.type: LoadBalancer` + kube-vip の VIP 指定 + `loadBalancerSourceRanges` によるローカルセグメント限定の構成です。 ### ローカルチャートから(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. 管理画面を開く: - **LoadBalancer(既定)**: `kubectl -n claude-code get svc claude-code` の `EXTERNAL-IP` を ブラウザで開く(`http:///`) - **Ingress**: 設定したドメイン(`https://claude-code.example.com/`) - **どちらも無効**: `kubectl -n claude-code port-forward svc/claude-code 8080:80` → `http://localhost:8080/` 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/` へ 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//`(WebSocket 含む)に自動的に付与されます。 ### anthropic — Claude Code 認証情報 詳細は [Claude 認証(API 課金なし / サブスクリプション)](#claude-認証api-課金なし--サブスクリプション) を参照。 | キー | 既定 | 説明 | |---|---|---| | `anthropic.authMode` | `credentials` | `credentials`(`~/.claude/.credentials.json`・推奨)/ `oauth`(`CLAUDE_CODE_OAUTH_TOKEN`)/ `apiKey`(`ANTHROPIC_API_KEY`) | | `anthropic.credentialsJson` | `""` | `--set-file anthropic.credentialsJson=$HOME/.claude/.credentials.json` で注入 | | `anthropic.apiKey` / `anthropic.oauthToken` | `""` | `--set` / `--set-file` で注入 | | `anthropic.existingSecret` | `""` | 既存 Secret を使う(キー名は `existingSecretKeyCredentials`(既定 `.credentials.json`)/ `...ApiKey` / `...OauthToken`) | | `anthropic.refresher.enabled` | `true` | `credentials` モードで、Secret 内 `refreshToken` を使いトークンを定期更新する CronJob を作成 | | `anthropic.refresher.schedule` | `*/30 * * * *` | refresher CronJob のスケジュール | | `anthropic.refresher.refreshIfExpiresInSeconds` | `3600` | `accessToken` の残り寿命がこの秒数未満なら更新 | | `anthropic.refresher.tokenUrl` / `clientId` | Claude Code CLI の公開値 | OAuth トークンエンドポイントとクライアント ID(**非公式・仕様変更で要更新**) | | `anthropic.extraEnv` | `{}` | 全セッションへ渡す追加 env(例 `ANTHROPIC_BASE_URL`、Bedrock/Vertex 切替など) | Secret は **セッション NS** に作成されます。`credentials` モードはファイルとしてマウントし セッション起動時に `$HOME/.claude/.credentials.json` へコピー(書き込み可能)、 `apiKey` / `oauth` モードは `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.guiIdentities.enabled` | `true` | **Web UI から identity を登録・削除できるようにする**(下記) | | `git.identities..type` | — | `ssh` / `https-token`(Helm 管理・UI では読み取り専用) | | `…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 にマウントされます。 #### GUI からの identity 登録(`git.guiIdentities.enabled: true`) Web UI の **「認証情報(identity)」カード** から、SSH 秘密鍵 + `known_hosts`、または HTTPS トークンを登録・削除できます。`helm` を触らずにセッションで使う鍵を追加できます。 - 登録内容はセッション NS の Secret(ラベル `claude-code.cafepieters.com/identity-source: gui`) として保存され、`git.identities`(Helm 管理・`identity-source: helm`)と同じ一覧に並びます。 Helm 管理のものは UI から削除できません。 - **セキュリティ**: 秘密鍵はブラウザ → gateway → manager をリクエストボディで通ります。 **経路の TLS(Ingress の `tls` / LB の TLS 終端)を必ず有効に**してください。 有効化すると manager にセッション NS の `secrets` への `get`/`list`/`create`/`update`/`delete` 権限が付きます(同 NS には Claude 認証情報 Secret も存在)。不要なら `enabled: false`。 - `known_hosts` は登録時必須です(`ssh-keyscan ` の出力を貼り付け。ホスト鍵 TOFU 無効化)。 **URL の形式**(`git.identities` で選んだ鍵で認証されます): | 形式 | 例 | identity | |---|---|---| | SSH(SCP 記法) | `git@192.168.9.65:websites/e-stay-tours.git` | `ssh` 型 | | SSH(URL 記法) | `ssh://git@192.168.9.65/websites/e-stay-tours.git` | `ssh` 型 | | HTTPS | `https://git.cafepieters.com/websites/e-stay-tours.git` | `https-token` 型(プライベート時) | > SSH のユーザー名は **常に `git`** です(Gitea・GitHub 共通)。自分の Gitea アカウント名 > (`claude@…` など)ではありません。認証は identity の鍵で行われるため、その鍵を持つ > アカウントが対象リポジトリの参照権限を持っている必要があります。 ### 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 → [管理画面へのアクセス(LoadBalancer / Ingress)](#管理画面へのアクセスloadbalancer--ingress) を参照。 `service.type` の既定は `LoadBalancer`、`ingress.enabled` の既定は `false`(両者は併用可)。 ## セッションイメージ 既定の `image.session` は CI(`session-image.yaml`)がビルドする **事前ビルドイメージ** `git.cafepieters.com/helmchart/claude-code/session:` です。 `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 get pods -l app.kubernetes.io/component=session` - **セッションのログ**: `kubectl -n logs claude-sess-` - **強制破棄**: UI から、または `kubectl -n delete pod,svc claude-sess-` - **回収の確認**: manager のログに `reaping session (idle|max-lifetime|phase=...)` - **トークンのローテーション**: `auth.token.existingSecret` の値を更新し、Deployment を再起動 - **アップグレード**: `helm upgrade`。`files/*` の変更は checksum アノテーションで自動ローリング ## トラブルシューティング | 症状 | 原因と対処 | |---|---| | セッションを「開く」と 502 / 一覧で `Failed` になる | セッション Pod のブートが失敗している。UI の **「ログ」ボタン**(または `kubectl logs -n claude-sess-`)で死因を確認。終了 Pod は `session.reaper.terminatedRetentionSeconds`(既定 900s)残る | | clone が ~60〜130 秒後にタイムアウトして `Failed` | セッション Pod から Git ホストへの egress が届いていない。多くは **NetworkPolicy(Cilium 等)** が `ipBlock` egress を通していない。`--set networkPolicy.enabled=false` で切り分け。恒久対応は `networkPolicy.sessionEgressExtra` に対象 CIDR を追加、または Cilium 環境なら egress ルールの調整 | | clone が `could not read Username` / `Authentication failed` | プライベートリポジトリに認証情報が無い。HTTPS URL には `https-token` identity(Gitea/GitHub の PAT)を、SSH URL(`git@host:...` / `ssh://git@host/...`)には `ssh` identity を選ぶ。**URL のスキームと identity の型を一致させる** | | clone が `repository ... not found` / `does not appear to be a git repository` | パス誤り、またはその identity のアカウントに対象リポジトリの参照権限が無い | | UI が 502(`/s/...` のログが gateway に出ない) | 前段(LB / Ingress)が実体の無いセッションに返している。まず一覧で該当セッションが `Running` か、上記でブート失敗していないか確認 | | セッション作成が `許可されていない Git ホスト` | `git.allowedHosts` に対象ホストを追加(IP と FQDN は別物) | | clone が `Host key verification failed` | identity の `knownHosts` が対象ホストの鍵と不一致。`ssh-keyscan ` で取り直す | | 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` → Gitea リリース → チャート publish。 - `nginx` / `python` の Alpine タグのみ更新の場合は `values.yaml` を更新してコミットするだけ (**リリースなし**)。 - **手動リリース(臨時)** — バージョン末尾にアルファベットを付与(`2.1.259` → `2.1.259-a`)。 対応する `CHANGELOG-.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.289 | 2.1.289 | 自動更新 | | 2.1.283 | 2.1.283 | 自動更新 | | 2.1.278 | 2.1.278 | 自動更新 | | 2.1.270 | 2.1.270 | 自動更新 | | 2.1.263-f | 2.1.263 | **Web UI から git identity(SSH 秘密鍵 + known_hosts / HTTPS トークン)を登録・削除**できる「認証情報」カードを追加(`git.guiIdentities.enabled`、既定 `true`)。`GET/POST/DELETE /api/identities`。GUI 登録分はセッション NS の Secret(`identity-source: gui`)として保存し、Helm 管理分と同じ一覧に統合。manager RBAC に同 NS の `secrets` CRUD を条件付き付与 | | 2.1.263-e | 2.1.263 | リポジトリ URL バリデーションを緩和(`ssh://user@…` / `user@host:path` / `host:path` も受理。ホスト allowlist は従来どおり適用)。エラーメッセージに受理形式と「SSH ユーザーは常に `git`」の注意を明記。UI の入力欄にヒントを追加 | | 2.1.263-d | 2.1.263 | 失敗したセッションの調査性を改善。`session.reaper.terminatedRetentionSeconds`(既定 900)で終了 Pod を即削除せず保持。UI に **「ログ」ボタン**(`GET /api/sessions//log`)を追加し、`Failed` の理由(exitCode / reason)を一覧に表示。セッションブートスクリプトが URL スキームと identity 型の不一致を警告し、`git clone` の出力をそのままログに出すよう変更 | | 2.1.263-c | 2.1.263 | 修正: gateway の nginx が起動時 `pcre2_compile() failed: missing closing parenthesis` でクラッシュループ。`location ~` の正規表現に `{10}` が含まれ、nginx がブロック区切りの `{}` と解釈していた。正規表現全体をダブルクォートで囲んで解消 | | 2.1.263-b | 2.1.263 | `anthropic.authMode: credentials`(既定)を追加。`~/.claude/.credentials.json` 全体を注入し、セッション内 `claude` の自己更新に加え、クラスタ内 refresher CronJob(`refreshToken` で Secret を定期更新)でトークン失効に自動追従。→ トークンが変わっても再デプロイ不要。refresher の RBAC は認証情報 Secret 1 個の get/patch に限定 | | 2.1.263-a | 2.1.263 | Service を `LoadBalancer` 対応(kube-vip 等でローカル LB の IP から管理画面へアクセス、`loadBalancerSourceRanges` で接続元制限、既定を `LoadBalancer` に変更)。Ingress は従来どおり併用可。API を使わないサブスクリプション認証(`claude setup-token` → `authMode: oauth`)の手順を README に追記 | | 2.1.263 | 2.1.263 | 自動更新 | | 2.1.259 | 2.1.259 | 初版。gateway + manager + 使い捨て session Pod + reaper、GitHub/Gitea 対応、アイドル自動破棄、NetworkPolicy / RBAC 最小化 / restricted PodSecurity |