diff --git a/.github/actions/pulumi-setup/action.yml b/.github/actions/pulumi-setup/action.yml index ce57ba8..2797516 100644 --- a/.github/actions/pulumi-setup/action.yml +++ b/.github/actions/pulumi-setup/action.yml @@ -1,4 +1,4 @@ -# pulumi.yml の preview と up に共通の準備。checkout は呼ぶ側でする(このファイル自体が checkout の中にある)。 +# pulumi.yml の preview と up に共通の準備。checkout は呼ぶ側でする。 name: pulumi setup description: bun と Pulumi の CLI を入れ、SDK を作って bun install と型の確認まで済ませる runs: @@ -7,12 +7,10 @@ runs: - uses: oven-sh/setup-bun@v2 with: bun-version-file: pulumi/.bun-version - # CLI だけを入れる (command なし) - uses: pulumi/actions@v7 with: pulumi-version-file: pulumi/.pulumi-version - # SDK (Pulumi.yaml の packages) を作って bun install まで済ませる。lock がずれたら落とす (--frozen-lockfile の代わり)。 - # Renovate の枝では postUpgradeTasks が同じ pulumi install で lock を作るので、ずれない (pulumi/README.md) + # lock がずれたら落とす(--frozen-lockfile の代わり) - shell: bash working-directory: pulumi run: | diff --git a/.github/workflows/cilium-drift.yml b/.github/workflows/cilium-drift.yml index 69270a1..ee50aea 100644 --- a/.github/workflows/cilium-drift.yml +++ b/.github/workflows/cilium-drift.yml @@ -1,13 +1,6 @@ -# **cilium の値が git とクラスタでズレていないかを見る。読むだけ。** -# -# cilium は Argo CD の管理外(CNI 本体なので、それが動いていないと Argo CD も動かない)。 -# machine config の inlineManifest に載っていて、bootstrap/cilium/values.yaml を直しても -# **人が render.sh → apply-config → upgrade-k8s を流すまでクラスタには届かない。** -# `upgrade-k8s` は流せば差分を出すが、流し忘れたことは教えてくれない。逆に、 -# クラスタ側だけ手でいじった設定も git には出てこない(`node-port-range` で実際に起きた)。 -# -# **当てるところまでは自動化しない**(talos/README.md「適用は CI にやらせない」)。 -# **代わりにズレを見つけるところまでを自動でやる。** 権限は cilium-config の `get` だけ。 +# cilium の値が git とクラスタでズレていないかを見る(読むだけ。権限は cilium-config の get だけ)。 +# cilium は inlineManifest なので、人が render.sh → apply-config → upgrade-k8s を流すまで届かない。 +# 当てるのは自動化しない(talos/README.md)。 name: cilium drift on: @@ -16,7 +9,7 @@ on: paths: - bootstrap/cilium/** - .github/workflows/cilium-drift.yml - # クラスタ側だけを手でいじったときは push が無いので、定期的にも見る。 + # クラスタ側だけ手でいじったときのため schedule: - cron: "0 3 * * 1" workflow_dispatch: @@ -61,8 +54,7 @@ jobs: --certificate-authority=bootstrap/apiserver/ca.crt \ --token="$T" "$@" } - # **`--kube-version` を渡すこと。** チャートは Capabilities を見て分岐するので、 - # 渡さないと helm の既定版で描かれて偽の差分が出る。 + # --kube-version が無いと helm の既定版で描かれて偽の差分が出る KV=$(k version -o json | jq -r .serverVersion.gitVersion) echo "kube-version=$KV" export HOWTO="talos/README.md「inlineManifests を直したとき」の render.sh → apply-config → upgrade-k8s を流すこと" @@ -75,12 +67,10 @@ jobs: k -n kube-system get cm cilium-config -o json | jq '.data' > live.json - # YAML はここまでで JSON にしてある。python は標準ライブラリだけで済ませる - # (ランナーに PyYAML が入っている保証は無い)。 + # PyYAML は無いかもしれないので JSON で渡す python3 - <<'PY' import os, sys, json - # 値が空文字のキーは両側から落とす。Talos の inlineManifest で当てると live に残らない - # (2026-10-17 から debug-verbose・nodeport-addresses・policy-cidr-match-mode が「git にしか無い」で毎回落ちていた) + # 値が空文字のキーは inlineManifest で当てると live に残らないので両側から落とす r = {k: v for k, v in json.load(open("rendered.json")).items() if v != ""} l = {k: v for k, v in json.load(open("live.json")).items() if v != ""} only_git = sorted(set(r) - set(l)) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 981c793..0b944f8 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -2,24 +2,13 @@ name: Claude Code Review on: pull_request: - # **`labeled` が要る。** bot の PR は `opened` の時点ではスキップされるので、 - # あとから `needs-review` を貼っても再実行されなかった(2026-09-07 に #85 で発覚)。 - # 下の `if` がラベルの有無を見ているので、無関係なラベルで起動しても素通りする。 + # `labeled` は、あとから needs-review を貼った bot の PR を拾うため types: [opened, synchronize, reopened, ready_for_review, labeled] jobs: claude-review: - # Draft PR はスキップ。fork からの PR も対象外 (secrets が渡らないため) — - # そちらは claude-code-review-fork.yml がラベル起動で処理する。 - # - # bot の PR も原則スキップするが、**`needs-review` ラベルが付いていれば見る**。 - # 自分でマージされる PR をレビューしても誰も読まないので飛ばす。人を待つ PR は - # レビューする価値がある、という切り分け。Renovate は自動マージしないまとまり - # (メジャーと chart) にだけこのラベルを付ける (5ym/repo-config)。 - # - # 実例: #7 に postgres 17 → 18 が入っていた。PVC の PG_VERSION は 17 なので - # そのまま上げると起動を拒否する。1 行の差分でも、依存そのものに内在するリスクは - # レビューで拾える。 + # Draft と fork(secrets が渡らない)はスキップ。bot の PR は needs-review が付いたもの + # (Renovate が自動マージしないまとまり)だけ見る。 if: >- github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository && @@ -43,13 +32,8 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} track_progress: true - # **Renovate の PR をレビューするのに要る。** action 自身が bot 起動を拒否し、 - # `Workflow initiated by non-human actor` で落ちる(2026-09-07 に #85 で発覚)。 - # 上の `if` で「bot は needs-review が付いているときだけ」に絞ってあるので、 - # ここは `*` ではなく renovate だけを名指しする。`*` は公開リポジトリでは - # 外部の App がプロンプトを持ち込める(action のドキュメントの警告)。 - # Renovate は 2026-10 から自前の GitHub App(`doa-renovate`)で動いている。名前が変わると - # ここで黙って落ちる(#206 で発覚)ので、両方を名指ししておく + # 無いと action が bot 起動を拒否する。`*` にしない(外部の App がプロンプトを持ち込める)。 + # Renovate の App 名が変わるとここで黙って落ちる allowed_bots: "renovate[bot],doa-renovate[bot]" prompt: | REPO: ${{ github.repository }} @@ -81,14 +65,7 @@ jobs: 突き合わせて**影響を言うこと (例: PVC の PG_VERSION と major の不一致) - リリースノートが本当に見つからなければ、探した先を書いたうえで「要確認」とすること - # WebFetch と gh release はリリースノートを読むため。**読み取りだけ**に絞る。 - # 取ってくる中身は第三者が書いたもので、レビュー本文への指示が混ざりうる - # (プロンプトインジェクション)。このワークフローはコメントしかできず、 - # マージも push もできないので影響は限定される。 - # - # **注釈をこのブロックの中に書かないこと。** `claude_args: |` は YAML の - # ブロックスカラーなので `#` はコメントにならず、そのまま CLI の引数として - # 渡って --allowedTools の解釈が壊れる(2026-09-07 に踏んだ。レビューが - # 「WebFetch の権限が無い」と言うのに設定上は許可されている、という形で出た)。 + # WebFetch と gh release はリリースノート用で、読み取りだけに絞る。 + # 注釈をこのブロックの中に書かない(`#` がそのまま CLI の引数になり --allowedTools が壊れる) claude_args: | --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh release view:*),Bash(gh release list:*),WebFetch" diff --git a/.github/workflows/pulumi.yml b/.github/workflows/pulumi.yml index 39a9eea..e24319d 100644 --- a/.github/workflows/pulumi.yml +++ b/.github/workflows/pulumi.yml @@ -1,37 +1,7 @@ -# **Cloudflare・Entra・NetBird の設定を Pulumi で当てる。** 中身は pulumi/。 -# -# PR → preview。差分を PR にコメントする(読み取りの鍵だけ) -# main → up。**マージした時点で当たる**(書き込みの鍵は Environment `pulumi-apply` に置き、 -# main からしか使えない) -# 毎週月曜 → preview --refresh --expect-no-changes。Cloudflare 側を直接いじったら落ちる。 -# Pulumi で表せない残り(カスタムドメイン・容量)も pulumi/report.ts で見る -# 鍵の期限も見る(pulumi/expiry.sh。30 日を切ったら Matrix の部屋 server に投稿) -# -# 書き込みの鍵は R2 の設定を書き換えられる(= バックアップのバケットを消せる)。 -# 守りは 2 つ: Environment で main に限ること、doany-restic に `protect` が付いていること -# (外すと Pulumi は消すのを拒否しなくなる。pulumi/index.ts)。 -# -# 秘密(リポジトリの Actions secrets): -# PULUMI_BACKEND_URL state の置き場(R2 の doany-pulumi。endpoint にアカウント ID が入るので secrets) -# PULUMI_CONFIG_PASSPHRASE state の暗号化。state に秘密は入れていないので、失くしたら作り直せる(pulumi/README.md) -# CLOUDFLARE_ACCOUNT_ID -# PREVIEW_CLOUDFLARE_API_TOKEN / PREVIEW_AWS_ACCESS_KEY_ID / PREVIEW_AWS_SECRET_ACCESS_KEY -# R2 の設定の読み取り + doany-pulumi の読み書き(state のロックに要る) -# PREVIEW_NB_PAT NetBird の読み取り (サービスユーザー gitops-pulumi-preview、Auditor) -# MATRIX_SERVER_WEBHOOK_URL 鍵の期限の投稿先(expiry) -# Environment `pulumi-apply` の秘密: -# CLOUDFLARE_API_TOKEN / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY -# R2 の設定の書き込み + doany-pulumi の読み書き -# NB_PAT NetBird の書き込み (サービスユーザー gitops-pulumi-apply、Admin) -# AWS_* は state の置き場(R2 の S3 API)のため。値は同じ API トークンから作る(ID と値の sha256)。 -# -# Entra(pulumi/entra.ts)には**鍵を置かずに**入る。GitHub Actions の OIDC トークンを Entra の -# ワークロード ID のフェデレーションで受ける(id-token: write が要る)。ID は秘密ではないので Variables: -# AZURE_TENANT_ID リポジトリの Variables -# PREVIEW_AZURE_CLIENT_ID リポジトリの Variables。アプリ登録「gitops pulumi preview (Entra read)」 -# (Application.Read.All。PR と main から入れる) -# AZURE_CLIENT_ID Environment `pulumi-apply` の Variables。「gitops pulumi apply (Entra write)」 -# (Application.ReadWrite.OwnedBy。所有者になっている共用のアプリ登録しか書けない) +# Cloudflare・Entra・NetBird を Pulumi で当てる(pulumi/)。PR → preview、main → up、 +# 毎週 → refresh して差分があれば落とす + report.ts + 鍵の期限。 +# 書き込みの鍵はバックアップのバケットを消せるので Environment `pulumi-apply`(main だけ)に置く。 +# 秘密と Variables の一覧は pulumi/README.md。 name: pulumi on: @@ -59,11 +29,8 @@ env: PULUMI_BACKEND_URL: ${{ secrets.PULUMI_BACKEND_URL }} PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - # doany.io のゾーン(dns.ts)。アカウント ID と同じく git に置かない CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} - # Cloudflare の通知の送り先(notifications.ts)。hookshot の受け口の ID を含むので secrets に CLOUDFLARE_NOTIFY_WEBHOOK_URL: ${{ secrets.CLOUDFLARE_NOTIFY_WEBHOOK_URL }} - # cf / Pulumi のテレメトリを切る DO_NOT_TRACK: "1" PULUMI_SKIP_UPDATE_CHECK: "true" # azuread provider は GitHub Actions の OIDC トークン(ACTIONS_ID_TOKEN_REQUEST_*)を自分で拾う @@ -74,27 +41,23 @@ jobs: preview: if: github.event_name != 'push' runs-on: ubuntu-latest - # 同じ PR の古い preview は捨ててよい。up とは別の列にする ── 1 列にまとめると、GitHub は - # 待ちを 1 件しか持たないので、マージ後の up が後から来た preview に押し出されて消えうる + # up と別の列にする(待ちは 1 件だけなので、up が preview に押し出されうる) concurrency: group: pulumi-preview-${{ github.ref }} cancel-in-progress: true permissions: contents: read - # 差分を PR にコメントする pull-requests: write # Entra へ OIDC で入る id-token: write env: ARM_CLIENT_ID: ${{ vars.PREVIEW_AZURE_CLIENT_ID }} - # NetBird のサービスユーザー gitops-pulumi-preview (Auditor、読み取りだけ) NB_PAT: ${{ secrets.PREVIEW_NB_PAT }} CLOUDFLARE_API_TOKEN: ${{ secrets.PREVIEW_CLOUDFLARE_API_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.PREVIEW_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.PREVIEW_AWS_SECRET_ACCESS_KEY }} steps: - uses: actions/checkout@v7 - # bun・Pulumi の CLI・SDK・型の確認(up と同じ) - uses: ./.github/actions/pulumi-setup - uses: pulumi/actions@v7 @@ -106,7 +69,7 @@ jobs: pulumi-version-file: pulumi/.pulumi-version comment-on-pr: true edit-pr-comment: true - # 毎週の実行(と手動実行)は実物を読み直し、1 つでも差分があれば落とす(= Cloudflare 側を直接いじった) + # 週次と手動は実物を読み直し、差分があれば落とす refresh: ${{ github.event_name != 'pull_request' }} expect-no-changes: ${{ github.event_name != 'pull_request' }} @@ -119,19 +82,15 @@ jobs: if: github.event_name == 'push' runs-on: ubuntu-latest environment: pulumi-apply - # 走っている up は止めない(途中で切ると state がロックされたまま残る)。待ちが押し出されても、 - # 残るのは新しい main なので取りこぼしは無い。preview と同時に走ると片方がロックで落ちるが、 - # state は壊れない(落ちた方を流し直す) + # 走っている up は止めない(途中で切ると state がロックされたまま残る) concurrency: group: pulumi-up cancel-in-progress: false permissions: contents: read - # Entra へ OIDC で入る id-token: write env: ARM_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} - # NetBird のサービスユーザー gitops-pulumi-apply (Admin) NB_PAT: ${{ secrets.NB_PAT }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -149,10 +108,8 @@ jobs: pulumi-version-file: pulumi/.pulumi-version always-include-summary: true - # 鍵の期限(pulumi/expiry.sh)。30 日を切ったか切れていたら Matrix の部屋 server に投稿する。 - # PR では投稿せずログに出すだけ。手動実行で `report` を付けると全部を投稿する。 - # 鍵は自分の期限しか読めないので、preview 用と apply 用をそれぞれで見る。apply 用は Environment `pulumi-apply` - # (main だけ)にあるので PR では見ない。 + # 鍵の期限。PR ではログだけ。鍵は自分の期限しか読めないので preview 用と apply 用を別々に見る + # (apply 用は main だけ)。 expiry: if: github.event_name != 'push' strategy: @@ -163,14 +120,12 @@ jobs: environment: ${{ matrix.key == 'apply' && 'pulumi-apply' || '' }} permissions: contents: read - # Entra へ OIDC で入る (preview だけ) id-token: write env: KEY: ${{ matrix.key }} NB_PAT: ${{ matrix.key == 'apply' && secrets.NB_PAT || secrets.PREVIEW_NB_PAT }} CF_TOKEN: ${{ matrix.key == 'apply' && secrets.CLOUDFLARE_API_TOKEN || secrets.PREVIEW_CLOUDFLARE_API_TOKEN }} CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - # hookshot の部屋 server の受け口 (https://m.doany.io/webhook/) WEBHOOK: ${{ secrets.MATRIX_SERVER_WEBHOOK_URL }} DRY_RUN: ${{ github.event_name == 'pull_request' }} REPORT_ALL: ${{ inputs.report == true }} @@ -184,7 +139,7 @@ jobs: tenant-id: ${{ vars.AZURE_TENANT_ID }} allow-no-subscriptions: true - run: pulumi/expiry.sh - # 確かめられなかったことも知らせる (鍵が切れて 401 になったときなど。黙って落ちると気づかない) + # 確かめられなかったことも知らせる(鍵が切れて 401 など) - if: failure() && github.event_name != 'pull_request' env: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/renovate-validate.yml b/.github/workflows/renovate-validate.yml index 35bd061..ebab127 100644 --- a/.github/workflows/renovate-validate.yml +++ b/.github/workflows/renovate-validate.yml @@ -1,9 +1,5 @@ -# **renovate.json を壊さないための見張り。** -# -# Renovate は**知らないトップレベルのキーをエラーにする。** `ignorePathsDescription` -# のような「それっぽい自己文書化のキー」も通らない(2026-09-08 に実際に書いて、 -# レビューで指摘されるまで気づかなかった)。**壊れていても Renovate が黙って -# 動かなくなるだけ**で、PR も来なくなるので気づきにくい。 +# renovate.json の見張り。壊れていても Renovate は黙って止まるだけで気づきにくい +# (知らないキーは `ignorePathsDescription` のようなものもエラー)。 name: renovate validate on: @@ -27,6 +23,5 @@ jobs: - uses: actions/setup-node@v7 with: node-version: 24.22.0 - # 版は固定しない ── **Renovate 本体が受け付けるか**を見たいので、 - # そのときの最新の判定に合わせるのが正しい。 + # 版は固定しない(最新の Renovate が受け付けるかを見る) - run: npx --yes --package renovate renovate-config-validator --strict diff --git a/.github/workflows/talos-boot.yml b/.github/workflows/talos-boot.yml index b1d9c2c..eb958c2 100644 --- a/.github/workflows/talos-boot.yml +++ b/.github/workflows/talos-boot.yml @@ -1,21 +1,11 @@ -# talos/render.sh の machine config を runner の QEMU(KVM)で実際に起動し、クラスタが立つところまで見る。 -# -# 描けるか・形が合うか(needle は render.sh)に加えて、動かして初めて分かる穴も拾う -# (2026-10-17、`helm template` に `--api-versions` が無く GatewayClass `cilium` が描かれず、 -# Gateway が `Waiting for controller` のまま公開経路が全部死んだ。#401)。2026-10-17 に talos-validate をここへまとめた。 -# Renovate が Talos / Kubernetes / Cilium / gateway-api / cert-manager / argo / infisical を上げる PR で効く。 -# -# **秘密はゼロ。** 使い捨ての秘密とダミーで描く(本物は SOPS 済みで、age の鍵は CI に渡さない)。 -# **外に副作用を出さない。** ACME・DNS の書き込み・R2・非公開レジストリには触らない: -# - ArgoCD は `extraObjects`(app-of-apps と GitHub App の repo-creds)と通知先を抜く -# - ClusterIssuer `letsencrypt` は自己署名に差し替える(本物の clusterissuer.yaml は当てない) -# - infisical は SMTP の env を抜く -# 手順の元は docs/talos.md「VM ブートドリル」。 +# talos/render.sh の machine config を runner の QEMU(KVM)で起動し、クラスタが立つところまで見る。 +# 秘密はゼロ(使い捨てとダミー)。外に副作用を出さない: ArgoCD の extraObjects と通知先、 +# 本物の ClusterIssuer(自己署名に差し替え)、infisical の SMTP を抜く。手順の元は docs/talos.md。 name: talos boot on: pull_request: - # push と同じ一覧(YAML のアンカー。GitHub Actions は 2025-09 から読む。マージキー `<<:` は読まない) + # YAML のアンカーは読むが、マージキー `<<:` は読まない paths: &paths - talos/** - bootstrap/cilium/** @@ -33,7 +23,7 @@ on: permissions: contents: read -# 同じ PR で積み上がると runner を食うので、古い方を止める。main は止めない(マージごとの結果を残す) +# PR では古い方を止める。main は止めない concurrency: group: talos-boot-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} @@ -51,7 +41,7 @@ env: jobs: boot: runs-on: ubuntu-latest - # 実測で 8 分(2026-10-17)。30 分を超えたら何かが止まっている + # 実測 8 分 timeout-minutes: 30 steps: - uses: actions/checkout@v7 @@ -80,7 +70,6 @@ jobs: sudo apt-get install -y -qq --no-install-recommends qemu-system-x86 ovmf zstd >/dev/null sudo curl -fsSL -o /usr/local/bin/talosctl \ "https://github.com/siderolabs/talos/releases/download/${{ steps.v.outputs.version }}/talosctl-linux-amd64" - # runner の kubectl は古いことがある。サーバと同じ版にそろえる sudo curl -fsSL -o /usr/local/bin/kubectl \ "https://dl.k8s.io/release/${{ steps.v.outputs.k8s }}/bin/linux/amd64/kubectl" sudo chmod +x /usr/local/bin/talosctl /usr/local/bin/kubectl @@ -91,21 +80,18 @@ jobs: run: | set -euo pipefail mkdir -p /tmp/vm - # 入れ済みの raw イメージをそのままディスクにする(ISO から入れる手間と installer の取得を省く) curl -fsSL --retry 3 \ "https://factory.talos.dev/image/${{ steps.v.outputs.schematic }}/${{ steps.v.outputs.version }}/metal-amd64.raw.zst" \ | zstd -d -o /tmp/vm/disk.raw - # 疎なファイルで 140G。EPHEMERAL の maxSize 64GiB と u-local-path の minSize 50GiB が両方取れる大きさ + # EPHEMERAL の maxSize 64GiB と u-local-path の minSize 50GiB が両方取れる大きさ truncate -s 140G /tmp/vm/disk.raw cp /usr/share/OVMF/OVMF_VARS_4M.fd /tmp/vm/OVMF_VARS.fd - name: machine config を生成する run: | set -euo pipefail - # 秘密は使い捨てとダミー(上の「秘密はゼロ」) talosctl gen secrets -o /tmp/secrets.yaml - # fj.doany.io は CI からは引かない。経路だけ通す(ghcr.io にダミーを置くと - # ghcr.io/siderolabs/kubelet の取得が 403 で落ちる) + # ダミーは fj.doany.io に置く(ghcr.io に置くと kubelet の取得が 403 で落ちる) cat > /tmp/registries.yaml <<'PATCH' machine: registries: @@ -115,11 +101,10 @@ jobs: username: ci-dummy password: ci-dummy PATCH - # **argocd / infisical の値は本物の values.yaml。** 秘密の側(secret-values.sops.yaml)だけダミーに替えて重ねる。 - # 通知の送り先は secret-values.sops.yaml にしか無いので、渡さなければ通知も出ない。 - # extraObjects は app-of-apps(`apps`)と GitHub App の repo-creds。CI で本物を同期させない + # values.yaml は本物、secret-values.sops.yaml だけダミー(通知先もそこにしか無い)。 + # extraObjects は app-of-apps と repo-creds。CI で本物を同期させない echo 'extraObjects: []' > /tmp/argocd-secret-values.yaml - # SMTP は外(smtp.office365.com)に繋ぎに行くので抜く(リストなので丸ごと置き換える) + # SMTP の env を抜く(リストなので丸ごと置き換える) yq '{"infisical": {"extraEnv": [.infisical.extraEnv[] | select(.name | test("^SMTP_") | not)]}, "postgresql": {"auth": {"password": "ci-dummy"}}, "redis": {"auth": {"password": "ci-dummy"}}}' \ bootstrap/infisical/values.yaml > /tmp/infisical-secret-values.yaml @@ -138,20 +123,15 @@ jobs: ARGOCD_SECRET_VALUES=/tmp/argocd-secret-values.yaml INFISICAL_SECRET_VALUES=/tmp/infisical-secret-values.yaml \ INFISICAL_SECRET=/tmp/infisical-secret.yaml CLOUDFLARE_SECRET=/tmp/cloudflare-secret.yaml \ ./talos/render.sh /tmp/out - # 外からは hostfwd の 127.0.0.1 で入る talosctl config endpoint 127.0.0.1 talosctl config node "$NODE" - name: VM を起動する run: | set -euo pipefail - # 8G / 4 vCPU。runner は 16G / 4 vCPU(ドリルは 12G だったが、それだと runner 側が苦しい)。 - # - # 網: bond0(eno1 + eno2)は QEMU の内部ハブ越しに 1 つの SLIRP へ。SLIRP を本番の 10.0.0.0/24 と - # IPv6 のプレフィックスに合わせ、RA も出させる(accept_ra / autoconf を確かめるため)。 - # dhcpstart を .2 にするのは maintenance mode(DHCP)でも hostfwd の宛先に居させるため。 - # ipv4=on は省けない(ipv6=on だけ書くと IPv4 が無効になる)。eno4 は外に出さない別の SLIRP。 - # ディスクは virtio-scsi にする。/dev/sda に見えるので main.yaml の diskSelector を書き換えずに済む。 + # runner は 16G なので 8G(12G だと runner が苦しい)。bond0(eno1 + eno2)はハブ越しに本番と同じ網の SLIRP へ。 + # dhcpstart は maintenance mode でも hostfwd の宛先に居させるため。ipv4=on は省けない(ipv6=on だけだと IPv4 が無効)。 + # virtio-scsi は /dev/sda に見えるので diskSelector を書き換えずに済む。 qemu-system-x86_64 \ -machine q35,accel=kvm -cpu host -smp 4 -m 8192 \ -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \ @@ -177,7 +157,6 @@ jobs: N1=$(name "$MAC_ENO1"); N2=$(name "$MAC_ENO2"); N4=$(name "$MAC_ENO4") echo " eno1 -> $N1 eno2 -> $N2 eno4 -> $N4" [ -n "$N1" ] && [ -n "$N2" ] && [ -n "$N4" ] || { echo "::error::NIC の名前が引けない"; exit 1; } - # 実機と違うのは NIC の名前だけ。ほかは描いたまま当てる sed -e "s/\beno1\b/$N1/g" -e "s/\beno2\b/$N2/g" -e "s/\beno4\b/$N4/g" \ /tmp/out/controlplane.yaml > /tmp/vm/controlplane.yaml talosctl apply-config --insecure -e 127.0.0.1 -n 127.0.0.1 -f /tmp/vm/controlplane.yaml @@ -187,10 +166,8 @@ jobs: run: | set -euo pipefail until talosctl version >/dev/null 2>&1; do sleep 5; done - # etcd が上がるまでは断られる until talosctl bootstrap 2>/dev/null; do sleep 5; done talosctl kubeconfig "$KUBECONFIG" --force - # 書かれるのは https://10.0.0.2:6443(runner からは届かない) kubectl config set-cluster doany --server=https://127.0.0.1:6443 until kubectl get --raw /readyz >/dev/null 2>&1; do sleep 5; done echo " API サーバが応答した" @@ -199,7 +176,7 @@ jobs: timeout-minutes: 12 run: | set -euo pipefail - # 在るまで待ってから rollout status。inlineManifest は Talos が順に流すので最初は居ない + # inlineManifest は順に流れるので、在るまで待ってから rollout status ro() { # $1=namespace $2=kind/name until kubectl -n "$1" get "$2" >/dev/null 2>&1; do sleep 5; done kubectl -n "$1" rollout status "$2" --timeout=10m @@ -213,7 +190,6 @@ jobs: ro kube-system ds/cilium-envoy ro kube-system deploy/cilium-operator ro kube-system deploy/coredns - # Hubble の証明書はクラスタの中の Job が作る(bootstrap/cilium/values.yaml の `hubble.tls`) until kubectl -n kube-system get secret hubble-server-certs hubble-relay-client-certs >/dev/null 2>&1; do sleep 5; done ro kube-system deploy/hubble-relay ro kube-system deploy/hubble-ui @@ -224,28 +200,26 @@ jobs: ro argocd deploy/argocd-server ro argocd deploy/argocd-repo-server ro argocd sts/argocd-application-controller - # infisical は postgres に繋がるまで Ready にならない。postgres の PVC は local-path - # (u-local-path の上)なので、ここが通れば PV の経路も通っている + # postgres の PVC は local-path なので、ここで PV の経路も見ている ro infisical deploy/infisical kubectl -n infisical wait --for=jsonpath='{.status.phase}'=Bound pvc --all --timeout=1m - # 上の一覧に無いもの(chart が増やした Pod など)の取りこぼし止め + # 上の一覧に無い Pod の取りこぼし止め for i in $(seq 24); do BAD=$(kubectl get pods -A --no-headers --field-selector=status.phase!=Running,status.phase!=Succeeded) [ -z "$BAD" ] && break sleep 5 done [ -z "$BAD" ] || { echo "$BAD"; echo "::error::Running でない Pod がある"; exit 1; } - # talosctl health はノードの側で回るので runner から 10.0.0.2 に届かなくてよい(Kubernetes 側の項目は SKIP になるので上で見ている) + # Kubernetes 側の項目は SKIP になるので上で見ている talosctl health --wait-timeout 5m - name: GatewayClass と Gateway timeout-minutes: 8 run: | set -euo pipefail - # #401 の穴そのもの。無いと Gateway は誰にも拾われない until kubectl get gatewayclass cilium >/dev/null 2>&1; do sleep 5; done kubectl wait --for=condition=Accepted gatewayclass/cilium --timeout=3m - # 本物の ClusterIssuer(ACME + Cloudflare DNS-01)の代わり。同じ名前にして gateway.yaml を書き換えずに当てる + # 本物の ClusterIssuer と同じ名前の自己署名 cat > /tmp/issuer.yaml <<'YAML' apiVersion: cert-manager.io/v1 kind: ClusterIssuer @@ -254,12 +228,11 @@ jobs: spec: selfSigned: {} YAML - # webhook が受け付けるようになるまで断られる until kubectl apply -f /tmp/issuer.yaml 2>/dev/null; do sleep 5; done until kubectl apply -f bootstrap/gateway/gateway.yaml -f bootstrap/gateway/redirect-https.yaml -f bootstrap/argocd/httproute.yaml; do sleep 5; done kubectl -n kube-system wait --for=condition=Ready certificate --all --timeout=3m kubectl -n kube-system wait --for=condition=Programmed gateway/doany --timeout=3m - # 公開経路を実際に通す。Service の nodePort はランダムなまま(bootstrap/cilium/values.yaml の `nodePort`)。http は https への 301、https は argocd-server まで + # http は https への 301、https は argocd-server まで code() { curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$@" || true; } for i in $(seq 36); do H=$(code -H 'Host: ac.doany.io' http://127.0.0.1:18080/) @@ -288,18 +261,18 @@ jobs: [ "$GOT" = "$WANT" ] || fail "bond0 の MAC が $GOT(cluster.yaml は $WANT)" echo " bond0 hardwareAddr: $GOT" - # 以下は forwarding=1(Kubernetes が上げる)のあとで見ないと意味が無い(cluster.yaml の accept_ra) + # 以下は forwarding=1 のあとで見ないと意味が無い(cluster.yaml の accept_ra) F=$(talosctl read /proc/sys/net/ipv6/conf/bond0/forwarding) [ "$F" = 1 ] || fail "bond0 の forwarding が $F。前提が成り立っていない" - # グローバルな IPv6 は静的な ::2 だけ。SLAAC(autoconf=0 で止めている)が付いていたら落とす + # グローバルな IPv6 は静的な ::2 だけ(SLAAC は autoconf=0 で止めている) STATIC=$(yq -r 'select(.kind == "BondConfig") | .addresses[].address | select(test(":"))' talos/patches/cluster.yaml) GLOBAL=$(talosctl get addresses -o json \ | jq -rs '.[] | select(.spec.linkName == "bond0" and .spec.family == "inet6" and .spec.scope != "link") | .spec.address' | sort) echo " bond0 の IPv6(link-local 以外): $(echo $GLOBAL)" [ "$GLOBAL" = "$STATIC" ] || fail "bond0 のグローバル IPv6 が静的な $STATIC だけになっていない" - # RA の既定経路。SLIRP の RA は間隔が長いので少し待つ + # SLIRP の RA は間隔が長いので待つ for i in $(seq 24); do R=$(talosctl get routes -o json \ | jq -rs '[.[] | select(.spec.family == "inet6" and .spec.outLinkName == "bond0" and (.spec.dst // "") == "" and .spec.protocol == "ra")] | length') diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index e882fca..674140a 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -1,8 +1,4 @@ -# **手元のコマンドのイメージ(tools/)が作れるかの見張り。** -# -# 版は Renovate が上げて、パッチは自動でマージされる。ダウンロードの URL の形が版で変わると -# (ファイル名の付け方が変わるなど)、マージされたあと手元の tools/t.ps1 で初めて 404 に気づくことになる。 -# PR の時点でイメージを作り、全部のコマンドが動くことを見る。 +# tools/ のイメージが作れて全部のコマンドが動くかを PR で見る(版で URL の形が変わると 404 になる)。 name: tools on: diff --git a/.sops.yaml b/.sops.yaml index 865ed30..f570779 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -3,24 +3,16 @@ # tools/t.ps1 sh -c 'mkdir -p ~/.config/sops/age && age -d -o ~/.config/sops/age/keys.txt recovery/sops-age.key.age' # のあと `tools/t.ps1 sops -d`(tools/t.ps1 の中の ~ はリポジトリの .home/)。 creation_rules: - # bootstrap/*/secret-values.sops.yaml(chart の値のうち秘密だけ)は丸ごと暗号化する。 - # 秘密しか入れない約束なので選り分けない。値の残りは同じ場所の平文の values.yaml。 + # secret-values.sops.yaml は秘密しか入れないので丸ごと - path_regex: ^bootstrap/[^/]+/secret-values\.sops\.yaml$ age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - # 残りの bootstrap/*.sops.yaml は「そのキーだけ」を暗号化する。マニフェストの構造は git で読めたほうが - # レビューできるため。 - # **bootstrap/ で暗号化するファイルは `.sops.yaml` で終える。** ArgoCD の `bootstrap` はその名前で外す - # (bootstrap/argocd/repos.yaml)。違う名前で `sops -e` すると、一致する規則が無いと言われて落ちる。 + # 残りはキーだけ(構造をレビューできるように)。bootstrap/ で暗号化するファイルは `.sops.yaml` で終える + # (ArgoCD の `bootstrap` はその名前で外す)。 - path_regex: ^bootstrap/.*\.sops\.yaml$ encrypted_regex: ^(ENCRYPTION_KEY|AUTH_SECRET|CLIENT_ID_GITHUB_LOGIN|CLIENT_SECRET_GITHUB_LOGIN|CF_DNS_API_TOKEN)$ age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - # talos/secrets.yaml は丸ごと暗号化する。`talosctl gen secrets` の出力は - # 全部が秘密(Talos の CA、Kubernetes の CA、etcd の CA、bootstrap token)で、 - # 選り分ける意味が無い。この規則が無いと talos/README.md の `sops -e -i secrets.yaml` - # が「一致する creation rule が無い」で落ちる。 + # 以下は全部が秘密なので丸ごと - path_regex: ^talos/secrets\.yaml$ age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r - # talos/registries.yaml も丸ごと。中身は fj.doany.io の pull トークンだけなので - # 選り分ける意味が無い。 - path_regex: ^talos/registries\.yaml$ age: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r diff --git a/bootstrap/argocd/httproute.yaml b/bootstrap/argocd/httproute.yaml index d5ebf37..f6e1472 100644 --- a/bootstrap/argocd/httproute.yaml +++ b/bootstrap/argocd/httproute.yaml @@ -1,5 +1,3 @@ -# 公開経路。証明書は Gateway([../gateway/](../gateway/))のリスナーに付いているので指定しない。 -# 当てるのは ArgoCD の Application `bootstrap`(../README.md「公開経路(HTTPRoute)もここにある」)。 apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: diff --git a/bootstrap/argocd/oidc-secret.yaml b/bootstrap/argocd/oidc-secret.yaml index 7eb94e2..f3b29b5 100644 --- a/bootstrap/argocd/oidc-secret.yaml +++ b/bootstrap/argocd/oidc-secret.yaml @@ -1,10 +1,5 @@ -# The Entra client secret ArgoCD's OIDC login uses, as a Secret ArgoCD can -# reference from argocd-cm ($argocd-oidc:client-secret in values.yaml). -# The label is what lets argocd-server read it. Only the login depends on -# this: syncing runs on the GitHub App credential that ships in -# secret-values.sops.yaml, so Infisical being down never blocks its own recovery. -# Applied by hand like the rest of this directory (the apps Application only -# syncs repos.yaml). +# ArgoCD の OIDC ログインの client secret(values.yaml の $argocd-oidc:client-secret)。ラベルで argocd-server が読む。 +# 同期は GitHub App の鍵で動くので、Infisical が落ちても復旧は止まらない。 apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalSecret metadata: diff --git a/bootstrap/argocd/repos.yaml b/bootstrap/argocd/repos.yaml index 5345a9f..f8d8880 100644 --- a/bootstrap/argocd/repos.yaml +++ b/bootstrap/argocd/repos.yaml @@ -1,12 +1,6 @@ -# **Forgejo 側は別の ApplicationSet(下の `repos-forgejo`)に分けてある(2026-10-05)。** -# ApplicationSet は generator が 1 つでも失敗すると**全体の Application を 1 本も作らず** -# 戻る(argo-cd の applicationset_controller.go: `GenerateApplications` がエラーを返したら -# 生成も更新もせずに requeue)。まっさらなクラスタでは Forgejo はまだ空で、読むための -# `forgejo-repo-creds` も無い(それを当てるのは **このリポジトリの Application** ── -# apps/forgejo/argocd-creds.yaml)。同じ ApplicationSet に同居させていると、gitea 側が落ちて -# このリポジトリ自身の Application も作られず、Forgejo もデプロイされないので**自力で抜けられない**。 -# 分けておけば GitHub 側が先に全部立ち、Forgejo が戻った時点で下が追いつく。 -# 2 つの template は同じ形を保つこと(片方だけ直すとアプリの扱いが Git の置き場で変わる)。 +# Forgejo 側は別の ApplicationSet(下の `repos-forgejo`)に分ける。generator が 1 つでも失敗すると +# Application を 1 本も作らないので、まっさらなクラスタ(Forgejo も forgejo-repo-creds もまだ無い)で +# 同居させると自力で抜けられない。2 つの template は同じ形を保つこと。 apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: @@ -15,33 +9,18 @@ metadata: spec: goTemplate: true goTemplateOptions: - # Every key referenced below has to exist in the repo's argocd.yaml. - # A missing or misspelled one fails the generator loudly instead of - # rendering an Application with an empty name or path. + # argocd.yaml にキーが欠けたら、空の名前で描かずに落とす - missingkey=error generators: - # Two steps: find the repos, then let each repo describe itself. - # 1. scmProvider walks the danything org and keeps repos that carry a - # deploy/argocd.yaml. - # 2. git reads that file out of the repo, and its keys become the - # template parameters below. - # Adding an app is therefore "push manifests + deploy/argocd.yaml to a new repo". - # This file is the one piece that isn't decided per-repo, because it's the - # rule for *finding* the repos -- it can't live in the repos it finds. + # deploy/argocd.yaml を持つリポジトリを探し、そのファイルのキーを template の引数にする - matrix: generators: - scmProvider: cloneProtocol: https github: organization: danything - # Default branch only. allBranches: true would produce one - # Application per branch, which is for preview environments. allBranches: false - # The GitHub App repo-creds ArgoCD already uses to clone - # danything/*, reused for the org API scan -- so there's no - # second credential to create, no anonymous 60 req/hr ceiling - # (this scan runs every 3 minutes), and worklog-cloud doesn't - # go invisible for being private. + # clone と同じ GitHub App(匿名だと 60 req/h の上限と、private が見えない) appSecretName: creds-2776534691 filters: - pathsExist: @@ -51,14 +30,11 @@ spec: revision: '{{ .branch }}' files: - path: deploy/argocd.yaml - # generator の結果から repo が消えても(改名の途中、一時的な API エラー)、Application は消しても - # その中の実リソース(Deployment や PVC)は道連れにしない。 + # generator から repo が一時的に消えても、実リソース(PVC など)は道連れにしない syncPolicy: preserveResourcesOnDeletion: true template: metadata: - # Declared per-repo rather than derived from the repo name, so a repo can - # keep an existing Application's name and have the controller adopt it in place. name: '{{ .name }}' spec: project: default @@ -68,35 +44,24 @@ spec: path: '{{ .sourcePath }}' directory: include: '*.yaml' - # argocd.yaml は apiVersion/kind の無い設定なので外す(sourcePath が deploy のリポジトリでは同期の中に入る)。 - # bootstrap/ も外す。gitops repo は同期対象を apps/ に絞っているので通常は当たらないが、 - # sourcePath が `.` の瞬間(移行中や設定ミス)に SOPS 暗号化済みの bootstrap/*.yaml を - # そのまま apply してしまうと Secret を壊すため、二重に防ぐ。 - # `_` で始まるディレクトリも外す。用意はしたが動かしていないアプリを、置き場を変えずに - # 寝かせておくため (例: apps/_/)。動かすときは `_` を外して git mv するだけ。 - # 照らすのは sourcePath からの相対パスで、gobwas/glob を区切り無しで使う (`*` も `/` を - # 跨ぐ) ので、`**/_*/**` は頭に `/` が要り、いちばん上の `_/…` には当たらない。 - # そのために `_*/**` も並べる + # argocd.yaml は manifest でない。bootstrap/ は sourcePath が `.` になったとき SOPS 済みを当てない保険。 + # `_` で始まるディレクトリは寝かせているアプリ。`*` は `/` も跨ぐが `**/_*/**` は頭の `/` が要るので + # `_*/**` も並べる exclude: '{**/argocd.yaml,bootstrap/**,_*/**,**/_*/**}' destination: server: https://kubernetes.default.svc - # Empty for a repo whose manifests span several namespaces, which is - # fine because every manifest in these repos carries its own. namespace: '{{ .namespace }}' syncPolicy: syncOptions: - CreateNamespace=true - # CRD を入れる Application より先に同期しにいったものは、既定の 5 回で諦めて - # OutOfSync のまま止まる(refresh=hard では戻らない。ドリル 5 回目で踏んだ)。 - # backoff を伸ばして自力で戻るようにする。15s から倍々で上限 5 分、10 回で約 33 分。 + # CRD より先に同期したものは既定の 5 回で諦めて止まる(refresh=hard でも戻らない)。約 33 分まで粘る retry: limit: 10 backoff: duration: 15s factor: 2 maxDuration: 5m - # These four live in the patch rather than in template because plain - # templating only substitutes strings, and they're booleans. + # 真偽値は template では文字列になるので patch で入れる templatePatch: | spec: source: @@ -108,14 +73,9 @@ spec: prune: {{ .prune }} selfHeal: {{ .selfHeal }} --- -# bootstrap/ の平文のマニフェスト(Gateway・auth・ClusterIssuer・InfisicalSecret など)。2026-10-17 まで -# GitHub Actions(bootstrap-apply.yml)が当てていた(../../docs/decisions.md「層の分け方」)。 -# -# **machine config が持つものは外す。** Helm の値と版(`*values.yaml`・`*version.yaml`)、SOPS のファイル -# (`*.sops.yaml`。名前で外すので、足しても exclude は触らなくてよい。../../.sops.yaml が他の名前では暗号化させない)、 -# machine config が作る Namespace(argocd・cert-manager・infisical)、`apiserver/`、`cilium/`。 -# このファイル自身は上の `apps`(argocd/values.yaml の extraObjects)が持つので外す。 -# 照合は bootstrap/ からの相対パスで、`*` は `/` もまたぐ(argo-cd の util/glob は区切り文字無しの gobwas/glob)。 +# bootstrap/ の平文のマニフェスト(Gateway・auth・ClusterIssuer・InfisicalSecret など)。 +# machine config が持つもの(値・版・SOPS・Namespace・apiserver/・cilium/)と、`apps` が持つこのファイルは外す。 +# `*` は `/` もまたぐ。 apiVersion: argoproj.io/v1alpha1 kind: Application metadata: @@ -137,8 +97,7 @@ spec: automated: prune: true selfHeal: true - # InfisicalSecret の CRD は apps/infisical-operator(別の Application)が入れるので、まっさらなクラスタでは - # 最初の何回かは落ちる。15s から倍々・上限 5 分・10 回(約 33 分)で自力で戻る(apps/k8up と同じ)。 + # InfisicalSecret の CRD は別の Application が入れるので、まっさらなクラスタでは最初は落ちる retry: limit: 10 backoff: @@ -146,9 +105,7 @@ spec: factor: 2 maxDuration: 5m --- -# Forgejo(fj.doany.io)の doa を見る。上の `repos` と分けている理由は上のコメント。 -# **ここが失敗していても上の `repos` は止まらない。** 失敗中は -# `kubectl -n argocd get applicationset repos-forgejo -o yaml` の `ErrorOccurred` に理由が出る。 +# Forgejo(fj.doany.io)の doa を見る。失敗の理由は `get applicationset repos-forgejo -o yaml` の ErrorOccurred。 apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: @@ -159,17 +116,9 @@ spec: goTemplateOptions: - missingkey=error generators: - # 同じ形で Forgejo(fj.doany.io)の doa(2026-09-16 に danything から改名)も見る。 - # **改名したら必ずここを直す。** 旧名のままだと Forgejo が 307 を返し、ArgoCD の - # git クライアントはリダイレクトを追わないので、対象のアプリが丸ごと Unknown で止まる。 - # あわせて Infisical /argocd/forgejo-repo-creds の url も直すこと(repo-creds は URL の - # 接頭辞で照合するため、ずれると資格情報が付かない)。**プライベートのリポジトリは GitHub に置かず - # Forgejo にだけ置く**方針(2026-09-15。GitHub Actions の課金はプライベートにだけ掛かる)。 - # Forgejo の API は Gitea と同じなので gitea の scmProvider で読める。 - # 読むための資格情報は apps/forgejo/argocd-creds.yaml(Infisical から)。scan と clone の両方に使う。 - # **同じリポジトリが GitHub と Forgejo の両方にあると Application 名がぶつかる**(上の `repos` と - # 同じ Application を取り合い、所有者の食い違いで片方が ErrorOccurred になる)。 - # 移すときは GitHub 側を消してから(apps/forgejo/README.md)。 + # owner を改名したら必ずここと Infisical /argocd/forgejo-repo-creds の url を直す + # (Forgejo の 307 を ArgoCD は追わず、repo-creds は URL の接頭辞で照合する)。 + # 同じリポジトリが GitHub にもあると Application 名がぶつかる。移すときは GitHub 側を消してから。 - matrix: generators: - scmProvider: @@ -189,7 +138,7 @@ spec: revision: '{{ .branch }}' files: - path: deploy/argocd.yaml - # ここから下は上の `repos` と同じ(説明も上)。直すときは両方そろえる + # ここから下は上の `repos` と同じ。直すときは両方そろえる syncPolicy: preserveResourcesOnDeletion: true template: diff --git a/bootstrap/argocd/values.yaml b/bootstrap/argocd/values.yaml index 5f47a21..1656eec 100644 --- a/bootstrap/argocd/values.yaml +++ b/bootstrap/argocd/values.yaml @@ -1,38 +1,24 @@ -# argo-cd の chart の値。版は version.yaml、秘密の値(通知の webhook と GitHub App の鍵)は -# secret-values.sops.yaml(SOPS)。描いて machine config に載せるのは talos/render.sh。 -# -# CRD は chart から描かせない。3 つで 1.83 MB あり、描き出しの 95%。 -# render.sh が `KubeExternalManifestConfig` で URL を渡す -# (なぜ URL なのかは docs/decisions.md「層の分け方」)。 +# argo-cd の chart の値。秘密の値は secret-values.sops.yaml。描いて machine config に載せるのは talos/render.sh。 +# CRD は描かせない(1.83 MB で描き出しの 95%)。render.sh が URL で渡す。 crds: install: false -# chart の既定は ecr-public.aws.com/docker/library/redis。匿名の取得は共有 IP だと絞られ、 -# talos-boot で ImagePullBackOff になった(2026-10-17)。mirror.gcr.io は Docker Hub の -# pull-through キャッシュで、中身は同じ(digest を確かめた)。tag は chart の既定に合わせてある。 -# **chart を上げる PR では、この tag を新しい chart の既定と見比べる**(上書きしたので chart に付いてこない)。 -# Renovate は Docker Hub の側で版を引く(../../renovate.json の mirror.gcr.io の規則)。 +# 既定の ecr-public は匿名の取得が絞られて ImagePullBackOff になる。mirror.gcr.io は Docker Hub のキャッシュ。 +# 上書きしたので chart に付いてこない。chart を上げる PR ではこの tag を新しい chart の既定と見比べる。 redis: image: repository: mirror.gcr.io/library/redis tag: 8.10.2-alpine -# Native OIDC straight to Entra ID (same tenant/app registration as -# auth/deployment.yaml) instead of Dex. +# Dex を使わず Entra ID に直接 OIDC(auth/deployment.yaml と同じアプリ登録) dex: enabled: false configs: params: - # TLS is terminated at the Cilium Gateway (cert-manager), so argocd-server - # talks plain HTTP internally to avoid a double-TLS redirect loop. + # TLS は Gateway で終端する(二重 TLS のリダイレクトループを避ける) server.insecure: true cm: url: https://ac.doany.io - # Local admin superuser disabled now that Entra ID OIDC login works; - # the Entra "admins" group has role:admin via RBAC below (shared - # with auth/deployment.yaml's oauth2-proxy --allowed-groups). + # ローカルの admin は使わない。chart の既定 admin.enabled: true も上書きする accounts.admin.enabled: "false" - # The chart default is admin.enabled: true; without this override every - # helm upgrade would re-enable the admin login it fought argocd-server's - # field ownership over. admin.enabled: "false" oidc.config: | name: Microsoft Entra ID @@ -44,10 +30,8 @@ configs: - openid - profile - email - # Secret は ArgoCD の管理対象から外す。git に平文 Secret は置かない方針なので ArgoCD が Secret を - # 作ることは無く、一方で Infisical operator が作る Secret には CR からコピーされた tracking-id が - # 付くため、ArgoCD が「git に無い管理対象」と見なして sync 時に prune してしまう - # (2026-09-06 の復元リハーサルで踏んだ)。 + # Secret は管理対象から外す。Infisical operator の Secret は CR の tracking-id がコピーされ、 + # 外さないと sync で prune される。 resource.exclusions: | - apiGroups: [""] kinds: ["Secret"] @@ -59,32 +43,16 @@ configs: scopes: '[roles, email]' notifications: secret: - # Helm's own empty placeholder secret would fight any externally - # managed argocd-notifications-secret for ownership, so let Helm skip it. + # helm の空の Secret が外で管理する argocd-notifications-secret と取り合う create: false - # notifiers/templates/triggers/subscriptions live here (not in a - # separately-applied ConfigMap) because the chart owns and re-renders - # argocd-notifications-cm on every release sync with notifications.cm.create - # true by default; anything applied out-of-band gets wiped the next time - # the chart is re-rendered. - # - # notifiers は secret-values.sops.yaml(`service.webhook.mattermost`)。送り先は Matrix の部屋 argocd - # (スペース「通知」)の hookshot の webhook(2026-10-06 に Mattermost → Zulip → Matrix と移した)。 - # 名前は mattermost のまま ── テンプレートと subscriptions が参照している。 - # 送るのは {"text": …}(Slack 互換)。hookshot の変換のスクリプト(apps/matrix/hookshot.yaml)が読む + # 通知の設定は chart が argocd-notifications-cm を描き直すのでここに書く(別に当てると消える)。 + # notifiers は secret-values.sops.yaml。送り先は Matrix の hookshot だが、名前は mattermost のまま + # (テンプレートと subscriptions が参照している)。 templates: - # Succeeded is back: Progressing -> Healthy flips too fast to read as a - # "deploy complete" signal, so sync-result gives that its own clear ping. template.app-sync-result: "webhook:\n mattermost:\n method: POST\n body: |\n {\"text\": \"[ArgoCD] {{.app.metadata.name}}: {{.app.status.operationState.phase}} {{if eq .app.status.operationState.phase \"Succeeded\"}}✅{{else if eq .app.status.operationState.phase \"Running\"}}\U0001F680{{else}}❌{{end}} ({{now | date \"2006-01-02 15:04:05\"}})\"}\n" - # Shared by both on-health-change branches, so all six health.status values - # need an emoji here (Healthy/Progressing included). template.app-health-status: "webhook:\n mattermost:\n method: POST\n body: |\n {\"text\": \"[ArgoCD] {{.app.metadata.name}}: {{.app.status.health.status}} {{if eq .app.status.health.status \"Healthy\"}}\U0001F49A{{else if eq .app.status.health.status \"Progressing\"}}\U0001F504{{else if eq .app.status.health.status \"Degraded\"}}\U0001F494{{else if eq .app.status.health.status \"Suspended\"}}⏸️{{else if eq .app.status.health.status \"Missing\"}}❓{{else}}❔{{end}} ({{now | date \"2006-01-02 15:04:05\"}})\"}\n" triggers: - # 'Running' sits between syncs as an intermediate phase, so this condition - # still flips false -> true on every new sync even though it's one `when`. - # Running and Succeeded both get oncePer (like the Healthy health-status - # branch below) so they fire once per revision instead of on every - # reconcile, while Error/Failed still notify every time. + # oncePer で revision ごとに 1 回。Error/Failed は毎回 trigger.on-sync-result: | - when: app.status.operationState.phase == 'Running' oncePer: app.status.sync.revision @@ -94,54 +62,26 @@ notifications: - when: app.status.operationState.phase == 'Succeeded' oncePer: app.status.sync.revision send: [app-sync-result] - # Healthy dropped: on-sync-result's Succeeded already covers the - # "deploy completed fine" case, so this stays for anomalies only. + # Healthy は on-sync-result の Succeeded で足りる trigger.on-health-change: | - when: app.status.health.status == 'Progressing' oncePer: app.status.sync.revision send: [app-health-status] - when: app.status.health.status in ['Degraded', 'Suspended', 'Missing', 'Unknown'] send: [app-health-status] - # Applies to every Application automatically (current and future, eg. via - # ApplicationSet) without needing per-app annotations. subscriptions: - recipients: - mattermost triggers: - on-sync-result - on-health-change -# The seed that makes ArgoCD start managing itself. It rides along with this -# chart install precisely so there's nothing extra to kubectl apply. The -# chart renders each entry through `tpl`, so anything put here must not -# contain Go template braces -- the ApplicationSet does, which is why it -# lives in argocd/repos.yaml and is pulled from git instead. +# ArgoCD が自分を管理し始める種。chart は各要素を `tpl` に通すので、Go テンプレートを含むもの +# (ApplicationSet)は repos.yaml に置く。 extraObjects: - # The GitHub App credential that lets ArgoCD read danything/* -- both the - # private repos (this one included) and the SCM Provider generator's org - # scan in argocd/repos.yaml. It genuinely can't be fetched from - # git, since it's what makes fetching from git possible, so it ships with - # the install instead. Created through the UI originally, hence the - # generated name: keeping that name means applying this adopts the - # existing secret instead of leaving two sets of credentials for the same - # URL behind. Plaintext because this is the one credential that can't - # come from Infisical: ArgoCD syncing is how Infisical itself gets - # deployed and recovered -- this repo is private. - # - # 中身は下の githubAppRepoCreds(鍵だけ secret-values.sops.yaml にあり、helm が map として重ねる)。 - # extraObjects はリストなので、そのままでは値ファイルを重ねると丸ごと置き換わる。 - # 文字列の要素は chart が `tpl` に通すので、ここで toYaml して 1 つの要素にする。 + # danything/* を読む GitHub App の repo-creds(git から取れないのでここに置く)。名前は UI で作ったときのもの。 + # 鍵は secret-values.sops.yaml。リストは値ファイルを重ねると丸ごと置き換わるので、map を toYaml して 1 要素にする。 - '{{ toYaml .Values.githubAppRepoCreds }}' - # The anchor, and the only thing here that has to be applied by hand - # (which the ArgoCD install already is). It syncs argocd/repos.yaml out of - # this repo, that ApplicationSet discovers the app repos, and each repo's - # own argocd.yaml decides how it gets deployed -- so changing any of that - # is a push, not a kubectl command. - # - # An Application syncs a whole directory, and this directory also holds - # the values that install ArgoCD, so the include below is what keeps - # ArgoCD out of its own management. Widening it (say to '*.yaml') would - # put this file under prune/selfHeal: deleting it would then uninstall - # ArgoCD, and values couldn't be tried out with kubectl any more. + # app-of-apps。include を広げると values.yaml まで prune/selfHeal の対象になり、消すと ArgoCD が消える。 - apiVersion: argoproj.io/v1alpha1 kind: Application metadata: diff --git a/bootstrap/argocd/version.yaml b/bootstrap/argocd/version.yaml index 0c767fb..c4a90b6 100644 --- a/bootstrap/argocd/version.yaml +++ b/bootstrap/argocd/version.yaml @@ -1,3 +1,2 @@ -# argo-cd の chart の版。読むのは talos/render.sh(repo もこの行の registryUrl から読む)。 # renovate: datasource=helm depName=argo-cd registryUrl=https://argoproj.github.io/argo-helm version: 10.10.5 diff --git a/bootstrap/auth/adguard-proxy.yaml b/bootstrap/auth/adguard-proxy.yaml index 13ec0f1..3de97ac 100644 --- a/bootstrap/auth/adguard-proxy.yaml +++ b/bootstrap/auth/adguard-proxy.yaml @@ -1,14 +1,5 @@ -# ah.doany.io の管理画面を SSO で守る oauth2-proxy。 -# -# AdGuard Home には SSO が無い。認証は設定ファイルに書いた bcrypt の basic 認証だけで、 -# 利用者の管理機能も無い。上流の認証プロキシ対応の要望(AdguardTeam/AdGuardHome#1737)は -# 2020 年から open のまま。公式の答えも「前段で認証しろ」なので、Hubble と同じ形にする。 -# -# `/dns-query` はここを通さない。AdGuard は Web の口で管理画面と DoH の両方を出しているので、 -# 全部を後ろに置くと DoH にも認証がかかって名前解決が止まる(2026-09-07 朝に一度やった)。 -# HTTPRoute でパスを分けてあり、`/dns-query` は DoH サイドカーへ直接行く。 -# -# 通すのは Entra のアプリロール `admin` を持つ人だけ。 +# ah.doany.io の管理画面を SSO で守る oauth2-proxy(AdGuard Home には SSO が無い)。 +# `/dns-query` はここを通さない(DoH にも認証がかかって名前解決が止まる)。HTTPRoute でパスを分けている。 apiVersion: apps/v1 kind: Deployment metadata: @@ -57,8 +48,7 @@ spec: value: .doany.io - name: OAUTH2_PROXY_EMAIL_DOMAINS value: '*' - # a.doany.io 側と同じく Entra のアプリロール admin で絞る。 - # グループクレームから移した理由は docs/entra.md + # Entra のアプリロール admin だけ - name: OAUTH2_PROXY_OIDC_GROUPS_CLAIM value: roles - name: OAUTH2_PROXY_ALLOWED_GROUPS diff --git a/bootstrap/auth/deployment.yaml b/bootstrap/auth/deployment.yaml index b5bdd7b..4eba6cd 100644 --- a/bootstrap/auth/deployment.yaml +++ b/bootstrap/auth/deployment.yaml @@ -18,17 +18,12 @@ spec: image: quay.io/oauth2-proxy/oauth2-proxy:latest args: - --http-address=0.0.0.0:4180 - # Only trust X-Forwarded-* from in-cluster traffic (i.e. the Cilium - # Gateway's Envoy), not literally anyone per --reverse-proxy's default. + # X-Forwarded-* はクラスタ内(Gateway の Envoy)からだけ信じる - --trusted-proxy-ip=10.42.0.0/16 ports: - containerPort: 4180 env: - # The generic oidc provider tries to request a literal "groups" - # OAuth scope when OIDC_GROUPS_CLAIM is set, which Entra rejects - # (AADSTS650053) -- groups come from the ID token automatically - # once groupMembershipClaims is set on the app, no scope needed. - # The dedicated entra-id provider knows this. + # 汎用の oidc は groups スコープを要求して Entra に弾かれる(AADSTS650053) - name: OAUTH2_PROXY_PROVIDER value: entra-id - name: OAUTH2_PROXY_OIDC_ISSUER_URL @@ -47,9 +42,7 @@ spec: secretKeyRef: name: auth-secrets key: cookie-secret - # Single fixed callback registered in the Entra app -- Entra - # requires an exact redirect URI match, so this can't vary - # per-subdomain the way the forwardAuth check itself does. + # Entra は redirect URI の完全一致を求めるので固定の 1 つ - name: OAUTH2_PROXY_REDIRECT_URL value: https://a.doany.io/oauth2/callback - name: OAUTH2_PROXY_COOKIE_DOMAINS @@ -58,16 +51,7 @@ spec: value: .doany.io - name: OAUTH2_PROXY_EMAIL_DOMAINS value: "*" - # Same admin-only restriction the email whitelist gave us, but - # via the Entra app role "admin" -- anyone else in the tenant can - # still complete OIDC login but gets rejected here before reaching - # *.s.doany.io. Shared with ArgoCD's RBAC (see argocd/values.yaml). - # - # Moved off the group claim on 2026-09-07 (docs/entra.md): the - # group Object IDs made the ID token big enough that the session - # did not fit in a cookie, which is the only reason redis is here. - # --oidc-groups-claim takes a single claim name, so unlike ArgoCD - # this cannot accept both at once. + # Entra のアプリロール admin を持つ人だけ通す(docs/entra.md) - name: OAUTH2_PROXY_OIDC_GROUPS_CLAIM value: roles - name: OAUTH2_PROXY_ALLOWED_GROUPS @@ -80,19 +64,8 @@ spec: value: "true" - name: OAUTH2_PROXY_UPSTREAMS value: static://202 - # セッションは Cookie に置く。redis は要らない。 - # - # 元は「Entra の ID トークンにグループの GUID が並ぶので Cookie に - # 収まらない」という理解で redis を置いていたが、それは誤りだった - # (2026-09-07)。このテナントで所属グループは 2 つしかなく、 - # groups クレームはせいぜい 100 バイト強。4293 バイトあったセッションの - # 中身は ID トークン・アクセストークン・リフレッシュトークンそのもの。 - # - # session-cookie-minimal がその 3 つを Cookie セッションから落とす。 - # 残るのは Email と User だけなので、X-Auth-Request-* を出すには十分。 - # pass-authorization-header / set-authorization-header / - # pass-access-token / cookie-refresh とは併用できない(どれも - # 落とすトークンを必要とする)。ここではどれも使っていない。 + # セッションは Cookie に置く(redis 不要)。minimal でトークンを落として Cookie に収める。 + # pass-authorization-header / set-authorization-header / pass-access-token / cookie-refresh とは併用できない。 - name: OAUTH2_PROXY_SESSION_STORE_TYPE value: cookie - name: OAUTH2_PROXY_SESSION_COOKIE_MINIMAL diff --git a/bootstrap/auth/httproute-hubble.yaml b/bootstrap/auth/httproute-hubble.yaml index 842c4be..e84ff5d 100644 --- a/bootstrap/auth/httproute-hubble.yaml +++ b/bootstrap/auth/httproute-hubble.yaml @@ -1,8 +1,4 @@ -# 公開経路。証明書は Gateway([../gateway/](../gateway/))のリスナーに付いているので指定しない。 -# 当てるのは ArgoCD の Application `bootstrap`(../README.md「公開経路(HTTPRoute)もここにある」)。 -# -# backend は Hubble UI ではなく oauth2-proxy。UI 自身は認証を持たないので -# 直接繋いではいけない(bootstrap/auth/hubble-proxy.yaml)。 +# backend は oauth2-proxy(Hubble UI は認証を持たないので直接繋がない) apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: diff --git a/bootstrap/auth/httproute-sub.yaml b/bootstrap/auth/httproute-sub.yaml index 133f988..7614185 100644 --- a/bootstrap/auth/httproute-sub.yaml +++ b/bootstrap/auth/httproute-sub.yaml @@ -1,5 +1,3 @@ -# 公開経路。証明書は Gateway([../gateway/](../gateway/))のリスナーに付いているので指定しない。 -# 当てるのは ArgoCD の Application `bootstrap`(../README.md「公開経路(HTTPRoute)もここにある」)。 apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: diff --git a/bootstrap/auth/httproute.yaml b/bootstrap/auth/httproute.yaml index 5f8e188..da24ce0 100644 --- a/bootstrap/auth/httproute.yaml +++ b/bootstrap/auth/httproute.yaml @@ -1,5 +1,3 @@ -# 公開経路。証明書は Gateway([../gateway/](../gateway/))のリスナーに付いているので指定しない。 -# 当てるのは ArgoCD の Application `bootstrap`(../README.md「公開経路(HTTPRoute)もここにある」)。 apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: diff --git a/bootstrap/auth/hubble-proxy.yaml b/bootstrap/auth/hubble-proxy.yaml index 4ded92c..a566885 100644 --- a/bootstrap/auth/hubble-proxy.yaml +++ b/bootstrap/auth/hubble-proxy.yaml @@ -1,13 +1,5 @@ -# hl.doany.io を SSO 付きで Hubble UI へ中継する oauth2-proxy。 -# Hubble UI 自身は認証を持たないので、直接 HTTPRoute に繋いではいけない。 -# Cilium の Gateway には ext auth が無いので、*.s.doany.io と同じ「oauth2-proxy 自身を -# 前段のプロキシにする」方式(--upstream で中継先を持つ)。 -# コールバックは自分で受ける。a.doany.io 側に投げると、あちらは元のホストを -# 知らないので認証後に a.doany.io に着地してしまう(`*.s.doany.io` は名前が -# ワイルドカードで Entra に個別の redirect URI を登録できないため共有しているが、 -# こちらは固定の 1 ホストなので自前で持てる)。 -# Entra のアプリ登録に https://hl.doany.io/oauth2/callback を追加済み。 -# cookie secret は a.doany.io 側と共有しているので、どちらかで入れば両方通る。 +# hl.doany.io を SSO 付きで Hubble UI へ中継する oauth2-proxy(UI は認証を持たず、Gateway に ext auth が無い)。 +# コールバックは自分で受ける(a.doany.io に投げると元のホストに戻れない)。Entra に登録済み。 apiVersion: apps/v1 kind: Deployment metadata: @@ -56,9 +48,7 @@ spec: value: .doany.io - name: OAUTH2_PROXY_EMAIL_DOMAINS value: '*' - # ロールでは絞らない(2026-10-07)。Hubble は見るだけの画面なので、Argo CD や Kubernetes の「見るだけ」と - # 揃えて、テナントに居る人(ゲストを含む)なら誰でも見られる。管理の画面(AdGuard・*.s.doany.io)は admin だけ - # (docs/entra.md「ゲストを足したときに使えるもの」) + # 見るだけの画面なのでロールで絞らない(docs/entra.md) - name: OAUTH2_PROXY_REVERSE_PROXY value: 'true' - name: OAUTH2_PROXY_SET_XAUTHREQUEST diff --git a/bootstrap/auth/referencegrant-adguard.yaml b/bootstrap/auth/referencegrant-adguard.yaml index 661fc60..a674507 100644 --- a/bootstrap/auth/referencegrant-adguard.yaml +++ b/bootstrap/auth/referencegrant-adguard.yaml @@ -1,6 +1,4 @@ -# adguardhome の HTTPRoute から auth の Service を指すための許可。 -# Gateway API は名前空間をまたぐ backendRef を既定で禁じているので、 -# 指される側(auth)に置く必要がある。 +# adguardhome の HTTPRoute から auth の Service を指す許可(名前空間をまたぐ backendRef は指される側に置く) apiVersion: gateway.networking.k8s.io/v1beta1 kind: ReferenceGrant metadata: diff --git a/bootstrap/auth/secrets.yaml b/bootstrap/auth/secrets.yaml index 0e46bd6..464ec1c 100644 --- a/bootstrap/auth/secrets.yaml +++ b/bootstrap/auth/secrets.yaml @@ -1,7 +1,4 @@ -# Secret auth-secrets は Infisical (il.doany.io) のフォルダ /auth/auth-secrets にあり、 -# Infisical 純正 operator (apps/infisical-operator) が Secret にする。 -# 値は Infisical の UI で書き換えるだけ(push-bridge が数秒で同期。auto-reload 注釈のある -# Deployment は自動で入れ替わる)。 +# Infisical の /auth/auth-secrets を operator が Secret にする(UI で書き換えれば数秒で同期)。 apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalSecret metadata: diff --git a/bootstrap/auth/sub-backend.yaml b/bootstrap/auth/sub-backend.yaml index 32e0d07..92bfa52 100644 --- a/bootstrap/auth/sub-backend.yaml +++ b/bootstrap/auth/sub-backend.yaml @@ -1,6 +1,4 @@ -# *.s.doany.io の中継先。LAN のホスト(10.10.0.2)を名前で指せるようにするためだけの -# Service と、手書きの EndpointSlice。Endpoint は selector では作れない(Pod ではない)。 -# 使うのは auth-sub の oauth2-proxy(sub-proxy.yaml の OAUTH2_PROXY_UPSTREAMS)。 +# *.s.doany.io の中継先(LAN の 10.10.0.2)を名前で指すための Service と手書きの EndpointSlice apiVersion: v1 kind: Service metadata: @@ -20,8 +18,7 @@ metadata: labels: kubernetes.io/service-name: sub-backend addressType: IPv4 -# API サーバが足す既定値(`protocol`・`conditions`)も書く。どちらも丸ごと比べるリストなので、 -# 書かないと ArgoCD がずっと OutOfSync と見る(2026-10-17、`bootstrap` に移したときに出た) +# API サーバが足す既定値(`protocol`・`conditions`)も書く。書かないと ArgoCD がずっと OutOfSync と見る ports: - name: http port: 80 diff --git a/bootstrap/auth/sub-proxy.yaml b/bootstrap/auth/sub-proxy.yaml index aabd310..53d8b5b 100644 --- a/bootstrap/auth/sub-proxy.yaml +++ b/bootstrap/auth/sub-proxy.yaml @@ -1,8 +1,5 @@ # *.s.doany.io を SSO 付きで LAN のホスト(sub-backend)へ中継する oauth2-proxy。 -# Cilium の Gateway には ext auth が無いので -# oauth2-proxy 自身を前段のプロキシとして置く(--upstream で中継先を持つ)。 -# コールバックは既存の a.doany.io 側インスタンスが受ける。cookie secret を共有しているので -# セッションはそのまま通る。 +# Gateway に ext auth が無いので前段のプロキシにする。コールバックは a.doany.io 側が受ける(cookie secret を共有)。 apiVersion: apps/v1 kind: Deployment metadata: @@ -51,8 +48,7 @@ spec: value: .doany.io - name: OAUTH2_PROXY_EMAIL_DOMAINS value: '*' - # a.doany.io 側と同じく Entra のアプリロール admin で絞る。 - # グループクレームから移した理由は docs/entra.md + # Entra のアプリロール admin だけ - name: OAUTH2_PROXY_OIDC_GROUPS_CLAIM value: roles - name: OAUTH2_PROXY_ALLOWED_GROUPS diff --git a/bootstrap/cert-manager/clusterissuer.yaml b/bootstrap/cert-manager/clusterissuer.yaml index 344b7ed..fb54db8 100644 --- a/bootstrap/cert-manager/clusterissuer.yaml +++ b/bootstrap/cert-manager/clusterissuer.yaml @@ -1,6 +1,4 @@ -# Let's Encrypt を Cloudflare の DNS-01 で。ワイルドカード証明書を取るので DNS-01。 -# トークンは同じ namespace の Secret にしか置けない(cert-manager は自分の namespace の -# Secret しか読まない)ので、cloudflare-secret.sops.yaml がここに置いている。 +# ワイルドカードを取るので DNS-01。トークンは cert-manager の namespace の Secret(cloudflare-secret.sops.yaml)。 apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: diff --git a/bootstrap/cert-manager/values.yaml b/bootstrap/cert-manager/values.yaml index 1f0a162..8bf1eea 100644 --- a/bootstrap/cert-manager/values.yaml +++ b/bootstrap/cert-manager/values.yaml @@ -1,18 +1,9 @@ -# cert-manager の Helm 値。版は [version.yaml](version.yaml)。 -# -# 当てるのは [../../talos/render.sh](../../talos/render.sh)。この値で `helm template` して -# machine config の inlineManifest にする(Cilium と同じ。更新は render.sh → apply-config → upgrade-k8s)。 -# -# 2026-09-08 まで版も値も git に無かった(`helm install` した一度きり)。 -# 作り直すと下の `enableGatewayAPI` が既定(false)に戻り、証明書が 1 枚も発行されなくなる。 +# cert-manager の chart の値。描いて machine config に載せるのは ../../talos/render.sh。 -# Gateway API を見る。これが無いと Gateway の `cert-manager.io/cluster-issuer` -# 注釈から Certificate を作ってくれない。 +# 無いと Gateway の `cert-manager.io/cluster-issuer` 注釈から Certificate を作らず、証明書が 1 枚も出ない config: enableGatewayAPI: true -# CRD は chart から描かせない。6 つで 1.30 MB あり、描き出しの 97%。 -# render.sh が `KubeExternalManifestConfig` で URL を渡す(argocd・gateway-api と同じ手)。 -# なぜ URL なのかは [../../docs/decisions.md](../../docs/decisions.md)「層の分け方」。 +# CRD は描かせない(1.30 MB で描き出しの 97%)。render.sh が URL で渡す。 crds: enabled: false diff --git a/bootstrap/cert-manager/version.yaml b/bootstrap/cert-manager/version.yaml index abbdd02..8d49d0f 100644 --- a/bootstrap/cert-manager/version.yaml +++ b/bootstrap/cert-manager/version.yaml @@ -1,3 +1,2 @@ -# cert-manager の chart のバージョン。当て方は values.yaml に書いてある。 # renovate: datasource=helm depName=cert-manager registryUrl=https://charts.jetstack.io version: v1.21.2 diff --git a/bootstrap/cilium/values.yaml b/bootstrap/cilium/values.yaml index ed29e58..3ff2acb 100644 --- a/bootstrap/cilium/values.yaml +++ b/bootstrap/cilium/values.yaml @@ -1,19 +1,13 @@ -# Cilium の Helm 値。これがクラスタの土台。 -# -# このファイルが「あるべき値」。当てるのは talos/render.sh で、ここから `helm template` して -# machine config の inlineManifest にする(更新は render.sh → apply-config → upgrade-k8s。 -# talos/README.md「上げ方 / 当て直し方」)。`cilium-config` の ConfigMap を直接いじらない ── -# 次の upgrade-k8s で黙って戻る(2026-09-07 まで値が git に無く、`node-port-range: "80,443"` が -# ConfigMap 直いじりでしか入っていなかった。当時は、戻ると公開している Web が全部落ちた)。 +# Cilium の値の正本。talos/render.sh が描いて inlineManifest にする(render.sh → apply-config → upgrade-k8s)。 +# cilium-config を直接いじらない(次の upgrade-k8s で黙って戻る)。 -# kube-proxy は使わない(Talos 側は talos/patches/cni.yaml で止めている)。 +# kube-proxy は talos/patches/cni.yaml で止めている kubeProxyReplacement: true -# KubePrism。Talos が localhost:7445 で API サーバを負荷分散する。 -# ここが違うと Cilium が API サーバに繋げず上がらない(talos-boot が流す render.sh の needle が `7445` を見ている)。 +# KubePrism。Talos が localhost:7445 で API サーバを負荷分散する。違うと Cilium が上がらない(render.sh の needle が見ている)。 k8sServiceHost: localhost k8sServicePort: 7445 -# Talos は cgroup を自分で用意する。Cilium に自動マウントさせると衝突する。 +# Talos は cgroup を自分で用意する。自動マウントさせると衝突する。 cgroup: autoMount: enabled: false @@ -24,100 +18,46 @@ ipam: ipv6: enabled: true -# 送信元 IP を保つのに要る(2026-09-07)。既定は iptables で SNAT していて、 -# hostPort に入ってくる IPv4 の送信元が cilium_host(10.42.0.148)に潰れていた。 -# IPv6 は素通りしていたので気づきにくい。実害: -# -# | | | -# | --- | --- | -# | NetBird の STUN(3478) | 外側アドレスを誤って返す → IPv4 の NAT 越えが成立せず relay 専用に落ちる | -# | AdGuard(53/853) | IPv4 クライアントのクエリログとクライアント別ルールが効かない | -# | 3proxy(3129/8444)・LiveKit(8443。Matrix の通話) | 送信元が全部同じに見える | -# -# `kubeProxyReplacement: true` と組み合わせるときの推奨設定でもある。 +# 送信元 IP を保つ。無いと hostPort に来る IPv4 の送信元が cilium_host に潰れる +# (NetBird の STUN が外側アドレスを誤って返す、AdGuard のクライアント別ルールが効かない、など)。 bpf: masquerade: true -# 既定はどれも無効。有効にしないと Pod は Running なのにホストのポートに何も来ない -# (eBPF なので `ss` にも現れない。確認は `cilium-dbg service list`)。 +# 既定はどれも無効。無いと Pod は Running なのにホストのポートに何も来ない(確認は `cilium-dbg service list`)。 hostPort: enabled: true externalIPs: enabled: true nodePort: enabled: true - # 範囲は既定(30000-32767)。Gateway の Service の nodePort は API サーバが割り当てるランダムな値のままでよい。 - # 外からの 80/443 は Envoy が hostNetwork で掴んだソケットに直接届き、ノード自身からも届く(下の gatewayAPI.hostNetwork)。 - # k3s の頃は nodePort 80/443 の L7LB リダイレクトが経路そのもので、揃える手当てが要った(2026-10-18 に外した。#436)。 - # - # **範囲を広げないこと。** 範囲に入る hostPort を Cilium は張らない(`80-32767` にしたとき - # AdGuard の 853、LiveKit の 8443、3proxy の 3129 が一斉に落ちた)。 + # 範囲は既定のまま。広げないこと: 範囲に入る hostPort を Cilium は張らない(853・8443・3129 が一斉に落ちた)。 -# Gateway API。CRD は v1.6.1 が要る(v1.5.1 だと GatewayClass が無言で止まる)。 +# Gateway API。CRD が古いと GatewayClass が無言で止まる。 gatewayAPI: enabled: true - # Envoy をホストネットワークで待たせる(2026-09-07)。LoadBalancer Service も - # nodePort も介さず、ノードの 80/443 に直接 bind する。 - # - # 何が変わったか - # - # LoadBalancer Service が 1 つも無くなった。それまで Gateway は LB-IPAM から - # `10.10.0.53` をもらう LoadBalancer で、参照はドキュメントの中だけだった。 - # おかげで LB-IPAM と L2 アナウンスを撤去できた(2026-09-07)。 - # Gateway の `status.addresses` はノードの `10.0.0.2` になる。 - # - # Service は消えず NodePort になる(`operator/pkg/model/translation/gateway-api/translator.go` - # の `toServiceType`: `if HostNetworkConfig.Enabled { return ServiceTypeNodePort }`)。nodePort の値は経路に関係しない。 - # - # ポートの指定は無い。Gateway のリスナー(80 / 443)がそのままホストに bind される。 - # `httpPort` / `httpsPort` / `sharedListenerPort` は `ingressController.hostNetwork` - # 側の項目で、こちらには無い(値の見た目が似ているので混同しやすい)。 + # Envoy がノードの 80/443 に直接 bind する(Service は NodePort になるが、値は経路に関係しない)。 + # ポートの指定は無い。httpPort などは ingressController.hostNetwork 側の項目。 hostNetwork: - # 下の envoy.securityContext とセット。80/443 は特権ポートなので、 - # NET_BIND_SERVICE が無いと `cannot bind '0.0.0.0:80': Permission denied` で - # 公開 Web が全部落ちる(2026-09-07 に実際にやった)。 - # 実際に付いていることは fork 後のプロセスで確認する: - # kubectl -n kube-system exec ds/cilium-envoy -- grep CapEff /proc/7/status - # → 0000000000000400 (CAP_NET_BIND_SERVICE のみ。starter が他を落とす) + # 下の envoy.securityContext とセット。無いと 80/443 の bind が Permission denied で公開 Web が全部落ちる。 + # 確認: kubectl -n kube-system exec ds/cilium-envoy -- grep CapEff /proc/7/status → 0000000000000400 enabled: true - # gRPC を通すのに要る(2026-09-07)。既定は false で、その場合 Envoy は TLS の - # ALPN で `h2` を広告しない。grpc-go のクライアントは ALPN の合意を必須にしているので、 - # NetBird のピアが `missing selected ALPN property` で管理サーバに繋げなかった。 - # - # 有効にすると全リスナーが「HTTP/2 を試して、駄目なら HTTP/1.1」を広告する。 - # つまりブラウザからの既存サイトも HTTP/2 になる。WebSocket は HTTP/2 上では - # 拡張 CONNECT が要るが、ブラウザはそれが使えなければ HTTP/1.1 の別接続に落ちるので - # ArgoCD・Infisical はそのまま動く(適用後に実際に確認した)。 - # - # チャートの注記どおり appProtocol サポートも一緒に有効になるが、 - # このクラスタに appProtocol を宣言している Service は 1 つも無いので影響しない。 + # gRPC に要る(無いと ALPN で h2 を広告せず、NetBird のピアが `missing selected ALPN property` で繋がらない)。 + # 全リスナーが HTTP/2 になるが、WebSocket はブラウザが HTTP/1.1 に落ちるので動く。 enableAlpn: true -# クラスタ内の通信を見る画面。Cilium 本体の hubble は元から有効(:4244 で待ち受け)で、 -# 足りていなかったのは Relay と UI の 2 Pod だけ。 -# -# UI 自身は認証を持たない。oauth2-proxy を前段に置いて守る -# (Cilium の Gateway には ext auth が無いので、*.s.doany.io と同じ「前段プロキシ」方式。 -# ../auth/hubble-proxy.yaml)。UI の Service を直接 HTTPRoute に繋がないこと。 +# UI 自身は認証を持たない。前段の oauth2-proxy(../auth/hubble-proxy.yaml)で守る。UI の Service を直接 HTTPRoute に繋がない。 hubble: relay: enabled: true ui: enabled: true - # Hubble の TLS 証明書はクラスタの中の Job / CronJob(cilium-certgen)が作る。既定の `helm` は描くたびに - # CA と証明書を作り直すので、machine config に秘密鍵が載り、何も変えなくても apply-config に差分が出ていた。 + # 証明書はクラスタ内の CronJob が作る。既定の `helm` は描くたびに作り直し、machine config に秘密鍵が載る。 tls: auto: method: cronJob -# Envoy に特権ポートを bind させるための権限。Gateway のリスナーは 80/443 で、 -# hostNetwork にするとホストのその番号を直接掴む。既定の Envoy にはこの権限が無い。 -# -# 2 つ要る。コンテナに capability を渡すだけでは足りず、`cilium-envoy-starter` が -# fork 後に全部落とすので、`keepCapNetBindService` で明示的に残す必要がある -# (chart の values のコメントに書いてある)。 -# -# 既定は `[NET_ADMIN, SYS_ADMIN]`。既定を消さないよう両方書く。 +# 特権ポートの bind。cilium-envoy-starter が fork 後に capability を落とすので keepCapNetBindService も要る。 +# 既定の NET_ADMIN / SYS_ADMIN を消さないよう両方書く。 envoy: securityContext: capabilities: diff --git a/bootstrap/cilium/version.yaml b/bootstrap/cilium/version.yaml index 49b4cfc..d8b61f8 100644 --- a/bootstrap/cilium/version.yaml +++ b/bootstrap/cilium/version.yaml @@ -1,5 +1,2 @@ -# cilium の chart のバージョンはここが唯一の出どころ。文章の中の数字は Renovate が追えないので、 -# 機械が読める形に出してある。読むのは talos/render.sh と .github/workflows/cilium-drift.yml。 -# 形は talos/versions.yaml と揃えてある。 # renovate: datasource=helm depName=cilium registryUrl=https://helm.cilium.io version: 1.20.3 diff --git a/bootstrap/gateway/gateway.yaml b/bootstrap/gateway/gateway.yaml index 19cad3b..26e12d5 100644 --- a/bootstrap/gateway/gateway.yaml +++ b/bootstrap/gateway/gateway.yaml @@ -1,6 +1,4 @@ -# Cilium Gateway(Gateway API)。公開している Web はすべてここを通る。 -# 証明書は cert-manager が ClusterIssuer letsencrypt(Cloudflare DNS-01)で発行する。 -# ワイルドカードなのでホストを足すたびに証明書を増やさなくてよい。 +# 公開している Web はすべてここを通る。証明書はワイルドカード(DNS-01)。 apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -15,7 +13,7 @@ spec: - doany.io - "*.doany.io" --- -# `sub`(*.s.doany.io)は 2 段のワイルドカードなので上の証明書では覆えない。別に発行する。 +# 2 段のワイルドカードは上の証明書で覆えない apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -29,8 +27,7 @@ spec: dnsNames: - "*.s.doany.io" --- -# `dp.l.doany.io`(宅内向けの denpa)も 2 段のワイルドカードなので別に発行する。 -# 公開 DNS には無く AdGuard の rewrite だけで引ける名前だが、DNS-01 なので A レコードは要らない。 +# 宅内向け(AdGuard の rewrite だけで引ける名前。DNS-01 なので公開の A は要らない) apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -50,14 +47,12 @@ metadata: name: doany namespace: kube-system annotations: - # ExternalDNS(apps/external-dns/)が作る CNAME の宛先。apex(doany.io)の A / AAAA は DDNS が書く。 - # これが無いと、ExternalDNS は Gateway の住所(LAN の 10.0.0.2)を宛先にしてしまう + # ExternalDNS の CNAME の宛先。無いと LAN の 10.0.0.2 を宛先にする external-dns.kubernetes.io/target: doany.io spec: gatewayClassName: cilium listeners: - # HTTP は全部 HTTPS に飛ばす。 - # 実際のリダイレクトは redirect-https.yaml の HTTPRoute フィルタが行う。 + # HTTPS へのリダイレクトは redirect-https.yaml - name: http protocol: HTTP port: 80 @@ -77,7 +72,7 @@ spec: allowedRoutes: namespaces: from: All - # *.s.doany.io(SSO 付きで LAN のホストへ中継)。証明書は 2 段目のワイルドカード。 + # SSO 付きで LAN のホストへ中継 - name: https-sub protocol: HTTPS port: 443 @@ -90,7 +85,6 @@ spec: allowedRoutes: namespaces: from: All - # *.l.doany.io(宅内からだけ引ける名前。いまは dp.l.doany.io だけ)。 - name: https-lan protocol: HTTPS port: 443 diff --git a/bootstrap/gateway/redirect-https.yaml b/bootstrap/gateway/redirect-https.yaml index e3cc254..bbf1620 100644 --- a/bootstrap/gateway/redirect-https.yaml +++ b/bootstrap/gateway/redirect-https.yaml @@ -1,5 +1,3 @@ -# 公開経路。証明書は Gateway([../gateway/](../gateway/))のリスナーに付いているので指定しない。 -# 当てるのは ArgoCD の Application `bootstrap`(../README.md「公開経路(HTTPRoute)もここにある」)。 apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: diff --git a/bootstrap/infisical/smtp-secret.yaml b/bootstrap/infisical/smtp-secret.yaml index 1089225..4eb1e3b 100644 --- a/bootstrap/infisical/smtp-secret.yaml +++ b/bootstrap/infisical/smtp-secret.yaml @@ -1,7 +1,4 @@ -# Secret infisical-smtp は Infisical (il.doany.io) のフォルダ /infisical/infisical-smtp にあり、 -# Infisical 純正 operator (apps/infisical-operator) が Secret にする。 -# 値は Infisical の UI で書き換えるだけ(push-bridge が数秒で同期。auto-reload 注釈のある -# Deployment は自動で入れ替わる)。 +# Infisical の /infisical/infisical-smtp を operator が Secret にする(UI で書き換えれば数秒で同期)。 apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalSecret metadata: diff --git a/bootstrap/infisical/values.yaml b/bootstrap/infisical/values.yaml index cb057f2..748d9ee 100644 --- a/bootstrap/infisical/values.yaml +++ b/bootstrap/infisical/values.yaml @@ -1,36 +1,21 @@ -# infisical-standalone の chart の値。版は version.yaml、DB と Redis のパスワードは -# secret-values.sops.yaml(SOPS)。描いて machine config に載せるのは talos/render.sh -# (ArgoCD に置けない理由は docs/decisions.md「infisical だけは ArgoCD に移せない」)。 -# -# https://github.com/Infisical/infisical/tree/main/helm-charts/infisical-standalone-postgres -# Bundles Postgres + Redis (bitnami subcharts). Single node, so one replica -# and in-cluster databases are fine (the Postgres dump is k8up's, see podAnnotations below). +# infisical-standalone の chart の値(Postgres + Redis 同梱)。DB と Redis のパスワードは secret-values.sops.yaml。 +# 描いて machine config に載せるのは talos/render.sh(ArgoCD に置けない理由は docs/decisions.md)。 infisical: - # Otherwise the Deployment/Service come out as - # infisical-infisical-standalone-infisical (release + chart + component). + # 無いと名前が infisical-infisical-standalone-infisical になる fullnameOverride: infisical replicaCount: 1 image: - # 既定は Docker Hub(infisical/infisical)。匿名の取得は絞られるので、同じものを - # mirror.gcr.io から引く(postgresql / redis は chart の既定で mirror.gcr.io)。 + # Docker Hub の匿名取得は絞られるので mirror.gcr.io から。chart の既定の tag は遅れるので固定する repository: mirror.gcr.io/infisical/infisical - # The chart's default tag lags releases; pin explicitly. tag: v0.166.8 - # ENCRYPTION_KEY / AUTH_SECRET / SITE_URL live in infisical/secrets.sops.yaml. + # ENCRYPTION_KEY / AUTH_SECRET / SITE_URL は infisical/secrets.sops.yaml kubeSecretRef: infisical-secrets extraEnv: - # Kubernetes auth makes Infisical call https://kubernetes.default.svc - # (TokenReview); its SSRF guard refuses cluster-internal IPs unless - # told otherwise. + # Kubernetes auth の TokenReview で kubernetes.default.svc を叩くのを SSRF ガードが拒むため - name: ALLOW_INTERNAL_IP_CONNECTIONS value: "true" - # Outgoing mail via Exchange Online SMTP AUTH on the info@doany.io - # mailbox (STARTTLS on 587 is Infisical's - # default, SMTP_REQUIRE_TLS=true). The password is the one value here - # that lives in Infisical itself (/infisical/infisical-smtp, pulled by - # smtp-secret.yaml), hence optional: on a from-scratch install the - # pod comes up mail-less, the operator fills the Secret once Infisical answers, - # and a rollout restart turns mail on. + # パスワードだけ Infisical 自身にある(smtp-secret.yaml)ので optional。まっさらな状態ではメール無しで + # 上がり、operator が Secret を作ったあと rollout restart でメールが有効になる。 - name: SMTP_HOST value: smtp.office365.com - name: SMTP_PORT @@ -48,11 +33,10 @@ infisical: key: SMTP_PASSWORD optional: true ingress: - # Exposed through the Cilium Gateway instead; the HTTPRoute lives in - # apps/infisical/httproute.yaml. + # 公開は Gateway(apps/infisical/httproute.yaml) enabled: false nginx: - # Also the on/off switch for the bundled ingress-nginx subchart. + # 同梱の ingress-nginx の切り替えも兼ねる enabled: false postgresql: enabled: true @@ -61,9 +45,7 @@ postgresql: database: infisicalDB primary: persistence: - # 既定の local-path(reclaim は Delete。2026-10-05 に local-path-retain から寄せた)。 - # chart を消しても PVC は残る ── StatefulSet の volumeClaimTemplates が作った - # PVC は StatefulSet を消しても消えない。後ろ盾は k8up の日次 pg_dump。 + # chart を消しても PVC は残る。後ろ盾は k8up の日次 pg_dump storageClass: local-path size: 5Gi podAnnotations: @@ -74,5 +56,5 @@ redis: auth: master: persistence: - # Queue/cache only; nothing worth keeping across restarts. + # キューとキャッシュだけ enabled: false diff --git a/bootstrap/infisical/version.yaml b/bootstrap/infisical/version.yaml index f8e588d..ed85570 100644 --- a/bootstrap/infisical/version.yaml +++ b/bootstrap/infisical/version.yaml @@ -1,3 +1,2 @@ -# infisical-standalone の chart の版。読むのは talos/render.sh(repo もこの行の registryUrl から読む)。 # renovate: datasource=helm depName=infisical-standalone registryUrl=https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/ version: 1.11.0 diff --git a/compose.yaml b/compose.yaml index 3e018fb..46ff067 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,13 +3,11 @@ services: tools: build: tools image: gitops-tools:latest - # wslc は host ネットワークに対応していない。ブラウザのログインは t.ps1 が中継する(tools/README.md)。 - # uid は指定しない(Windows のファイルに持ち主の uid は無い) + # wslc は host ネットワークに対応していない。ブラウザのログインは t.ps1 が中継する working_dir: /repo environment: - # **ログインと鍵はリポジトリの .home/ に置く**(.gitignore 済み)。手元のホームは汚さない + # ログインと鍵はリポジトリの .home/(.gitignore 済み) - HOME=/repo/.home - # 手元で設定しているときだけ渡す - INFISICAL_PROFILE volumes: - .:/repo diff --git a/deploy/argocd.yaml b/deploy/argocd.yaml index dad37b7..bc59670 100644 --- a/deploy/argocd.yaml +++ b/deploy/argocd.yaml @@ -1,14 +1,10 @@ -# This repo's ArgoCD配置. The ApplicationSet in bootstrap/argocd/repos.yaml -# scans the org for this file and builds this repo's Application from it -- so everything about how this repo gets -# deployed is decided here, not in the cluster or in bootstrap. +# このリポジトリの Application の設定(bootstrap/argocd/repos.yaml の ApplicationSet が読む) name: gitops namespace: '' -# Directory holding the manifests, relative to the repo root. sourcePath: apps recurse: true -# autoSync: false leaves the app on manual sync in the UI. autoSync: true prune: true selfHeal: true diff --git a/pulumi/dns.ts b/pulumi/dns.ts index 8329997..a884adb 100644 --- a/pulumi/dns.ts +++ b/pulumi/dns.ts @@ -1,8 +1,5 @@ -// doany.io の DNS のうち、ExternalDNS が作らないもの(メール・ワイルドカード・ドメインの確認)と、ゾーンの設定。 -// -// アプリごとの名前は ExternalDNS がアプリの HTTPRoute から作る(apps/external-dns/)。ここには書かない。 -// apex の A / AAAA は cloudflare-ddns が家の IP に合わせて書き換えるので、ここでは持たない(apps/cloudflare-ddns/)。 -// 既存のレコードは import で取り込んだ(2026-10-07。取り込み済みなら何もしない)。 +// doany.io の DNS のうち ExternalDNS が作らないものと、ゾーンの設定。 +// アプリごとの名前は ExternalDNS、apex の A / AAAA は cloudflare-ddns が持つ。 import * as cloudflare from "@pulumi/cloudflare"; @@ -13,38 +10,28 @@ const record = (key: string, args: Omit, imp new cloudflare.DnsRecord(key, { zoneId, ...args }, importId ? { import: `${zoneId}/${importId}` } : undefined); // --- Web --------------------------------------------------------------------------------------- -// 直接つなぐアプリ(社内用・重いもの)は全部これで家に来る。Cloudflare を通すアプリは ExternalDNS が -// 個別の名前を作って上書きする(docs/decisions.md「DNS:直接つなぐアプリと Cloudflare を通すアプリを分ける」) -// **NetBird の nb.doany.io もこれに頼っている。** NetBird のゾーンでは nb を nb.origin.doany.io への CNAME にしていて、 -// その答え(家の IP)はこのワイルドカードから来る(netbird.ts)。ここを変えると NetBird の管理サーバーが引けなくなる +// 直接つなぐアプリはこれで家に来る(Cloudflare を通すものは ExternalDNS が個別に上書きする)。 +// NetBird の nb.doany.io もこれに頼っている(netbird.ts)。変えると NetBird の管理サーバーが引けなくなる record("wildcard", { name: "*.doany.io", type: "CNAME", content: "doany.io", proxied: false, ttl: 1 }, "22d196005f64645eca6bccefd4e3fdee"); -// mado(ブラウザから RDP する入口。mado/)。**`main` ではなく JCOM 側の PC(10.10.0.2)に直接来る。** `main` が落ちたときの -// 入口なので、`main` の Gateway も cloudflare-ddns も通さない。JCOM は固定 IP(IPv4 も IPv6 のプレフィックスも)。 -// IPv4 の 80 / 443 は JCOM のルーターでこの PC に転送し、IPv6 はルーターが止めないので PC に足した固定のアドレスに直接来る -// (docs/router.md「10.10 側(JCOM)のルーター」) +// mado(ブラウザから RDP する入口)。main が落ちたときの入口なので、JCOM 側の PC(固定 IP)に直接来る(docs/router.md) record("md-a", { name: "md.doany.io", type: "A", content: "61.21.173.41", proxied: false, ttl: 1 }); record("md-aaaa", { name: "md.doany.io", type: "AAAA", content: "2405:1201:5201:3a00::2", proxied: false, ttl: 1 }); -// 証明書を出してよい認証局。cert-manager が使う Let's Encrypt だけ。Cloudflare の Universal SSL の認証局は -// Cloudflare が自分で足すので書かない +// 証明書を出してよい認証局は Let's Encrypt だけ(Universal SSL のぶんは Cloudflare が自分で足す) record("caa-issue", { name: "doany.io", type: "CAA", ttl: 1, data: { flags: 0, tag: "issue", value: "letsencrypt.org" } }); record("caa-issuewild", { name: "doany.io", type: "CAA", ttl: 1, data: { flags: 0, tag: "issuewild", value: "letsencrypt.org" } }); // --- メール(Exchange Online)---------------------------------------------------------------------- -// DNSSEC に対応した MX(Enable-DnssecForVerifiedDomain で払い出された。2026-10-07)。DANE はこれで効く。 -// 以前の doany-io.mail.protection.outlook.com は切り替えのあと消した(apps/mta-sts/README.md「DANE への切り替え」) +// DNSSEC に対応した MX(Enable-DnssecForVerifiedDomain で払い出された)。DANE はこれで効く record("mx-dnssec", { name: "doany.io", type: "MX", content: "doany-io.o-v1.mx.microsoft", priority: 0, ttl: 3600 }); record("autodiscover", { name: "autodiscover.doany.io", type: "CNAME", content: "autodiscover.outlook.com", proxied: false, ttl: 3600 }, "93eb5f05c05d9ca6ae8239cad26089e2"); -// 送るのは Exchange だけ(アプリも smtp.office365.com 経由)。以前あった a:b.doany.io は、ワイルドカード経由で -// 家の IP を許していたので外した。送り元は Exchange だけなので -all(2026-10-07) +// 送るのは Exchange だけ(アプリも smtp.office365.com 経由)なので -all record("spf", { name: "doany.io", type: "TXT", content: "v=spf1 include:spf.protection.outlook.com -all", ttl: 3600 }, "05522a739cbd45b787b93f1a700b8916"); -// DKIM(Exchange Online)。行き先は Get-DkimSigningConfig の Selector1CNAME / Selector2CNAME。 -// **明示しないとワイルドカードに吸われて doany.io の TXT(SPF)が返る**(2026-10-07 まではそうだった) +// DKIM。行き先は Get-DkimSigningConfig の Selector1CNAME / Selector2CNAME。明示しないとワイルドカードに吸われる record("dkim-selector1", { name: "selector1._domainkey.doany.io", type: "CNAME", content: "selector1-doany-io._domainkey.doany.q-v1.dkim.mail.microsoft", proxied: false, ttl: 3600 }); record("dkim-selector2", { name: "selector2._domainkey.doany.io", type: "CNAME", content: "selector2-doany-io._domainkey.doany.q-v1.dkim.mail.microsoft", proxied: false, ttl: 3600 }); -// なりすましを受け取り側で弾かせる。DKIM(selector1)と SPF が通るのを確かめてから reject にした(2026-10-07。 -// Gmail で dkim=pass / spf=pass / dmarc=pass)。集計の報告(rua)は info@ に来る +// 集計の報告(rua)は info@ に来る record("dmarc", { name: "_dmarc.doany.io", type: "TXT", content: "v=DMARC1; p=reject; rua=mailto:info@doany.io", ttl: 1 }, "b1a49318efbb097555b43fede5e26697"); // MTA-STS(RFC 8461)。方針ファイルは apps/mta-sts/。**方針を変えたら id も変える**(送り手は id で取り直す) @@ -60,17 +47,14 @@ record("google-site-verification-2", { name: "doany.io", type: "TXT", content: " record("github-verification", { name: "_gh-danything-o.doany.io", type: "TXT", content: '"f39e74c26c"', ttl: 1 }, "71200b09aa051711661e46f5f2e95b60"); // --- ゾーンの設定(Cloudflare を通す名前にだけ効く)----------------------------------------------- -// 設定は「取り込み」ではなく「書く」(PATCH なので、取り込まなくても同じ)。security_header を import すると -// プロバイダー(cloudflare 6.22)が nil pointer で落ちる(2026-10-07) +// 取り込まずに書く(PATCH なので同じ。security_header を import するとプロバイダーが nil pointer で落ちる) const setting = (settingId: string, value: unknown) => new cloudflare.ZoneSetting(`setting-${settingId}`, { zoneId, settingId, value }); setting("ssl", "strict"); setting("always_use_https", "on"); -// 1.0 / 1.1 は今使う理由が無い(既定のままだった) setting("min_tls_version", "1.2"); -// HSTS。以前は有効なのに max-age が 0 で、実際には効いていなかった。サブドメインは含めない -// (直接つなぐアプリは家の Gateway が応答するので、ここの設定は届かない)。preload もしない +// HSTS。サブドメインは含めない(直接つなぐアプリにはここの設定が届かない) setting("security_header", { strict_transport_security: { enabled: true, max_age: 31536000, include_subdomains: false, preload: false, nosniff: true }, }); diff --git a/pulumi/entra-ci.ts b/pulumi/entra-ci.ts index 94bb005..d0f6e5f 100644 --- a/pulumi/entra-ci.ts +++ b/pulumi/entra-ci.ts @@ -1,14 +1,6 @@ -// この Pulumi 自身が Entra に入るための、CI 用のアプリ登録 2 つの「信頼の設定」(ワイルドカード ID のフェデレーション)。 -// GitHub Actions のどの実行に、どのアプリ登録としてトークンを出すかを決める(README.md「Entra」)。 -// -// 2026-10-06 に `az` で手で作り、subject の形を間違えて AADSTS700213 で入れなかったことがある。git で持つ。 -// 2026-10-07 に取り込んだ。書き換えられるよう、apply のサービスプリンシパルを 2 つのアプリ登録の所有者にした -// (Application.ReadWrite.OwnedBy は自分が所有者のものしか書けない)。 -// -// **apply の信頼の設定は消せないように protect する。** 消すと apply が Entra に入れなくなり、自分では直せない -// (直すには管理者が az で作り直す)。preview のほうも、壊すと PR の preview が落ちる。 -// -// アプリ登録そのもの(名前・API の権限)は持たない。Graph の権限の付与は管理者の同意が要り、CI の権限では書けない。 +// この Pulumi の CI が Entra に入るための、アプリ登録 2 つの「信頼の設定」(README.md「Entra」)。 +// protect する: apply のものを消すと apply が Entra に入れなくなり、自分では直せない。 +// アプリ登録そのもの(名前・API の権限)は CI の権限では書けないので持たない。 import * as azuread from "@pulumi/azuread"; diff --git a/pulumi/entra.ts b/pulumi/entra.ts index 98e0959..4540ace 100644 --- a/pulumi/entra.ts +++ b/pulumi/entra.ts @@ -1,12 +1,5 @@ -// Entra ID のアプリ登録(`b0fa498f-…`、auth / argocd / headlamp / … が共用する 1 つ)の -// リダイレクト URI(2026-10-06)とアプリロール(2026-10-07)を持つ。アプリ登録のほかの設定(クライアントシークレット、 -// ロールの割り当て)には触れない ── `ApplicationRedirectUris` / `ApplicationAppRole` はそれぞれ自分の分だけを持つリソース。 -// -// **アプリを足す・消すときは、そのアプリのマニフェストと同じ PR でここも直す。** 以前は手で足し引きしていて、 -// ERPNext と Mattermost を消したあとも URI だけが残っていた。 -// -// CI の鍵は置かない。GitHub Actions から Entra へはワークロード ID のフェデレーション(OIDC)で入る -// (README.md「Entra」)。手元で流すときは Azure CLI のログイン(`az login`)がそのまま使われる。 +// 共用の Entra のアプリ登録(`b0fa498f-…`)のリダイレクト URI とアプリロール。ほかの設定には触れない。 +// アプリを足す・消すときは、そのアプリのマニフェストと同じ PR でここも直す。 import * as azuread from "@pulumi/azuread"; @@ -37,7 +30,6 @@ new azuread.ApplicationRedirectUris( type: "Web", redirectUris: WEB_REDIRECTS.map(([host, path]) => `https://${host}${path}`), }, - // 2026-10-06 に手で持っていた 12 本を取り込んだ(取り込み済みなら何もしない) { import: `/applications/${APPLICATION_OBJECT_ID}/redirectUris/Web` }, ); @@ -53,6 +45,5 @@ new azuread.ApplicationAppRole( description: "この登録の後ろにあるセルフホストのアプリの管理者", allowedMemberTypes: ["User"], }, - // 2026-10-07 に画面で作ってあったものを取り込んだ { import: `/applications/${APPLICATION_OBJECT_ID}/appRoles/fd51fd21-182f-4eb1-971e-c545c5862667` }, ); diff --git a/pulumi/expiry.sh b/pulumi/expiry.sh index 72fde50..4cdfc89 100755 --- a/pulumi/expiry.sh +++ b/pulumi/expiry.sh @@ -1,14 +1,7 @@ #!/usr/bin/env bash -# 鍵の期限を見て、30 日を切ったか切れていたら Matrix の部屋 server に知らせる。pulumi.yml の expiry ジョブから呼ぶ。 -# -# 期限を API から読めるものだけを見る。日付を手で書くファイルは置かない (書き忘れと書き換え忘れが起きる)。 -# **鍵は自分の期限だけを読む。** NetBird の Auditor は利用者の一覧は読めても他人のトークンは読めない (403) ので、 -# preview 用と apply 用をそれぞれのジョブで使う (KEY=preview|apply)。 -# - NetBird のサービスユーザーのトークン (README.md「NetBird」) -# - Cloudflare の API トークン (期限が付いていれば) -# - Entra のアプリ登録のクライアントシークレットと証明書 (テナントの全部。共用の Main が切れると SSO が全部止まる)。 -# preview のジョブだけで見る (az login 済みであること) -# 期限の無いもの (WINGET_TOKEN など) は対象外。 +# 鍵の期限を API から読み、WARN_DAYS 日を切ったか切れていたら Matrix に知らせる (pulumi.yml の expiry)。 +# 鍵は自分の期限だけを読む (KEY=preview|apply): NetBird のトークン、Cloudflare の API トークン、 +# Entra のテナントの全シークレットと証明書 (preview だけ。az login 済みであること)。 # # env: KEY NB_PAT CF_TOKEN CF_ACCOUNT WEBHOOK DRY_RUN REPORT_ALL WARN_DAYS set -euo pipefail diff --git a/pulumi/index.ts b/pulumi/index.ts index 0620c49..4ab0f8c 100644 --- a/pulumi/index.ts +++ b/pulumi/index.ts @@ -1,48 +1,33 @@ -// restic の置き場(R2 の doany-restic)の設定。**ここが正本。** -// -// 変えるときはこのファイルを直して PR を出す。PR に preview の差分が付き、main に入ると up が当たる。 -// Cloudflare の画面や `cf` で直接いじると、次の週次の preview(--refresh)がずれとして落ちる。 -// -// 持っているのは「壊れると restic のリポジトリが黙って壊れる / 外に漏れる」設定だけ。 -// Pulumi で表せない残り(カスタムドメインが付いていないこと、容量)は report.ts。 +// restic の置き場(R2 の doany-restic)の設定の正本。直接いじると週次の preview(--refresh)が落ちる。 +// Pulumi で表せない残り(カスタムドメイン・容量)は report.ts。 import * as cloudflare from "@pulumi/cloudflare"; -// Entra のアプリ登録のリダイレクト URI(entra.ts) import "./entra.ts"; -// この Pulumi の CI が Entra に入るための信頼の設定(entra-ci.ts) import "./entra-ci.ts"; -// Cloudflare の通知を Matrix に(notifications.ts) import "./notifications.ts"; -// doany.io の DNS(ExternalDNS が作らないもの)とゾーンの設定(dns.ts) import "./dns.ts"; -// NetBird の DNS・ネットワーク・ルート・ポリシー(netbird.ts) import "./netbird.ts"; -// アカウント ID は git に置かない(endpoint と同じ扱い。apps/k8up/README.md)。CI は secrets から渡す const accountId = process.env.CLOUDFLARE_ACCOUNT_ID; if (!accountId) throw new Error("CLOUDFLARE_ACCOUNT_ID が要る"); const name = "doany-restic"; -// バケット本体。**protect を外さないこと** ── 外すとコードの間違い 1 つでバックアップが丸ごと消える。 -// 2026-09-06 に手で作ったものを import で取り込んだ(import は取り込み済みなら何もしない)。 +// protect を外さないこと(コードの間違い 1 つでバックアップが丸ごと消える) const bucket = new cloudflare.R2Bucket( name, { accountId, name, - // docs/decisions.md「ツール選定(比較の結論)」(R2 のバケット doany-restic、APAC、Standard) location: "apac", storageClass: "Standard", }, { protect: true, import: `${accountId}/${name}/default` }, ); -// **消す・移すルールは 1 本も置かない。** restic の pack が消えるとリポジトリが壊れ、 -// `restic check` までは誰も気づかない。保持は restic の forget/prune だけで決める。 -// Infrequent Access への移行も同じ扱い ── 最低保存期間の課金と、check/prune の読み出し課金が乗る。 -// 残すのは R2 の既定ルール(失敗したマルチパートの残骸を 7 日で掃除する)だけ。 +// 消す・移すルールは置かない(pack が消えるとリポジトリが黙って壊れる。IA は課金が乗る)。 +// 残すのは失敗したマルチパートの掃除だけ。 new cloudflare.R2BucketLifecycle(name, { accountId, bucketName: bucket.name, diff --git a/pulumi/netbird.ts b/pulumi/netbird.ts index a976a51..5efeb79 100644 --- a/pulumi/netbird.ts +++ b/pulumi/netbird.ts @@ -1,11 +1,6 @@ -// NetBird (https://nb.doany.io、apps/netbird/) の設定。**ここが正本。** 2026-10-07 に画面で作ってあったものを import で取り込んだ。 -// -// 持っているのは DNS (ネームサーバー・ゾーン・レコード)、ネットワーク (自宅の LAN への経路)、exit ノードのルート、 -// アクセスのポリシー。**持っていないもの:** グループ (所属するピアはセットアップキーやログインで変わるので、 -// ここでは ID で指すだけ)、ピア、ユーザー、セットアップキー、トークン、Entra の IdP (下に理由)。アカウントの設定は持つ。 -// -// 鍵は state に入れない。provider は環境変数 NB_PAT を読む (PR は PREVIEW_NB_PAT = Auditor、main は NB_PAT = Admin。 -// どちらもサービスユーザーのトークン。README.md「NetBird」)。 +// NetBird (https://nb.doany.io) の設定の正本。DNS・ネットワーク・exit ノード・ポリシー・アカウントの設定を持つ。 +// グループ (ID で指すだけ)・ピア・ユーザー・セットアップキー・トークン・Entra の IdP は持たない。 +// provider は環境変数 NB_PAT を読む (README.md「NetBird」)。 import * as netbird from "@pulumi/netbird"; @@ -38,10 +33,8 @@ new netbird.NameserverGroup( new netbird.DnsSettings("settings", { disabledManagementGroups: [] }, { provider }); -// *.doany.io を家の Gateway (10.0.0.2) に向ける。exit ノード越しに家の外向きの IPv4 を引くと、家のルーターで -// 折り返しになって届かないため (2026-10-07)。ゾーンに無い名前 (apex など) は上のネームサーバーに流れる。 -// **nb.doany.io だけは外向きのまま。** 内部に向けると、管理サーバーとリレーへの接続が NetBird のトンネル頼みになり、 -// 切れたときに戻ってこられない +// *.doany.io を家の Gateway に向ける (外向きの IPv4 は家のルーターで折り返せず届かない)。 +// nb.doany.io だけは外向きのまま (内部に向けると、トンネルが切れたときに戻ってこられない) const zone = new netbird.DnsZone( "doany-io", { name: "doany.io", domain: "doany.io", enabled: true, enableSearchDomain: false, distributionGroups: [ALL] }, @@ -55,12 +48,8 @@ record("wildcard", "*.doany.io", "A", "10.0.0.2", "db2rtin4p630008pv9qg"); // ワイルドカードは 1 段ぶんしか拾わない (x.s.doany.io は *.doany.io に当たらない) record("wildcard-s", "*.s.doany.io", "A", "10.0.0.2", "db2rsg74p630008pv8ag"); -// nb.doany.io は家の外向きのアドレスを返したい。アドレスは書かず、**2 段の名前 nb.origin.doany.io に CNAME で向ける。** -// 2 段なのでこのゾーンのワイルドカードには当たらず、上のネームサーバー (AdGuard → Cloudflare) で引かれる。 -// Cloudflare では *.doany.io が doany.io への CNAME (プロキシなし) で、doany.io は cloudflare-ddns が家の IP に -// 書き換えるので、家の IP が変わっても追従する (IPv4 / IPv6 とも。2026-10-07)。 -// NetBird は同じ名前に CNAME と A / AAAA を並べられない (A / AAAA に戻すときは、先にこれを消す) -// 以前のキーは nb-a (A だったころ)。alias で同じリソースとして扱い、作り直さない +// 2 段の名前に CNAME で向けると、このゾーンのワイルドカードに当たらず Cloudflare の家の IP に追従する。 +// 同じ名前に CNAME と A / AAAA は並べられない。alias は以前のキー (nb-a) を作り直さないため record("nb", "nb.doany.io", "CNAME", "nb.origin.doany.io", "db2rtjf4p630008pv9s0", { aliases: [{ name: "nb-a" }] }); // --- 自宅の LAN への経路 ------------------------------------------------------------------------------- @@ -77,9 +66,8 @@ new netbird.NetworkRouter( opts(`${NETWORK}/daf80r713kb0009ahkrg`), ); -// リソースは必ず 1 つ以上のグループに入れる (provider の制約。2026-10-07 まではどこにも入っていなかった)。 -// アクセスの許可は下のポリシーがリソースを直接指しているので、このグループは今は何にも使っていない。 -// peers / resources は書かない (省略すると管理画面の値をそのまま保つ。中身はリソースの側の groups で決まる) +// リソースは 1 つ以上のグループに入れる (provider の制約。許可はポリシーがリソースを直接指すので使っていない)。 +// peers / resources は書かない (中身はリソースの側の groups で決まる) const homeLan = new netbird.Group("home-lan", { name: "Home LAN" }, { provider }); const lan = new netbird.NetworkResource( @@ -145,7 +133,6 @@ allow("to-lan-10-10", "daf86qf13kb0009ahrh0", "Users to 10.10.0.0/24", "もう }); // --- アカウントの設定とログイン ------------------------------------------------------------------------ -// 2026-10-07 に画面の値をそのまま取り込んだ。ログインは Entra だけ (ローカルのパスワードは止めてある) const ACCOUNT = "daf7c8f13kb0009ahad0"; new netbird.AccountSettings( "account", @@ -176,6 +163,5 @@ new netbird.AccountSettings( opts(ACCOUNT), ); -// 内蔵 IdP (Dex) につないだ Entra の IdP (`entra-daf87u713kb0009ahs4g`、アプリ登録は共用の b0fa498f-…) は**持たない**。 -// provider がクライアントシークレットを必須にしていて (API からは読めない)、持つには共用のアプリ登録のシークレットを -// GitHub に置くことになる。漏れたときの範囲 (全部の SSO) に見合わない。画面で持つ (2026-10-07 に取り込みを試して外した) +// Entra の IdP は持たない。provider がクライアントシークレットを必須にしていて、共用のアプリ登録の +// シークレットを GitHub に置くことになる (漏れると全部の SSO に及ぶ)。画面で持つ diff --git a/pulumi/notifications.ts b/pulumi/notifications.ts index efa316f..19bac01 100644 --- a/pulumi/notifications.ts +++ b/pulumi/notifications.ts @@ -1,8 +1,5 @@ -// Cloudflare の通知を Matrix の部屋 server(スペース「通知」)に流す(hookshot の汎用の受け口 `cloudflare`。apps/matrix/hookshot.yaml)。 -// -// 受け口の URL は ID を含むので git に置かない。CI は secrets の CLOUDFLARE_NOTIFY_WEBHOOK_URL から渡す -// (値は Infisical /matrix/matrix の hook-cloudflare から作った https://m.doany.io/webhook/)。 -// Cloudflare の通知の本文は `text` に来るので、hookshot の slack のスクリプトがそのまま読む。 +// Cloudflare の通知を Matrix の部屋 server に流す(hookshot。apps/matrix/hookshot.yaml)。 +// 受け口の URL は ID を含むので secrets(値は Infisical /matrix/matrix の hook-cloudflare から作る)。 import * as cloudflare from "@pulumi/cloudflare"; import * as pulumi from "@pulumi/pulumi"; diff --git a/pulumi/renovate-post-upgrade.sh b/pulumi/renovate-post-upgrade.sh index ed5400b..e0ff17e 100755 --- a/pulumi/renovate-post-upgrade.sh +++ b/pulumi/renovate-post-upgrade.sh @@ -1,13 +1,6 @@ #!/usr/bin/env bash -# Renovate の postUpgradeTasks から呼ぶ(renovate.json)。pulumi/ の依存を上げた枝で `pulumi install` を流し、bun.lock を作り直す。 -# -# Renovate は bun のプロジェクトでは自分で `bun install` して lock を直すが、ここの package.json は NetBird の SDK を -# `file:sdks/netbird` で指していて、SDK は `pulumi install` でしか作れない(git には入れない。pulumi/README.md)。 -# Renovate の `bun install` は SDK が無くて落ちるので、renovate.json でそれを止め(skipArtifactsUpdate: true)、ここで作る。 -# -# 動くのは Renovate のコンテナ(ghcr.io/renovatebot/renovate、apps/renovate/renovate.yaml の CronJob)の中、リポジトリの直下。 -# Pulumi の CLI は入っていないので、.pulumi-version の版を公式のチェックサムと照らしてから入れる。bun は containerbase の -# install-tool で .bun-version の版を入れる。許可は apps/renovate/renovate.yaml の RENOVATE_ALLOWED_COMMANDS +# Renovate の postUpgradeTasks から呼ぶ(理由は renovate.json)。`pulumi install` で bun.lock を作り直す。 +# Renovate のコンテナには Pulumi の CLI が無いので、チェックサムと照らしてから入れる。 set -euo pipefail cd "$(dirname "$0")" diff --git a/pulumi/report.ts b/pulumi/report.ts index 72c7adc..8b17cbd 100644 --- a/pulumi/report.ts +++ b/pulumi/report.ts @@ -1,9 +1,6 @@ -// Pulumi では表せない 2 つを見る。毎週の preview と一緒に CI が流す。**読むだけ。** -// -// - カスタムドメインが付いていないこと(R2CustomDomain は「付いているもの」しか持てず、 -// 「付いていない」を宣言できない) -// - 容量(アカウント全体。ズレではないので落とさず、Summary に記録するだけ) -// +// Pulumi で表せない 2 つを読むだけで見る(毎週の preview と一緒に CI が流す): +// - カスタムドメインが付いていないこと(R2CustomDomain は「付いていない」を宣言できない) +// - 容量(落とさず Summary に記録するだけ) // cd pulumi && bun install && CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… bun report.ts import { execFileSync } from "node:child_process"; @@ -11,8 +8,7 @@ import { appendFileSync } from "node:fs"; const BUCKETS = ["doany-restic"]; -// cf は自分と同じランタイム(bun)で起動する。cf の bin は `#!/usr/bin/env node` なので、 -// そのまま起動すると node を探しに行く。bun の上で cf が動くことは 2026-10-05 に確認済み。 +// cf の bin は `#!/usr/bin/env node` なので、自分と同じランタイム(bun)で起動する const CF = new URL("./node_modules/.bin/cf", import.meta.url).pathname; const GHA = !!process.env.GITHUB_ACTIONS; diff --git a/renovate.json b/renovate.json index 46b1255..d2fa241 100644 --- a/renovate.json +++ b/renovate.json @@ -4,14 +4,14 @@ "github>5ym/repo-config" ], "kubernetes": { - "description": "The kubernetes manager ships no default file patterns, so without this the manifests here are never scanned for image tags at all.", + "description": "kubernetes の manager は既定のファイルパターンを持たない。", "managerFilePatterns": [ "**/*.yaml" ] }, "customManagers": [ { - "description": "手元のコマンドのイメージ(tools/Dockerfile)。`# renovate: datasource=… depName=…` の次の行の `ARG 名前=版` を見る。**managerFilePatterns の正規表現は `/…/` で囲む。** 囲まないと glob と読まれて何にも当たらない (2026-10-07 まで customManagers は 3 つとも空振りで、kubectl・talosctl・Talos・chart の版は追われていなかった)。", + "description": "tools/Dockerfile の `# renovate:` の次の行の `ARG 名前=版`。managerFilePatterns の正規表現は `/…/` で囲む(囲まないと glob と読まれ何にも当たらない)。", "customType": "regex", "managerFilePatterns": [ "/^tools/Dockerfile$/" @@ -21,7 +21,7 @@ ] }, { - "description": "`# renovate: datasource=… depName=… [registryUrl=…]` の次の行にある version を見る。Talos の版も chart の版も、文章やコメントの中に散らばると Renovate が追えないので、`version.yaml` という 1 ファイルに出す約束にしてある。", + "description": "versions.yaml / version.yaml の `# renovate:` の次の行の version。", "customType": "regex", "managerFilePatterns": [ "/^talos/versions\\.yaml$/", @@ -32,7 +32,7 @@ ] }, { - "description": "Pulumi が Terraform の provider を包んで使う版 (pulumi/Pulumi.yaml の packages)。2 つある: 包む側のプラグイン (`source: terraform-provider` の次の `version:`) と、包まれる provider (`parameters:` の 2 つ目)。SDK は CI と手元の `pulumi install` が毎回作る。Renovate の枝では下の packageRules の postUpgradeTasks が pulumi install して bun.lock を作る (pulumi/README.md)。provider の版は Terraform の registry (Renovate の既定) で見る。生成が引く OpenTofu の registry にも同じ版が出ているが、Renovate が使う 1 件の provider の API (/v1/providers/<名前>) を OpenTofu は持たない (404)。", + "description": "pulumi/Pulumi.yaml の packages で包む Terraform の provider(`parameters:` の 2 つ目)。版は Terraform の registry で見る(OpenTofu の registry は Renovate が使う API を持たない)。", "customType": "regex", "managerFilePatterns": [ "/^pulumi/Pulumi\\.yaml$/" @@ -43,7 +43,7 @@ "datasourceTemplate": "terraform-provider" }, { - "description": "上の包む側のプラグイン (pulumi/pulumi-terraform-provider)。タグは v 付き、Pulumi.yaml は v なし。", + "description": "包む側のプラグイン。タグは v 付き、Pulumi.yaml は v なし。", "customType": "regex", "managerFilePatterns": [ "/^pulumi/Pulumi\\.yaml$/" @@ -58,7 +58,7 @@ ], "packageRules": [ { - "description": "pulumi/ の bun.lock は Renovate に作らせず、postUpgradeTasks の pulumi install で作る。package.json が NetBird の SDK を file:sdks/netbird で指していて、SDK は pulumi install でしか作れないので、Renovate 自身の bun install は落ちる (#304)。renovate-post-upgrade.sh が Pulumi の CLI と bun を入れて pulumi install し、作り直した bun.lock を同じ枝にコミットさせる。実行の許可は自前の Renovate (apps/renovate/renovate.yaml) の RENOVATE_ALLOWED_COMMANDS。Pulumi.yaml の packages の版 (customManagers) を上げたときも同じく作り直す。 matchFileNames に bun.lock も入れる: bun の manager は bun.lock を package file として扱うので、package.json だけだと当たらず、Renovate 自身の bun install が走ってしまった (2026-10-08)。 **pulumi/ の更新は専用の枝 (renovate/pulumi) にまとめる**: skipArtifactsUpdate は、まとめた枝の全部の更新が指定しているときだけ効く (Renovate の docs)。renovate/all に tools/Dockerfile などと一緒に入ると効かず、postUpgradeTasks も走らなかった (#310)。", + "description": "bun.lock は pulumi install で作る(SDK が file:sdks/netbird で、Renovate 自身の bun install は落ちる)。bun.lock も package file なので matchFileNames に入れる。skipArtifactsUpdate は枝の全部の更新が指定しているときだけ効くので専用の枝にする。許可は apps/renovate/renovate.yaml の RENOVATE_ALLOWED_COMMANDS。", "matchFileNames": [ "pulumi/package.json", "pulumi/bun.lock", @@ -79,7 +79,7 @@ } }, { - "description": "AdGuard Home の版上げ。起動時に設定を移行する (schema_version) ので、postUpgradeTasks の renovate-post-upgrade.sh が新しい版の --check-config で apps/adguardhome/deployment.yaml の設定 (initContainer の CONFIG) を移行し、同じ枝にコミットする。読めない設定ならスクリプトが落ちて PR に出る。実行の許可は自前の Renovate (apps/renovate/renovate.yaml) の RENOVATE_ALLOWED_COMMANDS。専用の枝にするのは、移行が落ちたときに all dependencies の PR まで止めないため", + "description": "AdGuard Home は新しい版の --check-config で deployment.yaml の設定を移行して同じ枝にコミットする。専用の枝は、移行が落ちても他の PR を止めないため。", "matchDatasources": [ "docker" ], @@ -99,7 +99,7 @@ } }, { - "description": "forgejo と rybbit の DB。PostgreSQL はメジャーが変わるとデータディレクトリの互換が切れるので、タグを差し替えるだけでは上がらない (dump と restore が要る)。17 のサポートは 2029-11 まであるので、上げるときは人が手順を組んでやる。", + "description": "PostgreSQL のメジャーはタグの差し替えでは上がらない(dump と restore が要る)。", "matchDatasources": [ "docker" ], @@ -109,7 +109,7 @@ "allowedVersions": "<18" }, { - "description": "Talos と Kubernetes と Gateway API の CRD は自動マージしない。ノードが 1 台なので、Talos を上げることは 唯一のノードの再起動、つまり全停止を意味する。PR は「新しい版が出た」という通知として使い、適用は talosctl upgrade で人がやる (talos/versions.yaml)。 needs-review ラベルを付けて Claude のレビューに乗せる (.github/workflows/claude-code-review.yml)。Gateway API は再起動を伴わないが、**CRD の版が上がると Gateway も HTTPRoute も全部それに乗り換わる**ので、同じく人が見る。**talosctl のイメージ**(etcd スナップショットの CronJob)と手元の talosctl(tools/Dockerfile の `siderolabs/talos`。kubectl は `kubernetes/kubernetes` で入る)も同じ群に入れる ── ノードの版と一緒に上げたいので、片方だけ自動マージで先に進ませない。", + "description": "Talos・Kubernetes・Gateway API は自動マージしない(ノード 1 台で全停止、CRD は全経路が乗り換わる)。適用は人。talosctl のイメージと手元の CLI もノードの版と一緒に上げる。", "groupName": "talos", "groupSlug": "talos", "automerge": false, @@ -124,7 +124,7 @@ ] }, { - "description": "argo-cd と infisical-standalone の chart (bootstrap/{argocd,infisical}/version.yaml) は自動マージしない。ArgoCD は全アプリの入口、infisical は全 Secret の出どころで、壊れると直す道具ごと止まる。PR は通知として使い、talos-boot が VM で起動まで見たうえで人がマージし、render.sh → apply-config → upgrade-k8s で当てる (talos/README.md)。2026-09-08 には SOPS 済みの CR の中の版を平文置換した PR が自動マージされて MAC が壊れた。版はいまは平文の version.yaml にある。", + "description": "argo-cd と infisical の chart は自動マージしない(壊れると直す道具ごと止まる)。当てるのは render.sh → apply-config → upgrade-k8s。", "matchDatasources": [ "helm" ], @@ -140,7 +140,7 @@ ] }, { - "description": "上と同じ扱いを、bootstrap/{argocd,infisical}/values.yaml で上書きしているイメージ (argocd の redis、infisical 本体) にも。chart と同じく machine config に載り、当てるのは render.sh → apply-config → upgrade-k8s。", + "description": "values.yaml で上書きしているイメージも chart と同じ扱い(machine config に載る)。", "matchDatasources": [ "docker" ], @@ -156,7 +156,7 @@ ] }, { - "description": "mirror.gcr.io (Docker Hub の pull-through キャッシュ) から引くイメージは、版を Docker Hub で引く。mirror.gcr.io は取得ならどの tag でも通す (無ければその場で Docker Hub から取ってくる) が、tags/list は誰かが一度取った tag しか返さないので、そこで引くと新しい版が見えない (2026-10-17 に rancher/local-path-provisioner で確かめた)。書き換えるのは tag だけで、ファイルの mirror.gcr.io/… はそのまま残る。", + "description": "mirror.gcr.io の tags/list は誰かが一度取った tag しか返さないので、版は Docker Hub で引く。", "matchDatasources": [ "docker" ], @@ -166,7 +166,7 @@ "overridePackageName": "{{{replace 'mirror.gcr.io/' 'docker.io/' packageName}}}" }, { - "description": "sourceUrl はリリースノートを PR 本文に載せるため。netbird のイメージは OCI ラベル (org.opencontainers.image.source) を持っておらず、Renovate が元リポジトリを辿れないと Release Notes の節ごと欠ける (#85)。netbird は 3 つのイメージが同じリポジトリから出ている (netbird = クライアント、netbird-server = combined サーバ、どちらも netbirdio/netbird)。dashboard だけは別リポジトリで、そちらは OCI ラベルを持っているので書かない。 サーバとクライアントは版を揃えて上げたいので、2 つのイメージを専用の枝 (renovate/netbird) にまとめ、同じ PR で自動マージする (片方だけ先に入らない)。メジャーは共通の設定どおり人を待つ。下の netbird provider の規則が Pulumi.yaml の分を後から上書きするので、こちらに入るのはイメージ (apps/netbird/) だけ。", + "description": "netbird のイメージは OCI ラベルが無くリリースノートが欠けるので sourceUrl を書く。サーバとクライアントは同じ PR で上げる。", "matchDatasources": [ "docker" ], @@ -179,7 +179,7 @@ "groupSlug": "netbird" }, { - "description": "NetBird の provider と、それを包む Pulumi のプラグインは自動マージしない。0.0.x で API が動くうえ、マージは NetBird の実物への up になる。PR の preview の差分を人が見る。**pulumi/Pulumi.yaml の分だけに当てる。** netbirdio/netbird は Docker のイメージ (apps/netbird/) と名前が同じで、絞らないとイメージの更新もこの枝に入る。そうなると枝の全部の更新が pulumi/ の規則 (skipArtifactsUpdate) に当たらなくなり、bun.lock を作る postUpgradeTasks が効かない (#315 のレビュー)。この枝の更新は全部 pulumi/ の規則にも当たる。", + "description": "NetBird の provider は自動マージしない(0.0.x で、マージが実物への up になる)。Pulumi.yaml に絞る(イメージと同名で、混ざると skipArtifactsUpdate が効かない)。", "matchFileNames": [ "pulumi/Pulumi.yaml" ], diff --git a/talos/add-namespace.awk b/talos/add-namespace.awk index 0d2ec5f..02463e6 100644 --- a/talos/add-namespace.awk +++ b/talos/add-namespace.awk @@ -1,12 +1,7 @@ -# helm の描き出しに namespace を補う。 -# chart によっては metadata.namespace を書かない(infisical の Deployment と -# Service が実際にそうだった)。helm / kubectl なら `-n` で決まるが、Talos の -# マニフェスト適用には既定の namespace が無いので default に落ちる。 -# 2026-10-18 に Talos v1.15.0-alpha.1 で外して確かめ直した: infisical の Deployment が default に出て起動しなかった。 -# -# awk -v ns=infisical -f ns.awk < helm-output.yaml +# helm の描き出しに namespace を補う(chart によっては書かず、Talos の適用では default に落ちる)。 +# awk -v ns=infisical -f add-namespace.awk < helm-output.yaml BEGIN { - # クラスタスコープのものには足さない(足すと API が弾く)。 + # クラスタスコープのものには足さない(足すと API が弾く) split("Namespace ClusterRole ClusterRoleBinding CustomResourceDefinition " \ "ValidatingWebhookConfiguration MutatingWebhookConfiguration APIService " \ "StorageClass PriorityClass RuntimeClass IngressClass PersistentVolume " \ diff --git a/talos/patches/apiserver.yaml b/talos/patches/apiserver.yaml index aacff19..14e27d9 100644 --- a/talos/patches/apiserver.yaml +++ b/talos/patches/apiserver.yaml @@ -1,28 +1,16 @@ -# API サーバの公開まわりと、外から入る人・CI の認証。 -# -# なぜ要るか: cilium-drift.yml が GitHub Actions からクラスタを読む(書き込みは無い。 -# 2026-10-17 まで bootstrap-apply.yml が bootstrap/ を当てていたが ArgoCD に移した)。kubeconfig を GitHub に -# 置くとアカウントが落ちた時点でクラスタ全部が落ちるので、**Actions の OIDC トークンを API サーバが直接受ける。** -# 保存する秘密はゼロで、トークンは実行のたびに発行されて数分で切れる。 -# Entra の issuer は Headlamp が利用者本人の id_token を転送してくるぶん(権限の中身は -# bootstrap/apiserver/rbac.yaml)。 +# API サーバの公開名と、外から入る CI(cilium-drift.yml の OIDC トークン)と人(Entra)の認証。 +# 権限の中身は bootstrap/apiserver/rbac.yaml。 --- apiVersion: v1alpha1 kind: KubeAPIServerConfig -# 外から `kubectl --server=https://ks.doany.io:6443` を証明書検証つきで使うために要る。 -# Talos はマシンのアドレス・ホスト名・Kubernetes の名前は自動で入れるが、公開名は入らない。 +# 外から ks.doany.io を証明書検証つきで使うため(公開名は自動では入らない) certExtraSANs: - ks.doany.io -# `service-node-port-range` は既定に戻した(2026-10-18。bootstrap/cilium/values.yaml の `nodePort`)。 --- apiVersion: v1alpha1 kind: KubeAuthenticationConfig -# `configuration` の中身は Kubernetes の AuthenticationConfiguration そのもの -# (apiVersion / kind は Talos が付けるので書かない)。 -# -# `anonymous` を消さないこと。ここを与えると既定を置き換えるので、 -# 書き忘れると kubelet や LB のヘルスチェック(/livez /readyz /healthz)が -# 匿名で叩けなくなり、コントロールプレーンが上がらなくなる。 +# `anonymous` を消さないこと。与えると既定を置き換えるので、書き忘れると +# /livez /readyz /healthz を匿名で叩けずコントロールプレーンが上がらない。 configuration: anonymous: enabled: true @@ -33,39 +21,31 @@ configuration: jwt: - issuer: url: https://token.actions.githubusercontent.com - # ワークフロー側で `&audience=` に同じ値を指定する。既定の audience - # (リポジトリ所有者の URL)のままにしないこと — 他のワークフローと共有されるため + # 既定の audience(所有者の URL)は他のワークフローと共有されるので使わない audiences: - https://ks.doany.io claimMappings: - # `sub` をそのまま使わない。`repo:danything@143234231/gitops@1311609465:ref:…` のように - # 組織 ID とリポジトリ ID が埋め込まれる形に変わっており、形式が変わると RBAC が黙って - # 効かなくなる。要る情報から組み立てる(例: gha:danything/gitops:refs/heads/main)。 + # `sub` は形式が変わると RBAC が黙って効かなくなるので、要る情報から組み立てる username: expression: '"gha:" + claims.repository + ":" + claims.ref' - # issuer は世界中の GitHub Actions が使う公開の IdP。issuer を信じるだけでは誰でも入れる。 - # 効いているのはここで、緩めると即座に穴になる。 + # issuer は世界中の GitHub Actions が使う公開の IdP。効いているのはここで、緩めると即座に穴になる。 claimValidationRules: - expression: 'claims.repository == "danything/gitops"' message: "danything/gitops 以外は受け付けない" - expression: 'claims.ref == "refs/heads/main"' message: "main 以外のブランチからは受け付けない" - # ワークフローのファイルまで固定する。許すのは cilium-drift.yml だけ(権限は cilium-config の get だけ)。 - expression: >- claims.workflow_ref.startsWith("danything/gitops/.github/workflows/cilium-drift.yml@") message: "cilium-drift.yml 以外のワークフローからは受け付けない" - # Entra ID。Headlamp が利用者本人のトークンを API サーバへ転送する(Headlamp 自身は権限を持たない)。 + # Entra ID。Headlamp が利用者本人の id_token を転送してくる。 - issuer: url: https://login.microsoftonline.com/22ac977d-b140-4dc8-8150-90d645d3475b/v2.0 - # oauth2-proxy と同じアプリ登録。**この組で出た id_token はどれもクラスタの資格情報になる** - # (a.doany.io や hl.doany.io の認証で出たものも通る)。分けたくなったら audiences を差し替える。 + # oauth2-proxy と同じアプリ登録なので、この組で出た id_token はどれもクラスタの資格情報になる audiences: - b0fa498f-7e6a-4fe1-a1c6-16fbbb6f397e claimMappings: - # gha: と混ざらないように接頭辞を付ける。 username: expression: '"entra:" + claims.preferred_username' - # アプリロール admin を持つ人だけ entra:admin、それ以外は entra:viewer。 - # ロールが 1 つも無い利用者には roles クレーム自体が無いので has() で守る。 + # ロールが無い利用者には roles クレーム自体が無いので has() で守る groups: expression: '(has(claims.roles) && "admin" in claims.roles) ? ["entra:admin"] : ["entra:viewer"]' diff --git a/talos/patches/cluster.yaml b/talos/patches/cluster.yaml index 764d040..b5112de 100644 --- a/talos/patches/cluster.yaml +++ b/talos/patches/cluster.yaml @@ -1,40 +1,24 @@ -# クラスタ共通。v1.14 では型付きドキュメントと v1alpha1 を併記できないので、 -# 項目ごとに書く場所が決まっている(../docs/talos.md「machine config の作法」)。 +# クラスタ共通。型付きドキュメントと v1alpha1 で書く場所が決まっている(../docs/talos.md「machine config の作法」)。 machine: sysctls: - # これが無いと IPv6 の既定経路が消える。Kubernetes が上がると - # `net.ipv6.conf.{all,bond0}.forwarding` が 1 になり、既定の `accept_ra=1` は - # 「forwarding が有効なら RA を無視する」なのでカーネルが RA 由来の default route を落とす。 - # 2 = forwarding でも RA を受ける。QEMU で再現・修正を確認済み(../docs/talos.md「VM ブートドリル」)。 + # 無いと IPv6 の既定経路が消える(Kubernetes が forwarding=1 にすると accept_ra=1 は RA を無視する) net.ipv6.conf.bond0.accept_ra: "2" - # bond0 の SLAAC の IID を MAC 由来(EUI-64)にしない。 - # 値は 3(random)。stable-privacy(2)は `stable_secret` が要り、未設定のまま 2 を書くと - # `write /proc/sys/net/ipv6/conf/bond0/addr_gen_mode: invalid argument` で - # KernelParamSpecController が数秒おきに失敗し続ける(実測)。 - # 3 は再起動ごとに IID が変わる。ブート間で固定したくなったら `stable_secret` を足して 2 に戻す。 - # (下の autoconf=0 で SLAAC のグローバルアドレスは付けないので、効くのはリンクローカルだけ) + # IID を MAC 由来にしない。2(stable-privacy)は stable_secret が無いと書き込みが失敗し続ける net.ipv6.conf.bond0.addr_gen_mode: "3" - # **bond0 に SLAAC のアドレスを付けない。** 公開する AAAA は下の静的アドレス(::2)のつもりだが、 - # cloudflare-ddns の `local.iface.stable:bond0` は一時アドレス以外から選ぶので、SLAAC のアドレスが - # あるとそちらを書く(2026-10-17 に、AAAA が EUI-64 の SLAAC になっているのを見つけた)。 - # addr_gen_mode=3 だと SLAAC が再起動ごとに変わるので、AAAA も毎回変わってしまう。 - # autoconf=0 はアドレスを作らないだけで、RA の既定経路(上の accept_ra)はそのまま受ける。 + # SLAAC のアドレスを付けない(あると cloudflare-ddns が静的な ::2 でなくそちらを AAAA に書く)。 + # RA の既定経路はそのまま受ける。 net.ipv6.conf.bond0.autoconf: "0" - # Forgejo の Runner(dind)で vite などがファイル監視を使う。既定の 128 は全コンテナの root で - # 共有なので足りず、worklog-cloud の e2e が EMFILE で落ちた(2026-09-15) + # Forgejo の Runner(dind)のファイル監視。既定の 128 では EMFILE で落ちる fs.inotify.max_user_instances: "8192" fs.inotify.max_user_watches: "1048576" - # IPv6 は bond0 に一本化する。eno4 側の RA でデフォルトルートを奪われると非対称ルーティングになる。 + # IPv6 は bond0 に一本化する(eno4 の RA に既定経路を奪われると非対称になる) net.ipv6.conf.eno4.disable_ipv6: "1" network: - # 名前は main 固定。local-path の PV の nodeAffinity が main を指している。 - # インタフェースは下の BondConfig / LinkConfig(型付きドキュメント)に書く。v1alpha1 の - # machine.network.interfaces は bond の MAC を指定できないため(2026-10-17 に移した)。 + # local-path の PV の nodeAffinity が main を指している hostname: main --- -# 10.0 側。**MAC は e6:2c:1b:1a:a0:39 に固定する。** ルーターの IPv6 ホスト公開のエントリがこの MAC を -# 指している(docs/router.md)。指定しないと bond は links の先頭(eno1)の MAC を引き継ぎ、 -# NIC や箱(Z440)を替えた日に変わって、外からの IPv6 が全部止まる(以前、再起動で変わって実際に止まった)。 +# MAC を固定する。ルーターの IPv6 ホスト公開がこの MAC を指していて(docs/router.md)、 +# 指定しないと eno1 の MAC を引き継ぎ、NIC や箱を替えた日に外からの IPv6 が全部止まる。 apiVersion: v1alpha1 kind: BondConfig name: bond0 @@ -46,18 +30,13 @@ miimon: 100 hardwareAddr: e6:2c:1b:1a:a0:39 addresses: - address: 10.0.0.2/24 - # 静的アドレス。プレフィックスは ISP 由来なので変わりうる。 - # 変わったときは cloudflare-ddns(local.iface.stable:bond0)が実際のアドレスを公開する。 + # プレフィックスは ISP 由来で変わりうる(公開は cloudflare-ddns が実際のアドレスで行う) - address: 240f:6d:842b:1::2/64 routes: - # destination を書かないと gateway の側の既定経路になる。 - # IPv6 の既定経路は RA 由来(`default via fe80::… proto ra`)なので書かない。 - # 上の `accept_ra: "2"` とセット。Kubernetes が上がったあとに - # `talosctl get routes -o yaml` で `protocol: ra` の `dst: ""` が残っていることを確かめること - # (クラスタ起動前だけ見ても意味が無い。forwarding が 1 になるのはそのあと)。 + # IPv6 の既定経路は RA 由来なので書かない(上の accept_ra とセット) - gateway: 10.0.0.1 --- -# 10.10 側。DHCP は DHCPv4Config / DHCPv6Config を書かない限り無効(明示の設定が 1 つでもあれば既定の DHCP は止まる) +# 10.10 側。DHCP は明示の設定が無い限り無効 apiVersion: v1alpha1 kind: LinkConfig name: eno4 @@ -74,19 +53,16 @@ podSubnets: - fd42::/64 serviceSubnets: - 10.43.0.0/16 - # /64 は Talos に弾かれる(/108 以下が必須)。 + # /108 以下が必須 - fd43::/108 --- -# HostnameConfig は消して v1alpha1 側に書く。gen config が既定で入れる -# `auto: stable` と `hostname` は併記できず、パッチからは `auto` を消せない -# (`auto: null` も `$patch: replace` も効かない)。下の KubeNodeConfig と同じ手で -# ドキュメントごと落としてから、下の machine.network.hostname を効かせる。 +# gen config が入れる `auto: stable` と `hostname` は併記できず、パッチから `auto` を消せないので +# ドキュメントごと落として machine.network.hostname を効かせる。 apiVersion: v1alpha1 kind: HostnameConfig $patch: delete --- -# ノード自身の名前解決。実機の systemd-resolved と同じ Cloudflare を見る。 -# クラスタ内の名前解決は CoreDNS、LAN 向けの公開リゾルバは AdGuard で、どちらもこれとは別。 +# ノード自身の名前解決(クラスタ内は CoreDNS、LAN 向けは AdGuard で別) apiVersion: v1alpha1 kind: ResolverConfig nameservers: @@ -95,31 +71,15 @@ nameservers: - address: 1.1.1.1 - address: 1.0.0.1 --- -# ノード(containerd)から fj.doany.io のイメージを取るため、名前をノード自身(Gateway)に向ける。 -# 公開 DNS のままだと A が WAN 側の 59.140.229.91 で、ノードからだとルーターで折り返さず届かない(PC からは届く。 -# AAAA のノード自身の IPv6 なら届く)。2026-10-18 に hostNetwork の Pod から確かめた。 -# 2026-09-15〜10-18 は Gateway に届かなかったので中継(apps/forgejo/node-access.yaml の nginx)の ClusterIP に向けていた -# (bootstrap/cilium/values.yaml の `nodePort`)。 +# ノードから fj.doany.io のイメージを取るため自分に向ける(公開 DNS の A は WAN 側で、ノードからは届かない) apiVersion: v1alpha1 kind: StaticHostConfig name: 10.0.0.2 hostnames: - fj.doany.io --- -# 単一ノードなので control-plane に taint を付けない。 -# -# `KubeNodeConfig` は既定で `node-role.kubernetes.io/control-plane: NoSchedule` を入れる。 -# ノードが 1 台しかないこのクラスタでは、toleration を持たない Pod が全部 Pending になる。 -# -# 2026-09-08 の VM ドリルで実際に踏んだ。cilium と CoreDNS は toleration を持っているので上がり、 -# ノードは `Ready` に見えるまま、metrics-server・hubble・local-path-provisioner が -# `no nodes available to schedule pods` で止まっていた。アプリが 1 つも上がらない。 -# -# taints だけを消す方法が無い -# -# `taints` はマップなので、`taints: {}` では消えない(マージなので何も起きない)。 -# 中に `$patch: replace` を書くとキーとして残ってしまい `name "$patch" is invalid` で -# validate が落ちる(実測)。上の HostnameConfig と同じく、 +# 単一ノードなので control-plane の NoSchedule を付けない(toleration の無い Pod が全部 Pending になる)。 +# taints はマップなので `{}` では消えず、`$patch: replace` は validate が落ちる。 # ドキュメントごと消して labels だけ書き戻すのが唯一の手。 apiVersion: v1alpha1 kind: KubeNodeConfig @@ -127,7 +87,7 @@ $patch: delete --- apiVersion: v1alpha1 kind: KubeNodeConfig -# labels は書き戻すこと。消したままだと `kubectl get nodes` の ROLES が `` になる。 +# 消したままだと ROLES が になる labels: node-role.kubernetes.io/control-plane: "" node.kubernetes.io/exclude-from-external-load-balancers: "" diff --git a/talos/patches/cni.yaml b/talos/patches/cni.yaml index f7c7ddc..1d6f165 100644 --- a/talos/patches/cni.yaml +++ b/talos/patches/cni.yaml @@ -1,32 +1,6 @@ -# Cilium を入れるために、Talos の既定の CNI と kube-proxy を止める。 -# -# ROADMAP には「`cni.name: none` と `proxy.disabled: true` にする」と書いてあったが、 -# patches には入っていなかった(2026-09-08 に気づいた)。このまま生成すると -# flannel と kube-proxy が動くクラスタになり、`kubeProxyReplacement: true` の -# Cilium と正面衝突する。 -# -# v1.14 では書き方が違う -# -# v1alpha1 の `cluster.network.cni` はもう書けない。書くと apply が弾く: -# -# * cluster network config in v1alpha1 config (.machine.cluster.network) -# can't be used with KubeFlannelCNIConfig document, please remove it to avoid conflicts -# -# `talosctl gen config` は型付きの `KubeFlannelCNIConfig` と `KubeProxyConfig` を -# 既定で吐くので、そちらを消す/無効にする。 -# -# - `KubeFlannelCNIConfig` に `enabled` は無い。ドキュメントが在ること自体が -# 「flannel を入れる」の意味なので、`$patch: delete` で丸ごと消す -# - `KubeProxyConfig` には `enabled` があるので `false` にする -# -# 生成物で確認済み(2026-09-08): -# -# $ grep -c KubeFlannelCNIConfig out/controlplane.yaml → 0 -# $ grep -A1 "kind: KubeProxyConfig" out/controlplane.yaml -# enabled: false -# $ talosctl validate --mode metal → valid -# -# CoreDNS は止めない(`KubeCoreDNSConfig`)。Cilium は CoreDNS を置き換えない。 +# Cilium のために Talos の既定の CNI(flannel)と kube-proxy を止める。CoreDNS は止めない。 +# v1alpha1 の cluster.network.cni は型付きドキュメントと衝突して書けない。 +# KubeFlannelCNIConfig は在ること自体が有効化なので丸ごと消す。 apiVersion: v1alpha1 kind: KubeFlannelCNIConfig $patch: delete diff --git a/talos/patches/etcd-backup.yaml b/talos/patches/etcd-backup.yaml index c53e663..ef0e498 100644 --- a/talos/patches/etcd-backup.yaml +++ b/talos/patches/etcd-backup.yaml @@ -1,13 +1,5 @@ -# Pod から Talos API を叩けるようにする。etcd スナップショットの日次取得のためだけ。 -# (受け手は ../../apps/k8up/etcd-snapshot.yaml) -# -# 渡す役割は `os:etcd:backup` だけで、namespace も k8up に絞る。Talos は -# `talos.dev/v1alpha1` の ServiceAccount を見て、この役割の talosconfig を -# 同名の Secret に書く。`os:admin` を Pod に置かずに済むのはこの仕組みだけ -# (talosconfig を Infisical に入れる案は、同じ証明書でノードを入れ直せてしまう)。 -# -# **当てても再起動は要らない**が、効くのは apply-config のあと。machine config に -# 最初から入っているので、手順としては何もしなくてよい。 +# etcd スナップショットの日次取得(../../apps/k8up/etcd-snapshot.yaml)のため、Pod から Talos API を叩く。 +# 役割は os:etcd:backup だけ、namespace も k8up に絞る(os:admin を Pod に置かずに済む唯一の仕組み)。 machine: features: kubernetesTalosAPIAccess: diff --git a/talos/patches/main.yaml b/talos/patches/main.yaml index c24c572..06600d3 100644 --- a/talos/patches/main.yaml +++ b/talos/patches/main.yaml @@ -1,28 +1,17 @@ -# 実機(main)だけの設定。 -# -# カーネル引数(intel_iommu=on / iommu=pt)は machine config ではなく Image Factory の schematic に入れている。 -# v1.14 では `machine.install` が UnattendedInstallConfig と衝突して書けないため。 -# schematic: 96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca -# customization.extraKernelArgs: [intel_iommu=on, iommu=pt] -# customization.systemExtensions.officialExtensions: [siderolabs/dvb-pt3, siderolabs/intel-ucode] +# 実機(main)だけの設定。カーネル引数と拡張は machine.install に書けないので schematic に入れている +# (talos/versions.yaml)。 --- apiVersion: v1alpha1 kind: UnattendedInstallConfig provisioning: diskSelector: - # SATA の SSD(Radeon R7、447 GB)。DL360 では Smart Array(hpsa)越しで Linux からは TRAN=sas に見える。 - # Z440 ではオンボードの SATA(AHCI)。どちらも当日 `talosctl get disks` を見てから確定させる。CEL 式。 + # SATA の SSD。箱を替えたら当日 `talosctl get disks` を見てから確定させる match: disk.dev_path == "/dev/sda" wipe: false --- -# PT3 のドライバ(../docs/decisions.md「PT3 チューナー」)。dvb-pt3 extension は earth_pt3 を blacklist して -# 配っているので、ここで明示的に読ませる。読めば /dev/dvb/adapter* が出る。 -# KubeVirt にパススルーする保険(vfio_pci / vfio_iommu_type1)は、ホストのドライバと PT3 を取り合うので外した。 -# -# 復調(tc90522)とチューナー(qm1d1c0042 / mxl301rf)も読ませる。earth_pt3 は probe のときに -# これらを request_module で引くが、Talos にはそれを満たす modprobe が無い。**並べた順には読まれない** -# (名前の順なので earth-pt3 が先頭)ため、それでも `Failed to create FE0` で /dev/dvb/*/frontend0 が -# できない。チューナーが揃ったあとで付け直すのは ../../apps/pt3/rebind.yaml(2026-10-17)。順を守る上流の修正は PR 中。 +# PT3 のドライバ(../docs/decisions.md「PT3 チューナー」)。dvb-pt3 は earth_pt3 を blacklist しているので明示で読ませる。 +# 復調とチューナーも要る(Talos には request_module を満たす modprobe が無い)。並べた順には読まれないので、 +# 付け直しは ../../apps/pt3/rebind.yaml。 machine: kernel: modules: diff --git a/talos/patches/volumes.yaml b/talos/patches/volumes.yaml index 6384c1f..2364f6a 100644 --- a/talos/patches/volumes.yaml +++ b/talos/patches/volumes.yaml @@ -1,15 +1,7 @@ -# ディスクの割り方。既定だと EPHEMERAL (= /var) がディスク全部を取り、etcd も -# コンテナイメージも PV データも同じ 1 枚に同居する。local-path は PVC の容量を -# 強制しないので、1 個の PVC が暴走すると etcd ごと道連れになる。 -# -# ボリュームの設定は「まだ確保されていないとき」にしか効かない。あとから書き足しても -# 黙って無視されるので、直すにはディスクを消して入れ直すことになる。 -# システムボリュームがユーザーボリュームより先に確保されるため、EPHEMERAL に maxSize を -# 書き忘れると grow が全部持っていって取り分が残らない (siderolabs/talos#12713)。 -# -# 64GiB の根拠は実機の使用量 (containerd 22G / etcd 84M / kubelet 925M / log 1.2G)。 -# 上限を切るほうが kubelet のイメージ GC (既定 85%) が現実的なところで効く。 -# 残り約 380 GB が下の local-path 用。 +# ディスクの割り方。既定だと EPHEMERAL(/var)が全部を取り、PVC の暴走で etcd ごと道連れになる。 +# ボリュームの設定は未確保のときにしか効かない(あとから書き足しても黙って無視され、直すには入れ直し)。 +# EPHEMERAL に maxSize が無いと grow が全部持っていく(siderolabs/talos#12713)。 +# 64GiB は実機の使用量(containerd 22G ほか)から。残りが local-path 用。 apiVersion: v1alpha1 kind: VolumeConfig name: EPHEMERAL @@ -19,9 +11,7 @@ provisioning: maxSize: 64GiB grow: true --- -# PV データの置き場。マウント先は Talos が決める (/var/mnt/ 固定) ので、 -# ../manifests/local-path.yaml の paths がここを指している。 -# PVC の容量を強制するわけではなく、溢れてもこのパーティションの中で止まる、が効き目。 +# PV データの置き場。/var/mnt/ 固定で、../manifests/local-path.yaml の paths がここを指す。 apiVersion: v1alpha1 kind: UserVolumeConfig name: local-path @@ -32,12 +22,7 @@ provisioning: grow: true filesystem: type: xfs -# noexec を外す。Talos v1.14 から /var 系は既定で nosuid,nodev,noexec になり、 -# PVC の中の実行ファイルが動かない。PVC から実行するものがある前提 (当初は Mattermost の -# プラグイン。2026-10-06 に撤去。いまは Forgejo が forgejo-repos の各リポジトリの hooks/ に置く -# サーバ側フックを git が exec するはずだが、未検証)。上流も Longhorn / vCluster が -# 同じ理由で壊れたと言っていて、mount.secure がその逃げ道。 -# nosuid と nodev も一緒に外れるが、PVC に書けるのはコンテナだけで、そこは -# PSA と securityContext で押さえているので釣り合うと判断した。 +# /var 系は既定で nosuid,nodev,noexec になり、PVC の中の実行ファイル(Forgejo のサーバ側フック)が動かない。 +# コンテナは PSA と securityContext で押さえている。 mount: secure: false diff --git a/talos/render.sh b/talos/render.sh index 79f02aa..497eed6 100755 --- a/talos/render.sh +++ b/talos/render.sh @@ -1,16 +1,8 @@ #!/bin/sh -# machine config を作る。手で書く部分(patches/)と、他から描いてくる部分 -# (Cilium・local-path-provisioner)を 1 か所で組む。 -# -# 値を二度書かないための道具。Cilium の設定は bootstrap/cilium/values.yaml が -# 正本で、machine config はそこから描いた派生物にする。手で合わせない -# (../docs/decisions.md「machine config と Cilium の chart をどう連動させるか」)。 -# -# ./talos/render.sh talos/clusterconfig -# -# SOPS ファイルは自分で復号する(age の鍵が要る)。CI は鍵を持たないので SECRETS / REGISTRIES などの -# env にダミーを渡し、そのときは復号しない。出力は秘密を含む。出力先は丸ごと消して作り直す。 -# 当て方は talos/README.md「上げ方 / 当て直し方」。 +# machine config を作る: ./talos/render.sh talos/clusterconfig +# chart の値は bootstrap/*/values.yaml が正本。ここに写さない(../docs/decisions.md)。 +# SOPS は自分で復号する。CI は鍵が無いので SECRETS などの env にダミーを渡す。 +# 出力は秘密を含み、出力先は丸ごと作り直す。当て方は talos/README.md。 set -eu OUT=${1:?usage: render.sh } @@ -18,11 +10,8 @@ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" WORK=$(mktemp -d) -# 呼び出し側から渡された SECRETS は消さない。自分で作ったものだけ片付ける。 OWN_SECRETS= -# 最後を `[ -n ... ] &&` で終えないこと。EXIT トラップの戻り値がスクリプトの -# 終了コードになるので、`OWN_SECRETS` が空だとテストが偽になって 1 で落ちる -# (CI が `SECRETS` を渡す場合がまさにそれ。実際に踏んだ)。 +# 最後を `[ -n ... ] &&` で終えない。トラップの戻り値が終了コードになり、空だと 1 で落ちる。 cleanup() { rm -rf "$WORK" if [ -n "$OWN_SECRETS" ]; then rm -f "$OWN_SECRETS"; fi @@ -37,17 +26,13 @@ if [ -z "$SECRETS" ]; then sops -d talos/secrets.yaml > "$SECRETS" fi -# fj.doany.io のイメージ取得の資格情報(talos/README.md「fj.doany.io の資格情報」)。 -# CI には age の鍵を渡さないので、`REGISTRIES` にダミーを入れて経路だけ通す。 REGISTRIES=${REGISTRIES:-} if [ -z "$REGISTRIES" ]; then REGISTRIES=$WORK/registries.yaml sops -d talos/registries.yaml > "$REGISTRIES" fi -# キーを名前で引く。「1 つ目の version」「最後の version」で数えていると、 -# versions.yaml にブロックが増えた瞬間に静かに別の値を掴む。 -# `yq` は使わない(版で構文が違うので、どこで回しても同じに動く awk にしてある)。 +# キーを名前で引く(位置で数えるとブロックが増えたとき別の値を掴む)。yq は版で構文が違うので awk。 ver() { # $1=トップレベルのキー $2=その下のキー awk -v top="$1:" -v key=" $2: " '$0 == top {f=1; next} /^[^ #]/ {f=0} f && index($0, key) == 1 {print substr($0, length(key)+1); exit}' talos/versions.yaml } @@ -55,7 +40,6 @@ TALOS=$(ver talos version) SCHEMATIC=$(ver talos schematic) K8S=$(ver kubernetes version) GWAPI=$(ver gatewayAPI version) -# bootstrap//version.yaml の版(Renovate が上げる) bver() { sed -n 's/^version: //p' "bootstrap/$1/version.yaml"; } CILIUM=$(bver cilium) CERTMGR=$(bver cert-manager) @@ -67,19 +51,13 @@ done echo "talos=$TALOS k8s=$K8S gateway-api=$GWAPI" echo "cilium=$CILIUM cert-manager=$CERTMGR argo-cd=$ARGOCD infisical-standalone=$INFISICAL" -# --- inlineManifest を描く ------------------------------------------------- -# `helm template` の出力をそのまま KubeInlineManifestConfig に包む。 inline() { # $1=name stdin=manifest printf 'apiVersion: v1alpha1\nkind: KubeInlineManifestConfig\nname: %s\nmanifest: |-\n' "$1" sed 's/^/ /' } -# bootstrap// の chart を描いて inlineManifest にする。chart と repo は version.yaml の -# `# renovate:` の行から、値は values.yaml から読む。値をここに写さない(../docs/decisions.md)。 -# namespace.yaml があれば先頭に付ける。chart は Namespace を描かないし、ArgoCD は -# machine config の中にいるので、machine config が最初に流れる時点では誰も作っていない。 -# `updatedAt` は infisical の chart が描いた時刻を Deployment と Pod に付ける注釈(values では消せない)。 -# 残すと描くたびに差分が出て、当て直すたびに infisical が再起動するので落とす。 +# chart と repo は version.yaml の `# renovate:` 行から読む。namespace.yaml は先頭に付ける(最初は誰も作らない)。 +# `updatedAt` は infisical の chart が付ける時刻(values で消せない)。残すと当て直すたびに再起動する。 chart() { # $1=name $2=namespace 残り=helm template に足す引数 _n=$1 _ns=$2 shift 2 @@ -99,26 +77,16 @@ chart() { # $1=name $2=namespace 残り=helm template に足す引数 echo " $_n -> $(wc -c < "$WORK/inline-$_n.yaml") バイト" } -# `--api-versions` は GatewayClass `cilium` のため。chart は Gateway API の CRD が「ある」ときだけ -# GatewayClass を描くが、`helm template` はクラスタを見ないので、渡さないと黙って抜ける。 -# 抜けると Gateway が `Waiting for controller` のまま公開経路が全部死ぬ(2026-10-17 に踏んだ)。 -# 下の needle で GatewayClass があることを見る。 +# `--api-versions` が無いと helm template は GatewayClass を黙って描かず、公開経路が全部死ぬ。 chart cilium kube-system --api-versions gateway.networking.k8s.io/v1/GatewayClass inline local-path < talos/manifests/local-path.yaml > "$WORK/inline-local-path.yaml" -# GitHub Actions(cilium-drift)と人(Entra)の RBAC。認証の設定と同じく、ArgoCD も CI も要らない層に置く -# (締め出されたときに直せる)。bootstrap/argocd/repos.yaml の `bootstrap` は apiserver/ を外している。 -# 認証の側(OIDC の issuer)は patches/apiserver.yaml の KubeAuthenticationConfig。 +# CI と人の RBAC。締め出されても直せるよう ArgoCD に載せない(repos.yaml の bootstrap は apiserver/ を外す)。 inline apiserver-rbac < bootstrap/apiserver/rbac.yaml > "$WORK/inline-apiserver-rbac.yaml" -# 値のうち秘密だけは secret-values.sops.yaml(SOPS)にあり、復号して values.yaml の上に重ねる。 -# CI は鍵を持たないので `ARGOCD_SECRET_VALUES` / `INFISICAL_SECRET_VALUES` にダミーを渡す -# (SECRETS / REGISTRIES と同じ)。 -# -# infisical はここに来るしかない。chart が DB と Redis のパスワードを -# Deployment の平文 env に焼き込むので、ArgoCD の Application には置けない -# (../docs/decisions.md「infisical だけは ArgoCD に移せない」)。 +# 秘密の値は secret-values.sops.yaml を values.yaml に重ねる。infisical は chart がパスワードを +# 平文 env に焼くので ArgoCD に置けない(../docs/decisions.md)。 secret_values() { # $1=name $2=env の上書き if [ -n "$2" ]; then echo "$2"; return 0; fi sops -d "bootstrap/$1/secret-values.sops.yaml" > "$WORK/$1-secret-values.yaml" @@ -130,8 +98,7 @@ INFISICAL_SV=$(secret_values infisical "${INFISICAL_SECRET_VALUES:-}") chart argocd argocd -f "$ARGOCD_SV" chart infisical infisical -f "$INFISICAL_SV" -# ArgoCD の CRD は URL で渡す(理由は bootstrap/argocd/values.yaml の `crds`)。 -# 版は chart の appVersion をそのまま使う。版を 2 つ持つと必ずずれる。 +# CRD は URL で渡す(理由は values.yaml の `crds`)。版は chart の appVersion から取る。 ARGOCD_APP=$(helm show chart argocd/argo-cd --version "$ARGOCD" 2>/dev/null \ | sed -n 's/^appVersion: *//p' | tr -d "\"'") [ -n "$ARGOCD_APP" ] || { echo "ERROR: argo-cd の appVersion を読めなかった" >&2; exit 1; } @@ -146,18 +113,10 @@ url: https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_APP}/manifests/ PATCH done -# cert-manager も bootstrap/ にいる。無いと証明書が 1 枚も発行されず、 -# Gateway の HTTPS リスナーに載せる Secret ができない。 chart cert-manager cert-manager -# SOPS 済みの Secret も入れる。infisical/secrets.sops.yaml は infisical の起動に要る -# (kubeSecretRef)ので、machine config に無いと起動しない。どちらも ArgoCD は復号できない。cloudflare-secret.sops.yaml は -# DNS-01 の資格情報。machine config はもともと秘密の塊なので信頼水準は変わらない。 -# `sops -d … | inline` と繋がないこと。`#!/bin/sh` には `pipefail` が無いので、 -# 復号に失敗しても `set -e` が拾わず、中身が空の KubeInlineManifestConfig が -# 黙って出る。そして下の needle は `inline` が付ける `name:` しか見ないので通ってしまう -# (chart の方は `cilium-operator` のような中身を見ているので気づける)。 -# いったんファイルに落とす。 +# ArgoCD が復号できない Secret(infisical の起動用と DNS-01 用)。`sops -d … | inline` と繋がない: +# pipefail が無いので復号に失敗しても空の manifest が黙って出て、needle(name: だけ)も通る。 inline_secret() { # $1=inline の名前 $2=SOPS ファイル $3=env の上書き if [ -n "$3" ]; then inline "$1" < "$3" > "$WORK/inline-$1.yaml" @@ -169,7 +128,7 @@ inline_secret() { # $1=inline の名前 $2=SOPS ファイル $3=env の上書 inline_secret infisical-secrets bootstrap/infisical/secrets.sops.yaml "${INFISICAL_SECRET:-}" inline_secret cloudflare-secret bootstrap/cert-manager/cloudflare-secret.sops.yaml "${CLOUDFLARE_SECRET:-}" -# cert-manager の CRD も URL で渡す(理由は bootstrap/cert-manager/values.yaml の `crds`)。 +# cert-manager の CRD も URL(理由は values.yaml の `crds`)。 cat >> "$WORK/external-crds.yaml" < "$WORK/gateway-api.yaml" </dev/null echo "wrote $OUT/controlplane.yaml ($(wc -l < "$OUT/controlplane.yaml") 行)" -# 描いたものが本当に入ったか。見るのは render.sh が組み立てた側(版・helm の出力・inline の中身)だけ。 -# patches/ を書き写しただけのキーは見ない。gen config は知らないキーを `unknown keys found during decoding` で -# 落とすので、通れば入っている(v1.15.0-alpha.1 で確かめた。2026-10-18)。逆に helm は知らない値を黙って捨てるので、 -# Cilium の値はここで見る。 -# 生成物では manifest が 1 行のエスケープ文字列になるので、引用符は \" で探す。 +# 描いたものが入ったか。patches/ のキーは gen config が知らないキーで落とすので見ない(helm は黙って捨てる)。 +# 生成物の manifest は 1 行のエスケープ文字列なので、引用符は \" で探す。 for n in "kube-apiserver:$K8S" "ghcr.io/siderolabs/kubelet:$K8S" \ 'name: cilium' cilium-operator KUBERNETES_SERVICE_PORT 'value: \"7445\"' 'cgroup-root: \"/sys/fs/cgroup\"' \ 'kind: GatewayClass' io.cilium/gateway-controller \ @@ -221,21 +173,18 @@ for n in "kube-apiserver:$K8S" "ghcr.io/siderolabs/kubelet:$K8S" \ "cert-manager/releases/download/$CERTMGR/cert-manager.crds.yaml" \ 'kind: Namespace\nmetadata:\n name: cert-manager' \ 'name: infisical-secrets' 'name: cloudflare-secret'; do - # `--` を付けておく(`-` で始まる needle を grep がオプションと取る) grep -qF -- "$n" "$OUT/controlplane.yaml" || { echo "ERROR: '$n' が生成物に無い" >&2; exit 1; } done -# 無いことを見る側。flannel は「ドキュメントが在ること」が有効化の意味(talos/patches/cni.yaml) +# 無いことを見る側(flannel はドキュメントが在るだけで有効になる) for n in KubeFlannelCNIConfig /var/local-path-provisioner; do ! grep -qF -- "$n" "$OUT/controlplane.yaml" || { echo "ERROR: '$n' が生成物に残っている" >&2; exit 1; } done -# CRD は URL 側。入ると argocd は 1.83 MB、cert-manager は 1.30 MB 増える(bootstrap/argocd/values.yaml、 -# bootstrap/cert-manager/values.yaml)。生成物は 1 行のエスケープ文字列になるので、包む前のファイルを見る。 +# CRD は inline に入れない(入ると MB 単位で増える)。包む前のファイルを見る。 for c in argocd cert-manager; do ! grep -q 'kind: CustomResourceDefinition' "$WORK/inline-$c.yaml" \ || { echo "ERROR: $c の CRD が inline に入っている" >&2; exit 1; } done -# Hubble の証明書はクラスタの中で作る(bootstrap/cilium/values.yaml の `hubble.tls`)。chart が Secret を描くのは -# `helm` 方式に戻ったとき +# Hubble の証明書はクラスタ内で作る(cilium/values.yaml の `hubble.tls`) ! grep -q '^ kind: Secret$' "$WORK/inline-cilium.yaml" \ || { echo "ERROR: cilium の inline に Secret(Hubble の証明書)が入っている" >&2; exit 1; } talosctl validate --config "$OUT/controlplane.yaml" --mode metal diff --git a/talos/versions.yaml b/talos/versions.yaml index 6f29564..a2e2242 100644 --- a/talos/versions.yaml +++ b/talos/versions.yaml @@ -1,43 +1,20 @@ -# Talos と Kubernetes の版の出どころ。 -# -# ここは Renovate が書き換える。手で触るのは schematic を変えたときだけ -# (拡張やカーネル引数を足したら Image Factory で新しい ID が出る)。 -# -# 上げること自体は自動化しない(ノード 1 台なので Talos の upgrade は全停止)。Renovate の PR は -# 新しい版が出たという通知として使い、適用は自分の都合のいい時間に手で。 -# 理由と手順は README.md「上げ方 / 当て直し方」。 +# Talos と Kubernetes の版。Renovate が書き換えるが、適用は手で(ノード 1 台なので upgrade は全停止)。 +# 手順は README.md「上げ方 / 当て直し方」。 talos: - # Talos のリリースを追う。Image Factory はどの版でもその場で installer を作るので、 - # リリースされた版はそのまま使える。Factory のレジストリはタグの一覧を返さない (/v2/…/tags/list が 404) - # ので、installer イメージを docker で追う形では Renovate が版を引けなかった (2026-10-07 まで)。 - # 手元の talosctl (tools/Dockerfile) と同じ depName なので、同じ PR で上がる。 - # PT3 のドライバ(dvb-pt3)が v1.15.0-alpha.1 からしか無いので、2026-10-13 から alpha に乗せている - # (../docs/decisions.md「PT3 チューナー」)。いまの版がプレリリースなので、Renovate は次の alpha / beta も PR にしてくる。 + # Factory のレジストリはタグ一覧を返さないので github-releases で追う。tools/Dockerfile の talosctl と同じ depName。 + # dvb-pt3 が v1.15.0-alpha.1 からなので alpha に乗っている(Renovate は次の alpha / beta も出す)。 # renovate: datasource=github-releases depName=siderolabs/talos version: v1.15.0-alpha.1 - # 拡張(dvb-pt3、intel-ucode)とカーネル引数(intel_iommu=on / iommu=pt)を焼き込んだ ID。 - # 中身を変えたら Image Factory で作り直して、この値を差し替える。 + # 拡張(dvb-pt3、intel-ucode)とカーネル引数(intel_iommu=on / iommu=pt)を焼き込んだ Image Factory の ID。 schematic: 96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca kubernetes: - # `gen config` に `--kubernetes-version` で渡すこと。渡さないと Talos の既定 - # (v1.14.0 なら v1.37.0)になり、ここに書いた値は無視される。2026-09-07 の Talos ブート - # ドリルで、生成物が v1.37.0 になっているのを見つけた(render.sh が渡し、needle で見ている)。 - # 上げるのは `upgrade-k8s`(README.md「上げ方 / 当て直し方」)。マイナーを飛ばさないこと。 - # 手元の kubectl は tools/Dockerfile が同じ depName で揃える。 + # 上げるのは upgrade-k8s で、マイナーを飛ばさない。tools/Dockerfile の kubectl と同じ depName。 # renovate: datasource=github-releases depName=kubernetes/kubernetes version: v1.37.1 gatewayAPI: - # Gateway API の CRD。無いと `Gateway` も `HTTPRoute` も `GRPCRoute` も - # 適用できず、公開経路が丸ごと消える。Cilium の chart は CRD を同梱しないので - # (`gatewayAPI.enabled: true` は「CRD は自分で入れてある前提」)、こちらで入れる。 - # - # standard チャンネルで足りる。使っているのは Gateway / HTTPRoute / - # GRPCRoute / ReferenceGrant の 4 つ(GRPCRoute は v1.1 から standard)。 - # standard-install.yaml は TCPRoute / UDPRoute / TLSRoute / ListenerSet / BackendTLSPolicy も含めて - # CRD を 10 個とも持っている(2026-09-08 に中身を確認)。 - # + # Gateway API の CRD(standard チャンネルで足りる)。 # renovate: datasource=github-releases depName=kubernetes-sigs/gateway-api version: v1.6.3 diff --git a/tools/Dockerfile b/tools/Dockerfile index a54d1ff..5a00b13 100644 --- a/tools/Dockerfile +++ b/tools/Dockerfile @@ -1,6 +1,4 @@ -# 手元で使う運用のコマンド一式(tools/README.md)。手元には Docker だけを入れ、版はここで固定する。 -# 版は Renovate が上げる(`# renovate:` の次の ARG。renovate.json の customManagers)。 -# kubectl と talosctl はノードの版と揃えたいので、Renovate では talos の群(自動マージしない)。 +# 手元で使う運用のコマンド一式(tools/README.md)。版は Renovate が `# renovate:` の次の ARG を上げる。 FROM oven/bun:1.4.3 AS bun FROM golang:1.27-trixie AS age @@ -32,9 +30,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl bzip2 git openssh-client less openssl \ && rm -rf /var/lib/apt/lists/* -# **入れるものは全部、公式が出しているチェックサムと照らし合わせる**(2026-10-07)。sops は age 鍵を、infisical は -# 秘密を扱うので、取得元やミラーでの差し替えに気づけるようにする。get は「ファイルを取る」、ok は -# 「チェックサムのファイル(ハッシュ ファイル名 の形)に照らす」。一致しなければ組み立てごと止まる +# 入れるものは全部、公式のチェックサムと照らす(ok は「ハッシュ ファイル名」の形のファイルに照らす) SHELL ["/bin/bash", "-euo", "pipefail", "-c"] RUN a=$TARGETARCH; p=$a; [ "$a" = amd64 ] && p=x64; \ mkdir /tmp/dl && cd /tmp/dl; \ @@ -55,7 +51,7 @@ RUN a=$TARGETARCH; p=$a; [ "$a" = amd64 ] && p=x64; \ ok "$gh/restic/restic/releases/download/${RESTIC_VERSION}/SHA256SUMS" "restic_${RESTIC_VERSION#v}_linux_$a.bz2"; \ get "$gh/pulumi/pulumi/releases/download/${PULUMI_VERSION}/pulumi-${PULUMI_VERSION}-linux-$p.tar.gz"; \ ok "$gh/pulumi/pulumi/releases/download/${PULUMI_VERSION}/pulumi-${PULUMI_VERSION#v}-checksums.txt" "pulumi-${PULUMI_VERSION}-linux-$p.tar.gz"; \ - # kubectl・talosctl・helm の本物は libexec に置く。/usr/local/bin のほうは creds.sh(下) + # kubectl・talosctl・helm の本物は libexec。/usr/local/bin のほうは creds.sh mkdir -p /usr/local/libexec/gitops; \ install -m 755 kubectl /usr/local/libexec/gitops/kubectl; \ install -m 755 "talosctl-linux-$a" /usr/local/libexec/gitops/talosctl; \ @@ -63,22 +59,19 @@ RUN a=$TARGETARCH; p=$a; [ "$a" = amd64 ] && p=x64; \ install -m 755 "sops-${SOPS_VERSION}.linux.$a" /usr/local/bin/sops; \ tar -xzf "cli_${INFISICAL_VERSION#v}_linux_$a.tar.gz" -C /usr/local/bin infisical; \ bunzip2 -c "restic_${RESTIC_VERSION#v}_linux_$a.bz2" > /usr/local/bin/restic; \ - # Pulumi は本体と bun・Node.js の言語ホストだけ。ほかの言語のぶんは 200MB 近いので入れない。 - # bun の言語ホストが無いと runtime: bun のプロジェクトで SDK を作れない (pulumi install。2026-10-07 まで抜けていた) + # Pulumi は本体と bun・Node.js の言語ホストだけ(bun が無いと pulumi install で SDK を作れない) tar -xzf "pulumi-${PULUMI_VERSION}-linux-$p.tar.gz" --strip-components=1 -C /usr/local/bin pulumi/pulumi pulumi/pulumi-language-bun pulumi/pulumi-language-nodejs; \ chmod +x /usr/local/bin/* /usr/local/libexec/gitops/*; \ cd / && rm -r /tmp/dl -# age はチェックサムのファイルを出していない(sigsum の証明だけ)。Go のモジュールとして入れ、Go の公開の -# チェックサムの台帳(sum.golang.org)で中身を確かめる +# age はチェックサムのファイルを出していないので、Go のモジュールとして入れ sum.golang.org で確かめる COPY --from=age /go/bin/age /go/bin/age-keygen /usr/local/bin/ COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun RUN npm install -g "cf@${CF_VERSION}" && npm cache clean --force -# kubectl・talosctl・helm はクラスタに繋ぐときだけ、暗号化した資格情報を tmpfs に戻して渡す(creds.sh) +# クラスタに繋ぐときだけ資格情報を tmpfs に作って渡す COPY --chmod=755 creds.sh /usr/local/bin/creds.sh RUN for t in kubectl talosctl helm; do ln -s creds.sh "/usr/local/bin/$t"; done -# HOME はリポジトリの .home/(compose.yaml) WORKDIR /repo diff --git a/tools/creds.sh b/tools/creds.sh index bbf607c..b0d3a6a 100755 --- a/tools/creds.sh +++ b/tools/creds.sh @@ -1,13 +1,7 @@ #!/bin/sh -# kubectl / talosctl / helm の入口(tools/README.md「クラスタの資格情報」)。本物は /usr/local/libexec/gitops/ にあり、 -# このスクリプトがその名前で呼ばれる。クラスタに繋ぐときだけ資格情報をコンテナの /dev/shm(tmpfs)に作り、 -# KUBECONFIG / TALOSCONFIG で渡す。**平文はディスクに書かない。** /dev/shm はコンテナと一緒に消える(t.ps1 は --rm)。 -# +# kubectl / talosctl / helm の入口(tools/README.md「クラスタの資格情報」)。繋ぐときだけ talos/secrets.yaml の CA で +# 1 日で切れる証明書を作り、/dev/shm(tmpfs)に置いて渡す。平文はディスクに書かない。 # 既定は読み取り用。管理者は `tools/t.ps1 --admin …`(GITOPS_ADMIN=1)のときだけ。 -# kubectl / helm 読み取り用は talos/secrets.yaml の CA でその場で証明書を作る(1 日で切れる)。 -# 管理者も同じく system:masters の証明書をその場で作る(ファイルで持たない) -# talosctl 同じく CA でその場で作る(os:reader / os:admin)。talosconfig は保存しない -# (docs/decisions.md「手で持つもの」) set -eu tool=${0##*/} real=/usr/local/libexec/gitops/$tool @@ -15,12 +9,11 @@ shm=/dev/shm/gitops die() { echo "$tool: $*" >&2; exit 1; } -# 最初の引数らしいもの(フラグを飛ばす)。`kubectl -n x get` だと x を拾うが、そのときは「繋ぐ」側に倒れるだけ -# (helm も「繋がないと分かっているもの」以外は繋ぐ側にしてある)。`--` より後ろはコマンドの引数なので見ない +# 最初の引数らしいもの。`kubectl -n x get` だと x を拾うが、そのときは「繋ぐ」側に倒れるだけ sub= for a in "$@"; do case $a in --) break ;; -*) ;; *) sub=$a; break ;; esac; done -# 繋がないもの(版を見る・描くだけ)と、資格情報を自分で渡しているものはそのまま通す。CI(tools.yml)もここを通る +# 繋がないものと、資格情報を自分で渡しているものはそのまま通す(CI の tools.yml もここを通る) pass() { exec "$real" "$@"; } for a in "$@"; do case $a in @@ -48,14 +41,12 @@ mkdir -p -m 700 "$shm" cd /repo nokey() { [ -s ~/.config/sops/age/keys.txt ] || die "age の鍵が無い(README「Infisical より下の層」の 1 行目で .home に置く)"; } -# talos/secrets.yaml の CA($1 = os か k8s)で、件名 $2 のクライアント証明書をその場で作り、$3 に置く。 -# 鍵 $4(ed25519 / EC)。1 日で切れる。ノードの時計が少し遅れていても通るように 5 分前から有効にする。 -# 作業中は CA の鍵も $w(tmpfs)に出るので、作り終えたらすぐ消す。結果は $3.crt / $3.key / $3.ca +# CA($1 = os か k8s)で件名 $2・鍵 $4(ed25519 / EC)の証明書を作り $3.crt / $3.key / $3.ca に置く。 +# ノードの時計の遅れを見て 5 分前から有効にする。CA の鍵が出る $w はすぐ消す。 mint() { nokey w=$(mktemp -d "$shm/w.XXXXXX") - # `( … ) || …` だと中の set -e が効かないので、終了コードを受けてから見る。パイプは最後の終了コードしか - # 返さないので、復号したものは一度ファイルに出す + # `( … ) || …` だと中の set -e が効かない。パイプは失敗を拾わないので復号はファイルに出す set +e ( set -e @@ -64,7 +55,7 @@ mint() { sops -d --extract "[\"certs\"][\"$1\"][\"$k\"]" talos/secrets.yaml >"$w/ca.$k.b64" base64 -d "$w/ca.$k.b64" >"$w/ca.$k" done - # Talos は ed25519 の鍵に独自の見出しを付けている。中身は PKCS#8 なので見出しだけ直せば openssl が読める + # Talos の ed25519 は独自の見出し。中身は PKCS#8 なので見出しだけ直す sed -i 's/ED25519 PRIVATE KEY/PRIVATE KEY/' "$w/ca.key" case $4 in ed25519) openssl genpkey -algorithm ed25519 -out "$w/c.key" ;; @@ -84,12 +75,10 @@ mint() { [ $rc = 0 ] || { rm -f "$3".*; die "証明書を作れなかった(talos/secrets.yaml を age の鍵で開けるか)"; } } -# 名前は talos/render.sh の gen config と同じ(doany)。行き先は作業用 PC の LAN にいるノードの口 10.10.0.4。 -# render.sh の 10.0.0.2 はクラスタの LAN で、手元からは届かない。NetBird はクラスタの上で動くので、 -# クラスタが落ちたときに使えない。10.10.0.4 は Talos の証明書(apid・kube-apiserver)の SAN に入っている +# 作業用 PC の LAN 側のノードの口(10.0.0.2 は手元から届かず、NetBird はクラスタが落ちると使えない) ep=10.10.0.4 -# talosconfig を $shm に作って置き場所を返す($1 = admin / readonly)。作るのは初回だけ +# talosconfig を作って置き場所を返す($1 = admin / readonly) talosconfig() { t=$shm/talosconfig-$1 if [ ! -s "$t" ]; then @@ -111,12 +100,10 @@ fi out=$shm/kubeconfig-$role if [ ! -s "$out" ]; then if [ $role = admin ]; then - # 管理者は system:masters(Talos の talosctl kubeconfig と同じグループ)。talosctl kubeconfig は 1 年もつ - # 証明書を毎回出すので使わず、読み取り用と同じく 1 日で切れるものを作る + # talosctl kubeconfig は 1 年もつ証明書を出すので使わない subj=/O=system:masters/CN=tools-admin else - # 読み取り用は bootstrap/apiserver/rbac.yaml の entra:viewer(組み込みの view + readonly-extra。Secret は読めない)。 - # Headlamp で Entra の viewer が持つ権限と同じにするため、同じグループ名を使う + # Headlamp の Entra の viewer と同じ権限(bootstrap/apiserver/rbac.yaml) subj=/O=entra:viewer/CN=tools-readonly fi mint k8s "$subj" "$out.c.$$" ec diff --git a/tools/t.ps1 b/tools/t.ps1 index 7e99a79..30c0ae2 100644 --- a/tools/t.ps1 +++ b/tools/t.ps1 @@ -5,13 +5,9 @@ # tools/t.ps1 --admin kubectl apply -f x # 書くときだけ管理者 (tools/README.md「クラスタの資格情報」) # 引数が無ければシェルに入る。Dockerfile を変えたら次の実行で作り直す (変わっていなければキャッシュで一瞬)。 # -# wslc の制限: -# - ネットワークは host にできない。ブラウザのログインは手元の localhost:<ポート> に結果を送ってくるので、 -# infisical login のときだけ、そのポートで待ち受けてコンテナの中の CLI に渡す (下の Start-LoginRelay)。 -# cf auth login には対応していない -# - uid の指定はしない (Windows のファイルに持ち主の uid は無い) -# 5.1 でも読めるように、このファイルは BOM 付きの UTF-8 にする (BOM が無いと 5.1 は Shift_JIS として読み、 -# 日本語のコメントで構文が壊れる)。文字列は ASCII だけにする +# wslc はネットワークを host にできないので、infisical login のときだけブラウザの戻りを中継する (Start-LoginRelay)。 +# cf auth login は未対応。uid は指定しない (Windows のファイルに持ち主の uid は無い)。 +# 5.1 でも読めるように BOM 付きの UTF-8 にする (無いと Shift_JIS と読まれ構文が壊れる)。文字列は ASCII だけ $ErrorActionPreference = 'Stop' $root = Split-Path $PSScriptRoot $image = 'gitops-tools:latest' @@ -26,8 +22,7 @@ if (-not (Test-Path $home_)) { $build = wslc image build --tag $image --progress quiet (Join-Path $root 'tools') 2>&1 if ($LASTEXITCODE -ne 0) { $build | Write-Host; throw "tools: image build failed ($LASTEXITCODE)" } -# infisical login (ブラウザ方式) の中継。コンテナの中で CLI が待ち受けるポートを見つけ、手元の同じポートで -# ブラウザからの POST を受けて、wslc exec の curl でコンテナの中の CLI に渡す。 +# infisical login (ブラウザ方式) の中継。コンテナの中の CLI が待ち受けるポートを手元でも受け、wslc exec の curl で渡す。 # 受け付けるのはログイン先 ($origin) からの POST / OPTIONS だけ。リクエストの値はコマンドに埋め込まない function Start-LoginRelay($name, $origin) { Start-Job -ArgumentList $name, $origin -ScriptBlock { @@ -110,12 +105,9 @@ function Start-LoginRelay($name, $origin) { } } -# ボリューム・作業場所・環境変数は compose.yaml の tools に書いてある (wslc-compose run で動かす)。 -# 標準入力はつなぐ。パイプやリダイレクトのときは TTY を付けない (-T)。 -# このスクリプトにパイプしたもの (`x | tools/t.ps1 kubectl apply -f -`) は標準入力ではなく $input に来るので、下で流し直す -# (流さないとコンテナには何も届かない) -# 標準入力が NUL か (リダイレクトされた文字デバイス = FILE_TYPE_CHAR)。ファイル (FILE_TYPE_DISK) や -# パイプ (FILE_TYPE_PIPE) からの入力は中身があるので NUL とは扱わない。端末はリダイレクトされていないので見ない +# 設定は compose.yaml の tools。パイプやリダイレクトのときは TTY を付けない (-T)。 +# パイプしたものは標準入力でなく $input に来るので下で流し直す。 +# Test-NulStdin: 標準入力が NUL (FILE_TYPE_CHAR) か。ファイルやパイプからの入力は NUL と扱わない function Test-NulStdin { if (-not [Console]::IsInputRedirected) { return $false } if (-not ('GitopsTools.Kernel32' -as [type])) { @@ -134,8 +126,7 @@ if ($rest.Count -and $rest[0] -eq '--admin') { $opts += '-e', 'GITOPS_ADMIN=1' $rest = @($rest | Select-Object -Skip 1) } -# 引数が 1 つのときも配列にする (if の値は 1 要素の配列が中身に展開され、$cmd[0] が 1 文字目になる。 -# 2026-10-07 まで `'x' | tools/t.ps1 cat` が `c: not found` で落ちていた) +# 1 つのときも配列にする (でないと $cmd[0] が 1 文字目になる) $cmd = @(if ($rest.Count) { $rest } else { 'bash' }) $relay = $null @@ -156,8 +147,7 @@ if ($browserLogin) { try { $compose = Join-Path $root 'compose.yaml' if ($piped) { - # PowerShell からネイティブのコマンドに流すと、改行が CRLF になり、5.1 では日本語が ? に化ける。 - # 行を LF でつないだ UTF-8 を base64 (ASCII だけ) にして渡し、コンテナの中で戻してからコマンドに渡す + # ネイティブに流すと CRLF になり 5.1 では日本語が化けるので、LF の UTF-8 を base64 で渡す $text = (@($input) -join "`n") + "`n" $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($text)) # 7.3 より前 (5.1 も) は引数の中の " をネイティブのコマンドに渡すときにエスケープしないので、自分でする @@ -165,10 +155,7 @@ try { if ($PSVersionTable.PSVersion -lt [version]'7.3') { $wrap = $wrap.Replace('"', '\"') } $b64 | wslc-compose --file $compose run @opts tools sh -c $wrap sh @cmd } elseif (Test-NulStdin) { - # 標準入力が NUL のところ (Claude Code のツールなど) では、wslc-compose run がつなぐ標準入力が無効なハンドルになり、 - # 1 秒ほど以上かかるコマンドの出力が落ちて ERROR_INVALID_HANDLE で終わる (2026-10-07)。NUL は中身が無いので、 - # 空のパイプに置き換えても失うものは無い。ファイルや外のパイプからの入力 (`echo x | pwsh -File tools/t.ps1 cat`) は - # NUL ではないので、下で今までどおりつなぐ + # 標準入力が NUL だと wslc-compose run の出力が落ちて ERROR_INVALID_HANDLE で終わる。空のパイプに置き換える @() | wslc-compose --file $compose run @opts tools @cmd } else { wslc-compose --file $compose run @opts tools @cmd