さくらのクラウド上に Talos Linux で Kubernetes クラスタを立てる、とりあえずの一式。 ictsc-drove の構成をベースに、 OS のプロビジョニング (Ansible + kubeadm) を Talos に置き換えたもの。
ictsc-k8s-dev/
│
├── terraform/ (さくらのクラウドのリソース定義。provider は sacloud/sakura)
├── talos/ (Talos の machine config パッチと生成スクリプト)
├── omni/ (self-hosted Omni のVM・Compose・移行runbook)
├── manifest/ (Kubernetes マニフェスト。Argo CD の app-of-apps)
│ ├── base/ (apps / infra の Application・values・workload)
│ ├── envs/ (env ごとの values・patch・クラスタリソース)
│ └── scripts/ (env 固有の values / resources を terraform output から生成する)
│
├── aqua.yaml (CLI のバージョン固定)
└── Taskfile.yaml (タスク定義)
- OS: Talos Linux
v1.13.9(nocloud イメージ) - Kubernetes:
v1.36.4/ CNI は Cilium (kube-proxy 置き換え) - IaC: Terraform (
sacloud/sakurav3.12.7) + Terraform workspace でdev/prodを分離 - GitOps: Argo CD (app-of-apps)
- 監視: Prometheus / Alertmanager / Grafana、Grafana Alloy + Loki(Pod ログを 7 日保持)
- ストレージ: NFS CSI(既定)+ Longhorn(dev、worker 3台の専用SSDへ3レプリカ)
- 入口: Cilium Gateway API + cert-manager (Let's Encrypt / HTTP-01)
- 認証: Dex (GitHub) + oauth2-proxy を Gateway API の ExternalAuth で前段に置く
- ノードのスペック (
terraform/vars.tf)- control plane / 踏み台: 2コア / 4GB / ディスク 40GB
- worker (dev): 6コア / 12GB / ディスク 40GB
- 以前このドキュメントには「使えるサーバスペックの上限は 2コア / 4GB」と
書いてあったが誤り。2026-08-29 に tk1a で 6コア / 12GB (
cloud/plan/6core-12gb) の worker 3台が問題なく起動することを確認済み。上限で悩んだら実際に apply して試すこと
| セグメント | 用途 |
|---|---|
ルータ+スイッチ (sakura_internet, dev / prod とも /27) |
全ノードの eth0。API VIP と LoadBalancer IP もここ |
vSwitch 192.168.100.0/24 (sakura_vswitch) |
全ノードの eth1。etcd のピア通信 |
グローバルIPの割り当て (terraform/locals.tf):
ip_addresses[0 .. cp-1] control plane
ip_addresses[cp] Kubernetes API VIP (Talos の shared VIP)
ip_addresses[cp+1] Ingress VIP (Cilium LB IPAM + L2 Announcement)
ip_addresses[cp+2 .. cp+1+wk] worker node
ip_addresses[cp+2+wk] 踏み台
必要なグローバルIP数は cp + worker + 3 (VIP 2個 + 踏み台 1個)。
netmask は後から変更できない (terraform/vars.tf の WARNING を参照) ので余裕を持たせている。
Talos API(50000/tcp)は踏み台限定だが、CPのKubernetes API(6443/tcp)は 送信元制限なしでTLS・認証・RBACにより保護する。VIPだけが公開対象ではない。 prodの通常のkubectlはOmni API proxyを使用し、上記の予約API VIPが稼働しているとは仮定しない。 経路と実測結果は API公開方針 を参照。
複数拠点への worker 配置については Omni / KubeSpan 設計案 を参照(未適用)。
dev の各 worker に20GiB SSDを1本追加し、Longhorn v1.12.1のV1 Data Engineで
3レプリカの分散ブロックストレージとして使う。Talos側では専用ディスクを
UserVolumeConfig でXFSに初期化し、/var/mnt/longhorn にマウントする。
Longhornには iscsi-tools と util-linux-tools が必要なため、
talos/schematic.yaml に両system extensionを定義している。既存クラスタへ導入する順序は次の通り。
# 専用SSDの作成・接続と、最新machine configの生成
$ task select-dev
$ task apply
# worker用configを次回再起動時の適用としてステージ
$ task stage-worker-config
# 拡張入りImage Factory installerで全ノードを1台ずつ更新する。
# ステージしたworker configもこの再起動で有効になる。
$ task upgrade-talos TO=v1.13.9その後、このリポジトリをpushするとArgo CDが longhorn-system にLonghornを導入する。
longhorn StorageClassは作成するが既定にはせず、既存の nfs を既定のまま維持する。
Longhornを使うPVCは storageClassName: longhorn を明示すること。
さくらのクラウドの「ディスクの修正」(disk_edit_parameter) は Talos に対応していない。
そこで ノードごとの machine config を cloud-init NoCloud (cidata) の ISO にして
CD-ROM としてアタッチしている。Talos の nocloud プラットフォームは
ラベル cidata のブロックデバイスから user-data を読むので、これで静的IPごと設定が入る。
そのため apply は 2 段構えになっている:
sakura_internetだけ先に作ってグローバルIPを確定させる- その出力から machine config と ISO を生成する
- 残り (アーカイブ・CD-ROM・ディスク・サーバ) を作る
task apply がこの順番を面倒みる。
Note
CD-ROM はあくまで初回ブートストラップ用。Talos は初回起動時に machine config を
STATE パーティションへ保存するので、構築後の設定変更は talosctl apply-config で行うこと。
Talos / Kubernetesの構成・アップグレード・ノード交換を集約するため、クラスタ外の
専用VMでself-hosted Omniを動かす。既存のdevクラスタは再構築せず、Omniのcluster
importで移行する。import直後は locked のまま差分をレビューし、明示的なunlock後に
管理を移管する。
移行中はcidataと従来のtalosctl経路を巻き戻し用に維持する。unlockと健全性確認が
終わるまで、既存のmachine config、PKI、CD-ROMを削除しない。構築と移行の詳細は
omni/README.md を参照。
CLI のバージョンは aqua で固定している (aqua.yaml)。
ただし ./init.sh を動かすのに必要な aqua / task / direnv だけは、
aqua より前に手で入れる必要がある (init.sh が task init を叩くため)。
aqua 管理外なのは3つだけ。
| ツール | なぜ aqua 管理外か |
|---|---|
aqua |
以降のツールを入れる本体 |
direnv |
シェルの hook を rc ファイルに仕込む必要があり、リポジトリ単位の管理に馴染まない |
zstd |
aqua の標準レジストリに無い。talos/scripts/fetch-image.sh が Talos イメージの展開に使う |
task も init.sh より前に必要だが、aqua.yaml に入れてあるので aqua install で入る
(下記の手順を参照)。別途インストールしなくてよい。
$ brew install aqua direnv zstd./init.sh が全部やる ので、通常は手で入れるものは無い。中でやっているのは:
direnv/zstdをディストリのパッケージから導入 (apt / dnf / pacman / zypper)aquaを GitHub Releases から取得 (チェックサム検証つき、~/.local/binへ配置).bashrc/.zshrcに PATH と direnv hook を追記 (既に書いてあれば触らない)aqua install->task init
Note
go install github.com/aquaproj/aqua/v2/cmd/aqua@latest でも入るが、
ツールチェーンの再取得とビルドで数 GB の一時領域を使うため、/tmp が
小さい VM では no space left on device で落ちる。init.sh は
ビルドしない配布バイナリを取りに行く。
Warning
空き容量に注意。 このリポジトリは Talos の raw イメージ (約 4GB) を
ダウンロードしてさくらへアップロードする。aqua が入れるツール類も数百 MB ある。
init.sh は起動時に /tmp と $HOME の空きを確認して警告する。
手で入れたい場合はこれでよい (arm64 なら aqua_linux_arm64.tar.gz)。
$ mkdir -p ~/.local/bin
$ curl -sSfL https://github.com/aquaproj/aqua/releases/download/v2.62.3/aqua_linux_amd64.tar.gz \
| tar -xz -C ~/.local/bin aqua
$ export PATH="$HOME/.local/bin:$PATH"aqua と direnv はどちらもシェルの設定が要る。.zshrc (bash なら .bashrc) に:
# zsh の場合 (.zshrc)
export PATH="$HOME/.local/bin:$PATH" # aqua 本体をここに置いた場合
export PATH="${AQUA_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/aquaproj-aqua}/bin:$PATH"
eval "$(direnv hook zsh)"# bash の場合 (.bashrc)
export PATH="$HOME/.local/bin:$PATH" # aqua 本体をここに置いた場合
export PATH="${AQUA_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/aquaproj-aqua}/bin:$PATH"
eval "$(direnv hook bash)"2行の PATH は役割が違う。
| PATH | 中身 |
|---|---|
$HOME/.local/bin |
aqua 本体の置き場 (init.sh はここに置く) |
${AQUA_ROOT_DIR:-...}/bin |
aqua が入れるツール (task / terraform / kubectl など) |
Note
PATH は $(aqua root-dir) と書いてもよいが、それだと aqua 自身が先に PATH に
通っている必要がある。上の ${AQUA_ROOT_DIR:-...} 形式なら aqua を実行せずに
展開できるので、go install で入れた直後でもそのまま動く。
設定を反映したら、リポジトリ直下で aqua install を先に叩く。ここで task が入る。
$ aqua install
$ task --version # aqua 側の task が引ければ OKこれで「必要なツール」は揃うので、あとは ./init.sh に進める。
./init.sh (中身は task init) が aqua install まで面倒をみるので、通常は個別に叩かなくてよい。
$ aqua install| ツール | バージョン | 備考 |
|---|---|---|
talosctl |
v1.13.8 | Taskfile.yaml の TALOS_VERSION と揃える |
terraform |
v1.13.2 | terraform-ictsc-net (CI が 1.13.2 固定) と tfstate を共有するため揃える |
tflint |
v0.64.0 | |
kubectl |
v1.36.3 | terraform/vars.tf の kubernetes_version と揃える |
helm |
v3.21.3 | Argo CD の repo-server が内蔵する Helm に合わせて v3 系に固定 |
gh |
v2.97.0 | task argocd-repo-key が deploy key の登録に使う。gh auth login 済みであること |
task |
v3.52.0 | |
jq |
1.8.2 | |
envsubst |
v1.4.3 | task init-env が .envrc.tmpl を埋めるのに使う (a8m/envsubst) |
このリポジトリは direnv 前提です。API キーやバケット名は .envrc に置き、
リポジトリに入った時点で direnv が自動で読み込む。.envrc は gitignore してあるので、
メンバーそれぞれが自分の手元で用意する(./init.sh が対話で聞いて、
リポジトリに入っている .envrc.tmpl を envsubst で埋めて作ってくれる)。
秘密情報を task が対話で聞き直すことはしない。task は常に環境変数を見るので、
新しい値が要るようになったら .envrc.tmpl にプレースホルダを足す。
作り直したいときは task reset-env。
.envrc に入る値は .envrc.tmpl を参照。内訳はこう:
| 変数 | 用途 |
|---|---|
KUBECONFIG / TALOSCONFIG |
kubectl / talosctl の設定をリポジトリ内に閉じる |
SAKURACLOUD_ACCESS_TOKEN / _SECRET |
さくらのクラウド API キー |
SAKURACLOUD_ZONE |
サーバを建てるゾーン (tk1b = 東京第2) |
TF_STATE_BUCKET / TF_CLI_ARGS_init |
tfstate 置き場 (さくらのオブジェクトストレージ) |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
同上のアクセスキー |
AWS_REQUEST_CHECKSUM_CALCULATION |
さくらのオブジェクトストレージ側の不具合のワークアラウンド |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
Dex が使う GitHub App。task auth-secrets が読む |
ACME_EMAIL |
Let's Encrypt の通知先。task render-env-values が読む |
terraform の backend "s3" ブロックは変数展開ができないので、バケット名は
TF_CLI_ARGS_init 経由で terraform init に渡している。
direnv が効いていれば terraform init を素で叩くだけでよい。
Warning
direnv が効いていないと terraform init が Enter a value: でバケット名を聞いてくる。
direnv allow を実行したか、シェルに direnv の hook を入れたかを確認すること。
$ direnv allow
$ echo $TF_CLI_ARGS_init # -backend-config=bucket=... が出れば OKSAKURACLOUD_ZONE はサーバを建てるゾーン (tk1b = 東京第2)。
tfstate を置くオブジェクトストレージは石狩 (s3.isk01.sakurastorage.jp / jp-north-1) で、
こちらはサーバのゾーンとは独立している。ゾーンを変える場合は
terraform/vars.tf の nameservers (ノードが引く再帰DNS) も合わせて変えること。
# 0. ブートストラップ用のツール (「必要なツール」を参照)
$ brew install aqua go-task direnv zstd
# 1. 初期化 (aqua install / .envrc の作成 / terraform init)
$ ./init.sh
# 2. ワークスペースを選ぶ
$ task select-dev
# 3. 構築 -> ブートストラップ -> CNI/Argo CD まで一気に
$ task uptask up の中身は以下と同じ。詰まったら個別に叩ける。
| task | 内容 |
|---|---|
task fetch-talos-image |
Image Factory から nocloud の raw イメージを取得 |
task plan-omni |
Omni専用VMのplanを表示(作成しない) |
task apply-omni |
Omni専用VMを作成(有料リソース) |
task configure-omni |
VM上にOmni + Dexを構成 |
task test-omni-infra-provider |
さくらのクラウド用Omni infra providerをテスト |
task configure-omni-infra-provider |
Omni VMでinfra providerを起動 |
task omni-import-dry-run |
devクラスタのOmni importを踏み台からdry-run |
task apply |
Terraform でインフラを構築 (上記 2 段構えを内包) |
task talos-bootstrap |
talosctl bootstrap で etcd を初期化 |
task kubeconfig |
kubeconfig を .kube/config に取得 |
task render-env-values |
terraform output を manifest の env 固有値に反映 |
task bootstrap-cluster |
Cilium と Argo CD を helm で入れる |
task argocd-repo-key |
deploy key を作って GitHub に登録し、Argo CD に持たせる |
task auth-secrets |
Dex / oauth2-proxy / Argo CD の OIDC 用 Secret を作る |
task start-gitops |
app-of-apps のルートを適用して Argo CD に引き継ぐ |
task health |
talosctl health でクラスタの健全性を確認 |
task auth-secrets は .envrc の GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET を読む。
GitHub App (または OAuth App) は ./init.sh より先に用意しておくこと
(「認証 (Dex + oauth2-proxy)」を参照)。未設定なら task はその場で落ちるので、
.envrc に足して direnv allow するか task reset-env でやり直す。
argocd-repo-key は gh で deploy key を登録するので、リポジトリの admin 権限が要る。
どちらも作成済みなら status 判定でスキップされる。
manifest/baseは利用者向けworkloadとクラスタ機能に分ける。ApplicationとHelm valuesは
コンポーネント単位で同じディレクトリに置く。
manifest/base/
├── apps/
│ ├── httpbin/
│ └── regalia/
└── infra/
└── alloy/
├── application.yaml
└── values.yaml
Helm chart外の追加リソースは同じアプリ配下のresources/に置く。
Lokiのalert ruleはconfigMapGeneratorでConfigMapにし、Loki Applicationの
Git sourceからHelm chartと一緒に同期する。
dev / prod の kustomization.yaml は manifest/base を読み、環境固有valuesが必要な
Applicationだけpatches で参照を追加する。Alloyのように差分がないものはpatch不要。
root Applicationは参照先が異なるため、manifest/root-dev.yaml と root-prod.yaml に分ける。
外部 Git リポジトリのマニフェストは、Application の spec.source で指定する。
source:
repoURL: git@github.com:owner/repository.git
targetRevision: main
path: deploy/overlays/dev追加した Application は manifest/base/kustomization.yaml に登録する。
private GitHub リポジトリなら、Argo CD へ read-only deploy key も登録する。
$ task argocd-repo-key REPO=owner/repositorydev / prod で targetRevision を分ける場合も、各envのpatchesで上書きする。
Application YAML は手で編集する。task render-env-values が更新するのは、先頭に生成コメントが
ある values/ と resources/ だけ。未構築の環境では生成ファイルがまだ存在しないため、
その環境の root Application は task up で生成を済ませてから適用する。
RegaliaのKubernetesリソースとデプロイ中のimage versionは
manifest/base/apps/regaliaで管理する。ictsc-regaliaのActionsはimageの発行までとし、
Argo CD Image Updaterが対応するoverlayのbackend/frontendをGit write-backで更新する。
main push -> dev へ sha-<commit>
v1.2.3 tag -> prod へ v1.2.3
Image UpdaterはArgo CDに登録済みのこのリポジトリのSSH認証情報を再利用する。
dev は codex/openapi-contract-rebuild の OpenAPI 版を試験配置している。
設定は manifest/base/apps/regalia/workload/overlays/openapi-dev。イメージを
preview-openapi-20260916-3 タグのdigestで固定し、試験中は dev の ImageUpdater を無効化する。
- 参加者: https://contest.k8s-dev.ictsc.net/
- 管理画面: https://contest.k8s-dev.ictsc.net/admin/ (旧 admin-contest ホストから転送)
- 両サイトとも専用 oauth2-proxy が Dex の
ictsc:ictsc2026グループを要求する。 セッションCookieは_regalia_previewで、他の基盤アプリと分離する。 - Discord認証は実接続。参加者は
ICTSC2026 予選ギルドと対応チームロールを要求し、 招待コード入力を省略する。管理者は同ギルドのICTSC2026 Staffロールを要求する。 参加者側ではチームロールを優先し、Staffのみの場合は管理画面へ自動で移動する。 管理者専用入口ではStaffロールで認証する。代理ログインは参加者セッションの確認後に移動する。 ICTSC_DEV_FAKE_MODE=trueはGitHubコンテンツ同期・SStateを無効にするため維持する。 Discord資格情報はESOのdiscord-oauth-clientSecretから参照する。- 新DBは
ictscore_openapi、Redis DBは2。仮認証セッションのDB1と旧版のDB0は使用しない。 旧DBictscoreは保持し、旧参加者・招待コードは新版に移行しない。 - 対応表は
config/regalia/dev-discord-teams.yaml(48チーム、コード2〜49)。 IDはDiscordロール名で照合済み。団体・人数上限は開発用の初期値。python3 scripts/regalia/render-discord-teams.py(PyYAML必要)で公開設定を生成し、 差分をコミットする。チーム本体は管理APIに保存し、初回登録にはpython3 scripts/regalia/import-teams.pyを使用する。既存データの不一致は上書きせず停止する。 通常の編集・メンバー確認は/admin/teamsを使用する。 - 参加者Cookieは
regalia-user-sessionを使用する。旧user-sessionの/apiスコープは ログイン・代理ログイン時に削除し、同名CookieのPath競合を解消する。 - CNPG Databaseリソースで新DBを管理(削除時 retain)。PreSync Job が初期schemaと
team color列を適用する。SQLはブランチの
backend/db/migrationsから取得したもの。 - 旧版に戻す場合は Argo CD source を
workload/overlays/devに戻す。 新DBに旧版マイグレーションを適用しない。
Warning
terraform apply がサーバ作成中にタイムアウトや 409 で落ちたら、孤児サーバを疑うこと。
ゾーンが混んでいるとサーバ作成に数十分かかることがあり、さくら側では作成が完了して
いるのに Terraform が ID を state に書く前に落ちる場合がある。再 apply すると
res_already_connected (ディスクが state 外のサーバに繋がっている) で必ず失敗する。
作り直す必要はない。terraform state show sakura_disk.worker_node[N] の server_id が
孤児サーバの ID なので、実物を API で確認したうえで import すればよい。
$ terraform state show 'sakura_disk.worker_node[2]' | grep server_id
$ terraform import 'sakura_server.worker_node[2]' <その server_id>
$ terraform plan # computed 属性と timeouts だけの差分になるので apply して落ち着かせるNote
manifest/envs/<env>/resources/ は bootstrap-cluster では撒かない。
ClusterIssuer や Certificate は cert-manager の CRD を必要とし、ExternalAuth 付きの
HTTPRoute は oauth2-proxy より後でなければならないため、順序は Argo CD の
sync-wave に任せている。
Note
talosctl health は「全 k8s ノードが Ready」「全 Pod が Running」まで待つ。
CNI は bootstrap-cluster で後入れするので、それより前に走らせると絶対に成功せず
--wait-timeout ぶん待たされる。そのため talos-bootstrap では etcd の起動確認までに留め、
本来の health チェックは CNI 投入後の task health に分けてある。
# Argo CD の初期パスワード
$ task argocd-passwordGrafana の Explore でデータソース Loki を選ぶと、Alloy が収集した全 namespace の
Pod ログを検索できる。代表的な LogQL は次のとおり。
{namespace="kube-system"}
{namespace="argocd", container="argocd-server"} |= "error"
{app="httpbin"} | json
付与するラベルは cluster / namespace / pod / container / node / app / job。
ログは Loki の NFS PVC (5Gi) に保存し、7 日を過ぎたデータを削除する。Alloy は
Kubernetes API 経由で Pod ログを読むため、Talos 自体のサービスログは収集対象外。
さくらのクラウドに DNS ゾーンは作っていない。ictsc.net は Cloudflare で管理していて、
VIP のレコードは terraform-ictsc-net の
k8s-records.tf がこのリポジトリの tfstate を terraform_remote_state で読んで生成する。
| レコード | 参照する output |
|---|---|
k8s-dev.ictsc.net (A) |
ingress_vip |
*.k8s-dev.ictsc.net (CNAME) |
― (k8s-dev.ictsc.net を向く。argocd.k8s-dev などを拾う) |
k8s.k8s-dev.ictsc.net (A) |
api_vip |
VIP は sakura_internet を作った時点 (task apply の1段目) で確定するので、
クラスタの構築完了を待たずに terraform-ictsc-net を apply してよい。
# ictsc-k8s-dev 側で apply したあと
$ cd ../terraform-ictsc-net && terraform applyNote
terraform-ictsc-net 側のオブジェクトストレージのキーには、
TF_STATE_BUCKET (既定 ictsc-void-k8s-dev) の READ 権限が要る。
DNS のレコードを作る前に動作確認したいときは、/etc/hosts に貼る。
$ task hosts
# --- ictsc-dev (terraform output hosts_entries) ---
203.0.113.3 k8s.k8s-dev.ictsc.net
203.0.113.4 k8s-dev.ictsc.net argocd.k8s-dev.ictsc.net
203.0.113.0 ictsc-dev-cp-1
...
# そのまま追記するなら
$ cd terraform && terraform output -raw hosts_entries | sudo tee -a /etc/hostsWarning
/etc/hosts は DNS より優先される。terraform-ictsc-net を apply して Cloudflare 側で
引けるようになったら、/etc/hosts の行は消しておくこと。消し忘れると VIP を
変更したときに古いIPを見続けることになる。
ノード名 (ictsc-dev-cp-1 など) は DNS には登録していないので、
そちらを名前で引きたい場合は /etc/hosts を使う。
認証を持たないアプリの前段に oauth2-proxy を置き、Cilium Gateway API の
ExternalAuth フィルタ (GEP-1494) から認可を問い合わせる。GitHub とのやり取りは
Dex に集約しているので、GitHub 側に登録する callback は Dex の1本だけ。
GitHub App --(callback: dex.<domain>/callback)--> Dex --OIDC--> oauth2-proxy
|
ExternalAuth (ext_authz) で問い合わせ
v
保護対象アプリ
task render-env-values が生成するファイルなので、manifest/scripts/render-env.sh
を編集してから再生成すること。GitHub 側の作業は不要。
values/dex.yamlのstaticClients[].redirectURIsにhttps://<host>/oauth2/callbackresources/gateway.yamlの CertificatednsNamesに<host>resources/httproutes.yamlに HTTPRoute を追加/oauth2は oauth2-proxy へ素通し (ここを保護すると OAuth が完了できない)- それ以外に
ExternalAuthフィルタを付けて実体へ流す sync-waveは oauth2-proxy より後 (下記の注意を参照)
Warning
認可バックエンドの Service が存在しない状態で ExternalAuth 付きの HTTPRoute が
生えると、Cilium は ext_authz フィルタを組まず認証なしで素通しする。
ResolvedRefs=False はステータスに出るがトラフィックは止まらない。
保護対象の HTTPRoute には必ず oauth2-proxy より後の sync-wave を付けること。
Note
oauth2-proxy はログイン後の戻り先を相対パスでしか保持しないため、callback は 保護対象ホスト自身に置く必要がある。別ホスト (auth. など) に集約すると ログイン後に元のアプリへ戻れない。
kubectl も同じ Dex を OIDC プロバイダとして使う。証明書ベースの管理者 kubeconfig
(task kubeconfig が取るもの) とは独立して動くので、Dex が落ちても締め出されない。
kubectl --> kubelogin --> ブラウザ --> Dex --> GitHub (org: ictsc)
|
ID Token
v
kube-apiserver が検証
ictsc org の ictsc2026 team のメンバーに cluster-admin が付く
(manifest/envs/<env>/resources/rbac-oidc.yaml)。
Note
Kubernetes 1.35 で --oidc-* フラグは削除された。そのため apiserver へは
AuthenticationConfiguration をファイルで渡している。Talos v1.13 には専用の
設定フィールドが無い (authConfig / authenticationConfig はどちらも
unknown key) ので、machine.files で置いて extraVolumes で読ませている。
生成は talos/scripts/gen-config.sh。
aqua install で kubelogin が入る (kubectl oidc-login として呼ばれる)。
自分の kubeconfig に context を足す:
$ kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1 \
--exec-command=kubectl \
--exec-interactive-mode=IfAvailable \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=https://dex.k8s-dev.ictsc.net \
--exec-arg=--oidc-client-id=kubernetes \
--exec-arg=--oidc-extra-scope=profile \
--exec-arg=--oidc-extra-scope=email \
--exec-arg=--oidc-extra-scope=groups
$ kubectl config set-context ictsc-dev-sso \
--cluster=ictsc-dev --user=oidc
$ kubectl config use-context ictsc-dev-sso
$ kubectl get nodes # ブラウザが開いて GitHub 認証 -> 成功すれば完了--cluster の名前は kubectl config get-clusters で確認すること
(task kubeconfig が作る context 名は workspace 名 = dev)。
Warning
上のコマンドには落とし穴が2つある。
--exec-interactive-modeは必須。client.authentication.k8s.io/v1には 既定値が無く、省略するとinteractiveMode must be specified for oidc to use exec authentication pluginでkubectlが何もできなくなる (v1beta1なら既定値があるので出ない)。--exec-argの値にカンマを書かない。set-credentialsはカンマを引数の 区切りとして解釈するため、--oidc-extra-scope=profile,email,groupsは--oidc-extra-scope=profile/email/groupsの3引数に分解されて壊れる。 スコープは1つずつ--exec-argを並べること。
トークンは ~/.kube/cache/oidc-login/ にキャッシュされる。作り直したいときは:
$ rm -rf ~/.kube/cache/oidc-loginDex の github connector は orgs に teams を書いていなくても、groups claim を
<org>:<team> 形式で返す。org 名だけの ictsc は返ってこないので、
RBAC には team を列挙する必要がある (ワイルドカードは書けない)。
新しい team を作ったら manifest/scripts/render-env.sh の rbac-oidc.yaml の
subjects に oidc:<org>:<team> を足して task render-env-values。
自分のトークンに何が入っているかは、ログイン後にこれで見られる:
$ cat ~/.kube/cache/oidc-login/* | jq -r .id_token \
| cut -d. -f2 | base64 -d 2>/dev/null | jq '{email, groups}'
{
"email": "you@example.com",
"groups": ["ictsc:ictsc2026"]
}Warning
apiserver に渡すファイルは permissions: 0o444 でなければならない。
apiserver は非 root (UID 65534) で動くため、0o400 にすると読めずに
exit 1 で crashloop する。また op: は create を使うこと。
overwrite は既存ファイル前提なので、初回は
file must exist: ... で writeUserFiles ごと失敗し、
kubelet も etcd も起動しなくなる。
Warning
この設定変更は control plane の再起動を伴う。必ず 1台ずつ
talosctl apply-config して、kubectl get pod -n kube-system | grep apiserver が
1/1 Running になったのを確認してから次へ進むこと。3台同時にやると
etcd の quorum を失う。
apid (50000) はパケットフィルタで踏み台からのみに絞ってある。
trustd (50001) はノード間の証明書配布に必要なため、クラスタの外部CIDRから許可する
(terraform/packet-filter.tf)。手元から直接は届かないので、ラッパー経由で叩く。
$ ./talos/scripts/talosctl-via-bastion.sh -n 192.168.100.1 version
$ ./talos/scripts/talosctl-via-bastion.sh -n 192.168.100.1 dmesgSSH のポート転送で踏み台を経由し、終わったらトンネルを畳む。-e は指定しない
(スクリプトが 127.0.0.1 に差し替える)。Taskfile.yaml の talosctl 呼び出しは
すべてこれを通しているので、task health などはそのまま使える。
| 環境変数 | 既定 | 用途 |
|---|---|---|
TALOS_EP |
control plane の1台目 | 経由する control plane のグローバル IP。upgrade-talos は「対象ノード以外」を経由するためにこれを指定している |
BASTION_SSH_USER |
ubuntu |
踏み台の SSH ユーザ |
TALOS_TUNNEL_PORT |
50000 |
ローカル側の待ち受けポート |
kubectl はトンネル不要。 6443 は送信元を絞っていない (認証は TLS + OIDC/証明書
- RBAC に任せている)。
Warning
戻り通信の許可レンジ (32768-61000) は apid/trustd のポートを含んでしまう。
さくらのパケットフィルタはステートレスなので、自分から出した通信の戻りを
destination port で許可するしかない。ところが 50000 / 50001 はそのレンジの内側に
あるため、素直に書くと「踏み台のみ許可」のつもりが全世界から apid に届く。
許可ルールの後、戻り通信の許可より前に 50000 / 50001 の明示的な deny を
置くこと (local.pf_deny_talos_api)。順序が意味を持つ。
確認は踏み台の外から:
$ nc -z -G 6 <control plane のグローバルIP> 50000 # 失敗すれば正しいCaution
terraform/vars.tf の talos_version / kubernetes_version を上げても
稼働中のクラスタは上がらない。 これらは「これから作るノード」の指定であって、
既存ノードには効かない。
しかも talos_version は、素のままだと 全ディスクを作り直す plan を作る
(source_archive_id が変わるため)。ディスクを作り直すと etcd も Talos の
STATE パーティション (machine config 本体) も消えるので、クラスタは全損する。
いまは sakura_disk に ignore_changes = [source_archive_id] を入れて
防いであるが、この lifecycle ブロックを外さないこと。
更新の経路は3系統ある。それぞれ独立していて、Terraform は OS/Kubernetes の 更新に一切関与しない。
| 対象 | やり方 |
|---|---|
| Talos OS | task upgrade-talos TO=v1.13.9 |
| Kubernetes | task upgrade-k8s TO=1.36.4 |
| Cilium / cert-manager などの Helm チャート | manifest/base/infra/<app>/application.yaml の targetRevision を上げて push (Argo CD が反映) |
$ task upgrade-talos TO=v1.13.9talosctl upgrade を control plane から1台ずつ 実行する。cp を並列で落とすと
etcd の quorum を失うため、1台が Ready に戻るまで次へ進まない。
cordon と evict は talosctl upgrade が自前でやる (--drain が既定 true)。
対象ノード自身を endpoint にすると再起動で経路が切れるので、別の cp を経由する。
machine config は STATE パーティションに残るので消えない。
Note
schematic に system extension を足した場合は、installer イメージを
ghcr.io/siderolabs/installer ではなく
factory.talos.dev/installer/<schematic> に変える必要がある。
現在はLonghorn用の iscsi-tools と util-linux-tools を含むため、
task upgrade-talos は talos/schematic.yaml に対応するImage Factory installerを使う。
$ task upgrade-k8s TO=1.36.4talosctl upgrade-k8s は 1回叩くだけで全ノードを面倒みる。ノードの自動検出、
Talos とのバージョン互換性チェック、イメージの事前 pull、
apiserver → controller-manager → scheduler → kubelet の順序まで自前でやるので、
ノードごとにループする必要はない。task は実行前に --dry-run の結果を表示する。
- マイナーは1つずつ (1.36 → 1.37 → 1.38)。飛ばせない
- パッチは飛ばしてよい (1.36.1 → 1.36.4)
Warning
--to を省くと talosctl 自身が持つ既定値になる。talosctl v1.13.8 の既定は
1.36.2 で、いま動いているのは 1.36.3 のため、省略すると ダウングレード になる。
そのため TO は必須にしてある。
Talos は OS のバージョンでネットワークインターフェースの名前を変えることがある。
実際 v1.13.9 で eth0/eth1 から ens3/ens4 に変わった。
名前に依存した設定は、OS を上げた瞬間に静かに壊れる。
machine config 側は名前ではなく PCI バスパスで選んでいる
(talos/scripts/gen-config.sh)。バスパスは Terraform が NIC を繋ぐ順序
(0 = グローバル / 1 = 内部) で決まり、Talos のバージョンでは変わらない。
- deviceSelector:
busPath: "0000:00:03.0" # グローバル (eth0 / ens3)
- deviceSelector:
busPath: "0000:00:04.0" # 内部 (eth1 / ens4)MAC でも選べるが、MAC はサーバを作った後にしか分からない。ISO を先に焼くこの構成
(apply-network -> talos-config -> apply-terraform) では使えない。
Warning
Cilium はインターフェースを名前でしか選べない。
manifest/envs/<env>/resources/cilium-l2-announcement.yaml の interfaces は
^(eth0|ens3)$ のように新旧どちらも許容しておくこと。
片方だけだと全ノードを上げ終わった瞬間に Ingress VIP の広報が止まり、
外部から一切到達できなくなる。しかも Gateway の PROGRAMMED は True の
ままなので気づきにくい。
名前で書いてしまっていた場合、上げたノードはこうなる。ノード自体は正常に 起動していて apid も応答するので、ハングと勘違いしやすい。
| 症状 | 理由 |
|---|---|
| グローバル IP に ping も talosctl も通らない | 静的アドレスがどのリンクにも適用されず、グローバル側が素のまま |
kubectl get node が NotReady |
kubelet が API にも DNS にも届かない |
内部セグメントの DHCP レンジ (192.168.100.200-250) に居る |
設定が当たらず DHCP にフォールバックし、踏み台からリースを貰う |
talosctl --insecure が tls: certificate required |
maintenance mode ではなく、config は入っている |
復旧は 再起動不要。踏み台から DHCP レンジを ping で走査してノードを見つけ、 バスパス版の config を当てるだけでよい。
$ ssh <踏み台> 'for i in $(seq 200 250); do ping -c1 -W1 192.168.100.$i >/dev/null 2>&1 && echo 192.168.100.$i; done'
$ talosctl -n <見つけたIP> -e <別のcpのグローバルIP> apply-config -f talos/build/<cluster>/config/<hostname>.yamlNote
上げる前に、全ノードへ先にバスパス版の config を当てておくと壊れない。 config の適用は再起動を伴わず、古い Talos (eth0/eth1) でも同じ NIC にマッチする。
実体の更新とは別に、以下も同じ値にしておくこと。揃えなくてもクラスタは動くが、 次にノードを増やしたときだけ古いバージョンで作られるという分かりにくい状態になる。
| ファイル | 項目 |
|---|---|
aqua.yaml |
siderolabs/talos (talosctl 本体)、kubernetes/kubernetes/kubectl |
Taskfile.yaml |
TALOS_VERSION (イメージ取得) |
terraform/vars.tf |
talos_version、kubernetes_version |
talosctl のバージョンは upgrade / upgrade-k8s の既定値を決めるので、
先に aqua.yaml を上げて aqua install しておくとよい。
tfstate は さくらのオブジェクトストレージ (石狩) の S3 backend に置いてあるので、
マシン間で共有されている。terraform 関連で持ち回るものは無い。
git に入っていないので手で持ち込む必要があるのは次の2つだけ。
| ファイル | 入手方法 |
|---|---|
.envrc |
./init.sh が対話で聞いて作る (API キーなどを手元で入力) |
talos/secrets.yaml |
既存クラスタを構築したマシンからコピーする |
$ ./init.sh
$ scp <構築したマシン>:<リポジトリ>/talos/secrets.yaml talos/secrets.yaml
$ task select-dev # workspace は default から始まるので必ず選ぶtalosconfig と .kube/config は secrets.yaml から再生成されるので、
コピーしなくてよい (task talos-config / task kubeconfig)。
Caution
talos/secrets.yaml はクラスタの PKI 本体。 これが無い状態で
task talos-config を走らせると、gen-config.sh は「初回構築だ」と判断して
別のクラスタの秘密情報を新規生成する。そのまま apply すると CD-ROM が
別 CA で署名された machine config で上書きされ、ノードが再ブートストラップした
瞬間にクラスタへ参加できなくなる。talosctl も一切通らなくなる。
現在は task talos-config の冒頭で「tfstate にサーバが在るのに
secrets.yaml が無い」場合に停止するようにしてあるが、そもそも先に
コピーしておくこと。
Note
secrets.yaml を紛失すると既存クラスタを操作する手段が完全に失われる。
構築したマシン以外にもバックアップを取っておくこと。
.envrc.tmpl—.envrcの雛形 (これはコミットする).envrc— さくらのクラウドの API キーなど (gitignore 済み / direnv が読む)talos/secrets.yaml— クラスタの PKI。これを失うとクラスタを操作できなくなるので 必ずどこかにバックアップすること (gitignore 済み)talos/talosconfig— talosctl のクライアント証明書 (gitignore 済み)
Secret の値は Git に置かない。基盤の認証情報はクラスタに直接作る (Argo CD の管理外)。
task up が下記を呼ぶので、通常は手で作る必要はない。
| task | 作る Secret |
|---|---|
task auth-secrets |
dex/dex-secrets oauth2-proxy/oidc argocd/argocd-oidc |
task argocd-repo-key |
argocd/repo-<repo> (deploy key。GitHub 側にも登録する) |
dev では External Secrets Operator (ESO) の Webhook Provider を使う。
基盤の7 Secret と Regalia の Discord OAuth を有効化済み。
さくらの
POST /secretmanager/vaults/{id}/secrets/unveil を呼び、15分間隔で
各 Kubernetes Secret を更新する。共通 Helm chart
charts/sakura-secrets が ExternalSecret を生成し、共有 ClusterSecretStore を参照する。
アプリ側は manifest/base/apps/regalia/secrets/dev.yaml に対応だけを書く。
CSI ドライバや Pod のボリュームは不要。
prod は既存のダミー定義のままで、この移行の対象外。
secrets:
- name: discord-oauth-client
keys:
client-id: scoreserver-discord-client-id
client-secret: scoreserver-discord-client-secret初回の準備後に enabled: true にする。Secret を増やす際はこの一覧へ追加する。値の変更はさくら側だけで行う。
認証キーは external-secrets/sakura-secret-manager-credentials の1か所に置く。
manifest/base/infra/external-secrets/resources/clustersecretstore.yaml の
conditions.namespaces で利用可能な namespace を制限する。
別 namespace へ導入する場合はこの許可リストに追加する。認証キーの複製は不要。
現在はユーザー承認により管理用 API キーを一時利用中。専用読取キーの発行後に同じ Secret を更新する。
dev の regalia Application は workload とこの chart を複数ソースで同期する。
-
KMS キーと保管庫は
terraform/secrets.tfで dev 用に管理する。 ID はterraform -chdir=terraform output -raw secret_manager_vault_idで確認できる。 次の名前で実際の値を登録する。保管庫内の名前 値 Kubernetes Secret のキー scoreserver-discord-client-idDiscord OAuth Client ID client-idscoreserver-discord-client-secretDiscord OAuth Client Secret client-secretDiscord Developer Portal の OAuth2 Redirects は
https://contest.k8s-dev.ictsc.net/api/auth/callbackを登録する。 -
対象保管庫の
unveil権限を持つ専用 API キーを用意する。.envrcにSAKURA_SECRETS_ACCESS_TOKEN、SAKURA_SECRETS_ACCESS_TOKEN_SECRET、SAKURA_SECRETS_VAULT_IDを設定してdirenv allowする。 この API キーは初回投入が必要で、ESO 自身から取得する構成にはしない。 -
認証 Secret を投入し、既存ダミー Secret を Argo CD の prune から保護する。 ダミーを Git の管理対象から外す変更を同期する前に実行する。 投入タスクは Bash・jq・kubectl を使用し、認証情報を環境変数から標準入力で渡す。 秘密の値をコマンド引数や一時ファイルに書き出さない。
SECRET_KUBE_CONTEXT=admin@ictsc-dev task sakura-secret-credentials kubectl --context admin@ictsc-dev -n scoreserver annotate secret discord-oauth-client \ argocd.argoproj.io/sync-options=Prune=false --overwrite
-
ClusterSecretStore の許可リストに
scoreserverを追加する。 Regalia の values をenabled: trueにし、dev workload の kustomization にcomponents: [../../components/external-secrets]を追加してダミー定義を外す。 Argo CD のexternal-secretsApplication を先に同期し、controller、webhook、 CRD が Ready になってからregaliaを同期する。root の sync-wave だけでは、 子 Application の CRD 導入完了を保証しないため順番を確認する。 ESO はcreationPolicy: Orphanで既存 Secret を引き継ぐ。 参照元の削除や API 障害時には既存の Secret を保持する。 -
同期を確認して backend を再起動する。Secret の値そのものは表示しない。
kubectl --context admin@ictsc-dev -n scoreserver wait \ --for=condition=Ready clustersecretstore/sakura-secret-manager --timeout=120s kubectl --context admin@ictsc-dev -n scoreserver wait \ --for=condition=Ready externalsecret/discord-oauth-client --timeout=120s kubectl --context admin@ictsc-dev -n scoreserver rollout restart deployment/scoreserver-backend kubectl --context admin@ictsc-dev -n scoreserver rollout status deployment/scoreserver-backend
値を更新した際も、同期完了後に backend の再起動が必要(環境変数は自動更新されない)。
即時同期は ExternalSecret に force-sync annotation を設定して要求できる。
復旧時には、API 認証 Secret の再投入と保管庫側の値が必要。
仕様: さくら Secret Manager API ・ESO Webhook Provider
基盤の対応表は manifest/envs/dev/secrets/<namespace>.yaml にまとめる。
各 namespace に <namespace>-secrets Application を作り、共通の ClusterSecretStore を参照する。
| namespace | 移行対象の Secret |
|---|---|
| dex | dex-secrets |
| oauth2-proxy | oidc |
| argocd | argocd-oidc |
| monitoring | grafana-oidc、alertmanager-discord、loki-object-storage、tempo-object-storage |
Dex と各 OIDC クライアントは同じ保管庫内のキーを参照する。 既存の client secret と cookie secret をコピーし、移行のための再生成はしない。 証明書・CNPG の DB 認証・Helm のリリース情報等は各 controller の管理を継続する。 Argo CD Deploy Key と ESO の API 認証は GitOps 起動前に必要なため初期投入を維持する。
基盤 Secret の移行では、保管庫と ESO CRD を先に用意し、values を有効化する前に
下記を実行する。インポートは共通 chart の生成結果から対応表を読み、現在の値をコピーする。
Bash・jq・curl・Helm・kubectl を使用する。
SAKURACLOUD_ACCESS_TOKEN / SAKURACLOUD_ACCESS_TOKEN_SECRET には保管庫の一覧・取得・書込権限、
SAKURA_SECRETS_* の専用キーには ESO の読取権限を持たせる。
export SECRET_KUBE_CONTEXT=admin@ictsc-dev
export SAKURA_SECRETS_VAULT_ID=$(terraform -chdir=terraform output -raw secret_manager_vault_id)
for ns in dex oauth2-proxy argocd monitoring; do
task sakura-secret-import -- "$ns" "manifest/envs/dev/secrets/$ns.yaml"
done
task sakura-secret-credentialsインポートは既存の値と一致すればスキップし、異なる値が既にある場合は上書きせず停止する。 途中で失敗してもコピー済みの項目は残るため、原因を解消して同じコマンドを再実行できる。 別プロセスから保管庫を書き換えながらインポートしないこと。 Discord OAuth のダミー値はインポートせず、実際の値を登録する。
その後、対象 namespace の values を enabled: true に変更して push し、各 namespace の
ExternalSecret がすべて Ready になることを確認する。初回は値を変えないため一斉再起動は不要。
後日のキー更新では、Dex と対応クライアント等、環境変数を読む利用先を同期後に再起動する。
Object Storage キーを Terraform 側で更新した場合も、保管庫側へ新しい値を反映する。
既存の task auth-secrets / alert-secrets / object-storage-secrets は
ESO への切替後は上書き投入をスキップする。
クラスタ再構築時は初期 Secret の投入後、保管庫の値を ESO から復元してから認証系サービスを起動する。
既存保管庫に対し新規生成値をインポートしないこと。
KMS と保管庫には prevent_destroy を設定している。クラスタ全体の destroy 時も、
保管庫を残すかを先に決める。Secret 値は Terraform リソースとして登録しない。
- ノード名の DNS 登録 (VIP は Cloudflare に登録済み。ノードは
/etc/hosts運用) - IPv6 (
enable_ipv6 = false。drove は dual stack) - ストレージ (Rook/Ceph)
- kubelogin (kubectl の OIDC 認証。Dex は導入済み)
- Secret の Git 管理 (SOPS / sealed-secrets)
GitOps CI は dev/prod の Kustomize、各 Application の固定 Helm chart と環境別 values、
workload overlay を展開してスキーマ検証する。CRDは利用chartと固定版のbootstrap定義から取得し、
スキーマ不明のリソースも失敗扱いにする。vendorのCRD定義そのものは検査を除外し、
そこから抽出したschemaでカスタムリソースを検査する。nullフィールドはAPIのpruningに
合わせて除去してから検査する。Secretやクラスタ認証は不要。
ローカルでは aqua install と python -m pip install PyYAML==6.0.3 の後、
python scripts/validate-gitops.py --environment dev(または prod)で実行できる。
これは構文・スキーマの検証であり、Admission/CELや実機動作の保証ではない。
TerraformのPR検証はbackend/PKI認証なしでfmt・validate・tflintを実行する。
従来のPR planは未生成machine configのfilemd5で失敗していたため、実環境planは
手動workflowへ分離した。手動dev planでは既存PKIからconfig/ISOを生成してからplanする。
prodはOmni管理のためこの従来CIの対象外で、環境専用の task tf:prod / task tf:omni:prod を使う。
manifest/base/apps/regalia/workload/overlays/openapi-dev/frontend.yaml の
ICTSC_DEMO_MODE: "true" で、競技時間と再回答待ち時間の画面表示を固定する。
解除は同じ値を "false" に変更して GitOps で反映し、ブラウザを再読み込みする。
バックエンドの受付期間・提出間隔は実時間で判定される。
デモ中は再提出の待ち時間表示も固定されるため、繰り返し提出する練習は
デモモードを解除して行う。
起動時に生成する runtime-config.js には専用の emptyDir をファイル単位で
マウントする。init container と frontend は同じ digest のイメージを使用し、
コンテナのルートファイルシステムは読み取り専用のまま維持する。