From 69ca42b5ea3892e9c8f6f6273963ac485c9cc857 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 31 Jul 2026 00:10:22 +0000 Subject: [PATCH] feat: add kubernetes deploy manifests and image build scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit weko-k8s の web デプロイ (deploy/weko/manifest_template/deploy-web.yaml + scripts/make_weko_manifests.sh + scripts/deploy_weko.sh) の流儀に揃えて、 k8s 上で動かすためのテンプレートと運用スクリプトを追加する。 - deploy/jcgroups/manifest_template: __PLACEHOLDER__ 入りのマニフェスト。 nginx / web / worker を 1 Pod 3 コンテナに同居させ、hostAliases で nginx イメージに焼き込まれた upstream `web:5050` を 127.0.0.1 に向ける - deploy/middleware: 検証用の単一インスタンス PostgreSQL / Redis / RabbitMQ - scripts/build_images.sh: アプリ / nginx イメージのビルド。Nuxt の serverName は ビルド時に静的 SPA へ展開されるため、--build-arg SERVER_NAME で FQDN を注入する - scripts/make_jcgroups_manifests.sh: params.env からマニフェストと server.config.toml / shibboleth2.xml / 証明書を生成する。設定ファイルは テンプレートを複製せずリポジトリ本体を唯一の正として環境依存の値だけ上書きし、 [develop] を除去して置換結果を assert する - scripts/deploy_jcgroups.sh / delete_jcgroups.sh: 生成物の適用と削除 ミドルウェアは JAIRO Cloud 共通クラスタ (pgpool.weko3pg / weko-sentinel-service.weko3re / weko-rabbitmq.weko3ra) を使う前提を既定とし、 Redis は Sentinel 接続と DB 番号の明示に対応する。共有 Redis では DB 番号が 機関ごとに払い出し済みのため、リポジトリ既定値 (0/1/2) の暗黙利用を禁止する。 nginx/Dockerfile には SERVER_NAME の build-arg を追加した。 Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 4 + k8s/README.md | 433 +++++++++++++++ .../jcgroups/manifest_template/configmap.yaml | 14 + .../manifest_template/deploy-web.yaml | 286 ++++++++++ .../jcgroups/manifest_template/ingress.yaml | 38 ++ .../manifest_template/job-db-init.yaml | 57 ++ .../jcgroups/manifest_template/namespace.yaml | 4 + .../jcgroups/manifest_template/secret.yaml | 20 + .../jcgroups/manifest_template/service.yaml | 29 + .../jcgroups/manifest_template/volume-pv.yaml | 26 + .../manifest_template/volume-pvc.yaml | 16 + k8s/deploy/jcgroups/params.env.example | 166 ++++++ .../manifest_template/middleware.yaml | 194 +++++++ k8s/scripts/build_images.sh | 74 +++ k8s/scripts/delete_jcgroups.sh | 62 +++ k8s/scripts/deploy_jcgroups.sh | 158 ++++++ k8s/scripts/make_jcgroups_manifests.sh | 515 ++++++++++++++++++ nginx/Dockerfile | 9 + 18 files changed, 2105 insertions(+) create mode 100644 k8s/README.md create mode 100644 k8s/deploy/jcgroups/manifest_template/configmap.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/deploy-web.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/ingress.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/job-db-init.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/namespace.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/secret.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/service.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/volume-pv.yaml create mode 100644 k8s/deploy/jcgroups/manifest_template/volume-pvc.yaml create mode 100644 k8s/deploy/jcgroups/params.env.example create mode 100644 k8s/deploy/middleware/manifest_template/middleware.yaml create mode 100755 k8s/scripts/build_images.sh create mode 100755 k8s/scripts/delete_jcgroups.sh create mode 100755 k8s/scripts/deploy_jcgroups.sh create mode 100755 k8s/scripts/make_jcgroups_manifests.sh diff --git a/.gitignore b/.gitignore index 935edb2f..fd9bcf91 100644 --- a/.gitignore +++ b/.gitignore @@ -231,3 +231,7 @@ logs .env .env.* !.env.example + +# k8s deploy parameters (secret_key / DB password / cert paths) +params.env +k8s/**/params.env diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 00000000..9def3610 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,433 @@ +# Kubernetes デプロイ + +JAIRO Cloud Groups Manager を k8s で動かすためのイメージビルド手順とマニフェスト。 +構成・命名・スクリプトの流儀は `weko-k8s` の web デプロイ +(`deploy/weko/manifest_template/deploy-web.yaml` + `scripts/make_weko_manifests.sh` ++ `scripts/deploy_weko.sh`) に合わせてある。 + +``` +k8s/ +├── deploy/ +│ ├── jcgroups/ +│ │ ├── manifest_template/ __PLACEHOLDER__ 入りのマニフェスト +│ │ │ ├── namespace.yaml +│ │ │ ├── configmap.yaml +│ │ │ ├── secret.yaml +│ │ │ ├── deploy-web.yaml nginx + web + worker (1 Pod 3 コンテナ) +│ │ │ ├── service.yaml +│ │ │ ├── ingress.yaml +│ │ │ ├── volume-pv.yaml +│ │ │ ├── volume-pvc.yaml +│ │ │ └── job-db-init.yaml +│ │ └── params.env.example 環境ごとのパラメータ (これをコピーして使う) +│ └── middleware/ +│ └── manifest_template/ +│ └── middleware.yaml 検証用 PostgreSQL / Redis / RabbitMQ +└── scripts/ + ├── build_images.sh イメージ2種のビルド (+ push) + ├── make_jcgroups_manifests.sh params.env → 実マニフェスト生成 + ├── deploy_jcgroups.sh 生成物を kubectl apply + └── delete_jcgroups.sh リソース削除 +``` + +## 構成 + +``` + Ingress (TLS 終端 / backend-protocol: HTTPS) + │ 443 + ┌────────────────────────────────▼─────────────────────────────────┐ + │ Deployment: -web Service: -nginx (443/80) │ + │ ┌──────────────────────┐ ┌────────────────┐ ┌──────────────────┐ │ + │ │ nginx │ │ web │ │ worker │ │ + │ │ nginx + shibd │ │ uwsgi │ │ celery │ │ + │ │ + shibauthorizer │─▶│ :5050 │ │ │ │ + │ │ + shibresponder │ │ (Flask API) │ │ │ │ + │ │ + 静的 SPA │ └────────────────┘ └──────────────────┘ │ + │ └──────────────────────┘ hostAliases: web → 127.0.0.1 │ + └──────────────────────────────────┬───────────────────────────────┘ + │ + ┌────────────┬───────────────┴───────────────┬──────────────┐ + │ │ │ │ + ┌─────▼──────┐┌────▼───────────┐┌─────────────────▼────┐┌────────▼─────────┐ + │ pgpool ││ weko-rabbitmq ││ weko-sentinel-service ││ 共有ストレージ │ + │ .weko3pg ││ .weko3ra ││ .weko3re → redis ││ /var/tmp/jcgroups │ + │ DB:jcgroups││ vhost:jcgroups ││ db 割当 + db4 ││ (NFS RWX) │ + └────────────┘└────────────────┘└──────────┬───────────┘└──────────────────┘ + JAIRO Cloud 共通ミドルウェア (別 Namespace) │ db4 + └──▶ WEKO も同じ db4 を読む +``` + +weko と同じく **nginx / web / worker を1つの Pod に同居させている**。理由: + +- `nginx` / `shibd` / `shibauthorizer` / `shibresponder` は Unix ドメインソケット + (`/opt/shibboleth/*.sock`) で通信するため、そもそも 1 コンテナに同居が必須 + (`nginx/supervisord.conf`)。 +- nginx イメージには `upstream api_server { server web:5050; }` が焼き込まれている + (`nginx/conf.d/default.conf`)。同居させて `hostAliases` で `web` を + `127.0.0.1` に向ければ、nginx が起動時に解決した ClusterIP をキャッシュし続ける + 問題 (Pod 入れ替え後に 502) を避けられる。アプリ用の Service は作っていない。 + +`web` と `worker` は同じイメージで `command` だけ差し替える +(`compose.prod.yaml` と同じ方針)。 + +## 手順 + +### 1. イメージをビルドする + +```bash +cd k8s/scripts +./build_images.sh <レジストリ> <タグ> [--push] + +# 例 +./build_images.sh groups.example.ac.jp nrt.ocir.io/xxxxxxxx/jc 20260730 --push +``` + +作られるイメージ: + +| イメージ | 中身 | 起動するもの | +|---|---|---| +| `/jc-groups-manager:` | Python / Flask (`Dockerfile` の prod ステージ) | uwsgi, celery, `flask db` | +| `/jc-groups-manager-nginx:` | nginx + Shibboleth SP + 静的 SPA (`nginx/Dockerfile` の prod ステージ) | supervisord | + +**nginx イメージは FQDN ごとにビルドし直す必要がある。** +`configs/app.config.ts` の `serverName` は Nuxt のビルド時に静的 SPA へ +インライン展開されるため、実行時に環境変数で差し替えられない +(ビルド済みバンドルに `const zd="localhost", No=\`https://${zd}\`` の形で残る)。 +`build_images.sh` は `--build-arg SERVER_NAME=` を渡してこれを注入し、 +生成物を grep して実際に入ったかを確認する。 +1 イメージを全環境で使い回したいなら `serverName` を `runtimeConfig` へ移す改修が必要。 + +またアプリイメージは `COPY . .` で `configs/server.config.toml` を焼き込むため、 +`developer_login = true` のままビルドしようとすると `build_images.sh` が止める。 +実行時には Secret でマウント上書きするので、イメージ内の TOML は +プレースホルダのままにしておくこと。 + +### 2. ミドルウェアを用意する + +PostgreSQL / Redis / RabbitMQ は **JAIRO Cloud 共通のクラスタ (weko-k8s) を使う**。 +`params.env.example` の既定値はその前提になっている。 + +| ミドルウェア | 接続先 | 実体 (weko-k8s) | +|---|---|---| +| PostgreSQL | `pgpool.weko3pg:5432` | Zalando postgres-operator + pgpool (`deploy/postgresql`) | +| Redis | `weko-sentinel-service.weko3re:26379` (master `mymaster`) | Redis + Sentinel (`deploy/redis`) | +| RabbitMQ | `weko-rabbitmq.weko3ra:5672` | RabbitMQ Cluster Operator (`deploy/rabbitmq`) | + +いずれも別 Namespace なので `.` の DNS で引く。 +weko3pg / weko3re / weko3ra には NetworkPolicy が無いので、Namespace をまたいだ +接続はそのまま通る。 + +#### PostgreSQL: ロールを作る + +DB 自体は db-init Job の `flask db init` (`sqlalchemy_utils.create_database`) が作るので、 +**ロールを CREATEDB 権限付きで用意しておけばよい**。pgpool 越しは md5 認証。 + +```bash +PG=$(kubectl get pod -n weko3pg -l application=spilo,spilo-role=master -o name | head -1) +kubectl exec -n weko3pg $PG -- psql -U postgres -c \ + "CREATE ROLE jcgroups WITH LOGIN CREATEDB PASSWORD '';" +``` + +既存の `invenio` ロール (`postgresql-infrastructure-roles` の ConfigMap で +`createdb` 付き) を使い回すこともできるが、権限分離のため専用ロールを推奨。 + +#### RabbitMQ: vhost を作る + +WEKO は機関ごとに vhost を切っている (`scripts/make_rabbitmq_vhost.sh`)。 +同じ流儀で jcgroups 用の vhost を作り、使うユーザに権限を与える。 + +```bash +RA=$(kubectl get pod -n weko3ra -l app.kubernetes.io/name=weko-rabbitmq -o name | head -1) +kubectl exec -n weko3ra $RA -- rabbitmqctl add_vhost jcgroups +kubectl exec -n weko3ra $RA -- rabbitmqctl set_permissions -p jcgroups invenio ".*" ".*" ".*" +``` + +`RABBITMQ_URL` の末尾が vhost 名になる (`.../5672/jcgroups`)。 +`//` で終わらせると既定 vhost `/` の意味になるので注意。 + +#### Redis: DB 番号を確保する + +共通 Redis は 1 インスタンスを全機関で共有する構成 (`databases 40000`)。 +**空いている DB 番号を運用側で確保してから `params.env` に書く。** +共通クラスタで既に決まっている番号: + +| DB 番号 | 用途 | +|---|---| +| 機関ごとに払い出し | WEKO の `CACHE_REDIS_DB` / `ACCOUNTS_SESSION_REDIS_DB_NO` / `CELERY_RESULT_BACKEND_DB_NO` (`repositories.txt` の 16〜18 列目) | +| 3 | `CRAWLER_REDIS_DB` (固定) | +| 4 | `GROUP_INFO_REDIS_DB` (固定) | + +jcgroups 側は `REDIS_DB_APP_CACHE` / `REDIS_DB_ACCOUNT_STORE` / +`REDIS_DB_RESULT_BACKEND` に空き番号を指定する。`RedisSentinelCache` のときは +この3つを明示しないと `make_jcgroups_manifests.sh` が止まる (リポジトリ既定値の +0/1/2 を黙って使うと機関の DB とぶつかるため)。 + +**`REDIS_DB_GROUP_CACHE` は 4 のまま変えない。** WEKO の +`GROUP_INFO_REDIS_DB = 4` が固定で、ここが WEKO との唯一の実行時結合点。 + +Sentinel ノードは headless Service を1件書けばよい (全 Sentinel Pod の IP が +引ける)。WEKO の `instance.cfg` も +`CACHE_REDIS_SENTINELS = [("weko-sentinel-service.weko3re","26379")]` と同じ書き方。 + +#### 検証用に自前のミドルウェアを立てる場合 + +`DEPLOY_MIDDLEWARE=true` にすると単一インスタンスの PostgreSQL / Redis / RabbitMQ を +同じ Namespace に立てる。そのときは接続先も併せて変える: + +``` +POSTGRES_HOST=postgres +REDIS_CACHE_TYPE=RedisCache +REDIS_URL=redis://redis:6379 +REDIS_DB_APP_CACHE=0 # 単体 Redis なので既定値のままでよい +REDIS_DB_ACCOUNT_STORE=1 +REDIS_DB_RESULT_BACKEND=2 +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672// +``` + +### 3. パラメータを用意する + +```bash +cp ../deploy/jcgroups/params.env.example /path/to/params.env +vi /path/to/params.env +``` + +FQDN・イメージ・レプリカ数・ストレージ・ミドルウェア接続先・証明書のパスを書く。 +`params.env` は `.gitignore` 済み。 + +### 4. マニフェストを生成する + +```bash +./make_jcgroups_manifests.sh /path/to/params.env /path/to/generated +``` + +``` +/path/to/generated// +├── manifests/*.yaml __PLACEHOLDER__ を置換したマニフェスト +├── conf/server.config.toml 環境用に書き換えたアプリ設定 +├── conf/shibboleth2.xml entityID / Host を FQDN に置換したもの +├── ssl/server.crt|key nginx / Shibboleth SP のサーバ証明書 +├── sp/server.crt|key mAP Core 接続用クライアント証明書 +├── institutions//... 機関別クライアント証明書 +└── params.env 使ったパラメータの控え +``` + +`server.config.toml` と `shibboleth2.xml` は**テンプレートを複製せず、リポジトリの +`configs/server.config.toml` / `nginx/shibboleth2.xml` を唯一の正として環境依存の値 +だけを上書きする**。設定項目が増えても追従漏れが起きない。生成時に +`[develop]` セクションは丸ごと落とし、置換結果を assert している。 + +**生成物には `secret_key`・DB パスワード・秘密鍵が入る。Git に入れないこと。** + +### 5. デプロイする + +```bash +./deploy_jcgroups.sh /path/to/generated/ + +# 2回目以降 (DB 初期化 Job を流さない) +./deploy_jcgroups.sh /path/to/generated/ --skip-db-init +``` + +やること: + +1. Namespace 作成 +2. ファイル由来の Secret / ConfigMap を `kubectl create ... | kubectl apply -f -` で作成 + (中身が Git に載らないようマニフェスト化していない) +3. ConfigMap / Secret / PV / PVC / Service / Ingress を apply +4. `DEPLOY_MIDDLEWARE=true` なら検証用ミドルウェアも apply して Ready 待ち +5. DB 初期化 Job (`flask db init && flask db create`) を流して完了待ち +6. `deploy-web.yaml` を apply して `rollout status` 待ち + +削除は `./delete_jcgroups.sh /path/to/generated/ [--all]`。 +`--all` を付けると Namespace / PV / PVC も消える (データが消える)。 + +## 作られる Secret / ConfigMap + +`` は FQDN の `.` `_` を `-` に置換したもの (weko-k8s と同じ流儀)。 +例: `groups.example.ac.jp` → `groups-example-ac-jp` + +| 名前 | 中身 | 作られ方 | +|---|---|---| +| `-secret` | `server.config.toml` | deploy スクリプトが `conf/` から作る | +| `-tls` | nginx / SP のサーバ証明書 | deploy スクリプトが `ssl/` から作る | +| `-cert` | Ingress 用 TLS Secret | deploy スクリプトが `ssl/` から作る | +| `-sp-cert` | mAP Core 接続用クライアント証明書 | deploy スクリプトが `sp/` から作る | +| `-institution-certs` | 機関別クライアント証明書 | deploy スクリプトが `institutions/` から作る | +| `-middleware` | PostgreSQL のパスワード | `secret.yaml` | +| `-configmap` | TZ / FLASK_ENV などの環境変数 | `configmap.yaml` | +| `-shibboleth2` | `shibboleth2.xml` | deploy スクリプトが `conf/` から作る | + +**アプリ設定は環境変数で上書きできない。** `src/server/config.py` に env 読み込み機構が +ないため、接続先・シークレットはすべて `server.config.toml` 経由になる。 +`-configmap` に置けるのは Flask / Python / OS レベルの環境変数だけ。 + +機関別クライアント証明書は `weko-group-cache-db` が +`/var/mnt//server.crt|key` を読む (`loader.py:213-214`)。 +Secret のキー名に `/` は使えないので、`make_jcgroups_manifests.sh` が +`INSTITUTION_CERTS_DIR` の中身から `items[].path` を生成して +サブディレクトリを組み立てる (キー名は `.server.crt`)。 + +## テンプレートの書き方 + +`__PLACEHOLDER__` は `make_jcgroups_manifests.sh` の `render()` が sed で置換する。 +加えて**マーカー行**という仕組みがある: + +```yaml + #__NODE_SELECTOR__nodeSelector: + #__NODE_SELECTOR__ nodeType: __NODE_TYPE__ +``` + +条件を満たすときはマーカー文字列だけを削って有効化し、満たさないときは行ごと削除する。 +`params.env` の値で出力を切り替えるのに使っている。 + +| マーカー | 制御するもの | 有効になる条件 | +|---|---|---| +| `#__IMAGE_PULL_SECRET__` | `imagePullSecrets` | `IMAGE_PULL_SECRET_NAME` が非空 | +| `#__NODE_SELECTOR__` | `nodeSelector` | `NODE_TYPE` が非空 | +| `#__STORAGE_PVC__` | 共有ストレージを PVC にする | `STORAGE_TYPE=nfs` | +| `#__STORAGE_EMPTYDIR__` | 共有ストレージを emptyDir にする | `STORAGE_TYPE=emptydir` | +| `#__EXTERNAL_TRAFFIC_POLICY__` | `externalTrafficPolicy: Local` | `SERVICE_TYPE` が ClusterIP 以外 | +| `#__INSTITUTION_CERT_ITEMS__` | 機関別証明書の `items:` | `INSTITUTION_CERTS_DIR` が非空 | + +生成後に `__[A-Z0-9_]*__` が残っていないかを grep して、置換漏れなら失敗させる。 + +## 設計上の注意 + +### `enableServiceLinks: false` は必須 + +k8s は Namespace 内の Service ごとに Docker link 互換の環境変数を注入する。 +Service 名 `redis` に対して `REDIS_PORT=tcp://:6379` が入り、 +`weko-group-cache-db` の設定フィールド `REDIS_PORT` (pydantic-settings が +環境変数から読む) と衝突して**起動に失敗する**。 + +``` +pydantic_core._pydantic_core.ValidationError: 1 validation error for RuntimeConfig +cache_groups.redis_port + Input should be a valid integer, unable to parse string as an integer + [type=int_parsing, input_value='tcp://10.96.201.60:6379', input_type=str] +``` + +`POSTGRES_PORT` / `RABBITMQ_PORT` も同様に注入されるため、Service 名を変えるのでは +なく注入自体を止める。全 Pod spec に入れてある。 + +注入されるのは**同一 Namespace の Service だけ**なので、共通ミドルウェア +(別 Namespace) を使う構成では実際には衝突しない。ただし +`DEPLOY_MIDDLEWARE=true` で `redis` / `postgres` / `rabbitmq` を同じ Namespace に +立てた瞬間に再発するため、設定は外さないこと。 + +### TLS は SP に HTTPS で届ける + +Shibboleth SP は `handlerSSL="true"` / `cookieProps="https"` で動くため、Ingress で +TLS を落として HTTP で Pod に渡すと SP のハンドラが動かない。 +`ingress.yaml` は weko と同じく `nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"` +で Pod に対して張り直す。TLS ごと素通しさせたい場合は +`SERVICE_TYPE=LoadBalancer` で 443 を直接公開するか、ingress-nginx の +ssl-passthrough を使う (その場合クライアントに見せる証明書は `-tls` の方)。 + +### Celery の並列数を必ず明示する + +`supervisord.worker.conf` の `celery -A server.celery_app worker` には +`--concurrency` 指定がない。Celery はノードの CPU 数だけ prefork するため、 +CPU limit を付けてもコア数分 (検証環境では 64) fork してメモリを食い潰す。 +`deploy-web.yaml` は `CELERY_CONCURRENCY` を明示的に渡している。 + +### supervisord は nginx コンテナだけ + +アプリイメージの prod ステージは `USER root` のまま supervisord が pyuser に降格する +構造。k8s では 1 コンテナ 1 プロセスが素直なので、`web` は `uwsgi --ini`、 +`worker` は `celery ... worker` を直接 `command` で起動し、`runAsUser: 1000` で +非 root 実行にしている。nginx コンテナだけは shibd との同居のため supervisord のまま。 + +### 共有ストレージ + +`/var/tmp/jcgroups` (`[storage.local]` の `temporary` / `storage`) は web が +アップロードした CSV を worker が読むため両者に見せる必要がある。 +1 Pod 同居なので `REPLICAS=1` なら `STORAGE_TYPE=emptydir` で足りるが、 +複数レプリカでは Pod をまたいで同じ物が見えないといけないので +`STORAGE_TYPE=nfs` (ReadWriteMany) を使う。RWX を用意できない場合は +`server.config.toml` の `[storage] type` を `object_storage` に切り替える。 + +RWX を提供しない StorageClass では PVC が Pending のまま Pod が起動しない +(例: kind の既定 StorageClass は `NodePath only supports ReadWriteOnce ...` で失敗)。 + +### ヘルスチェック用エンドポイントがない + +アプリにも nginx conf にも `/healthz` 相当がない。現状は +nginx は `GET /` (静的 index.html)、web は 5050 の tcpSocket、 +worker は `celery inspect ping` で代替している。 +nginx conf に `location /healthz { return 200; }` を足すのが本来は望ましい。 + +### ミドルウェアは JAIRO Cloud 共通クラスタを使う + +接続先と事前準備は「手順 2」を参照。ここでは設計上の含意だけ。 + +- **DB / vhost / Redis DB 番号は共有資源**。jcgroups 専用に確保してから使う。 + 特に Redis は機関ごとに DB 番号が払い出されているので、空き番号の管理が必要。 +- **アプリ側に接続先を切り替える口は `server.config.toml` しかない** + (`src/server/config.py` に env 読み込み機構がない)。接続先を変えるときは + `params.env` を直して `make` → `deploy` をやり直し、`CONFIG_REVISION` を + 上げて Pod を rollout させる。 +- `deploy/middleware/manifest_template/middleware.yaml` は + `DEPLOY_MIDDLEWARE=true` のときだけ使う**検証用の単一インスタンス構成**。 + こちらは単体 Redis なので `REDIS_CACHE_TYPE=RedisCache` に変えること。 + +### WEKO との結合点 + +実行時に WEKO と共有するのは **Redis db4 のグループキャッシュのみ**。 + +| 項目 | 値 | +|---|---| +| Redis DB | `redis.database.group_cache` = 4 (`REDIS_DB_GROUP_CACHE`) | +| キー | `_gakunin_groups` | +| 値 | Hash: `updated_at` (ISO8601), `groups` (カンマ区切りのグループID) | +| 書き込み | Celery task `update_task()` (`src/server/services/group_caches.py:188`) | +| 読み出し | WEKO の `GROUP_INFO_REDIS_DB = 4` (`instance.cfg`) | + +`src/server/datastore.py:61` の実装上、`app_cache` / `account_store` / +`result_backend` / `group_cache` は**すべて同じ `[redis]` 設定から生成される** +(DB 番号だけが違う)。共通 Redis を使う構成ではこれが都合よく働き、 +`group_cache` が WEKO と同じインスタンスの db4 を指す。 + +逆に言うと、**jcgroups のキャッシュだけ別の Redis に置くことはできない**。 +将来 jcgroups 専用 Redis に移すなら、`config.py` に group_cache 専用の接続先設定を +追加する改修が必要。 + +## 検証済みの内容 + +kind (k8s v1.34) で `STORAGE_TYPE=emptydir` / `REPLICAS=1` として実デプロイして確認した。 + +### 共通構成 + +| 項目 | 結果 | +|---|---| +| `build_images.sh` でイメージ2種のビルド | 成功 (serverName の焼き込み確認まで) | +| `make_jcgroups_manifests.sh` の置換 | 置換漏れなし・TOML / XML の assert 通過 | +| 生成された `server.config.toml` | `tomllib` でパースして値を確認 | +| 本番相当パラメータ (NFS / pull secret / nodeSelector / LoadBalancer / replicas=2 / 機関別証明書2件) のスキーマ検証 | `kubectl apply --dry-run=server --validate=strict` 通過 | +| db-init Job | Completed | +| Pod 起動 | nginx / web / worker の 3/3 Running | +| `GET /` (静的 SPA) / `GET /_nuxt/*.js` | 200 | +| `GET /api/auth/check` (nginx → hostAliases → uwsgi) | 401 `E401` (未認証として正しい応答) | +| `GET /api/dev/accounts` | 404 (prod では dev blueprint 未登録 = 正しい) | +| `GET /Shibboleth.sso/Metadata` | 200 (shibd 稼働) | +| web → worker のファイル共有 (`/var/tmp/jcgroups`) | 疎通 | +| 非 root 実行 (`runAsUser: 1000`) | uwsgi / celery とも正常動作 | +| `delete_jcgroups.sh` → `deploy_jcgroups.sh --skip-db-init` | 既存 DB を保持して再デプロイ成功 | + +### 共通ミドルウェア構成 (`DEPLOY_MIDDLEWARE=false`) + +JAIRO Cloud 共通クラスタと同じ形 (Service 名 / Namespace / Sentinel / headless +Service / `databases 40000`) を kind 上に模擬して確認した。 + +| 項目 | 結果 | +|---|---| +| PostgreSQL ロール作成 → `flask db init` で DB 作成 | `jcgroups` DB が owner=jcgroups で作られ、4 テーブル生成 | +| クロス Namespace 接続 (`pgpool.weko3pg` / `weko-rabbitmq.weko3ra` / `weko-sentinel-service.weko3re`) | すべて疎通 | +| RabbitMQ の専用 vhost | `Connected to amqp://invenio:**@weko-rabbitmq.weko3ra:5672/jcgroups` | +| Sentinel 経由の Redis 接続 (`RedisSentinelCache`, master `mymaster`) | 接続警告なし・read/write 成功 | +| Redis DB 番号の割り当て | `app_cache=100` / `account_store=101` / `group_cache=4` が実際に使われることを Redis 側の keyspace で確認 | +| DB 番号の未指定 / 重複 / Sentinel ノード未指定 | いずれも `make_jcgroups_manifests.sh` が停止 | + +未検証: mAP Core (`sptest.cg.gakunin.jp`) への到達を要する機能、 +Shibboleth 実 IdP との SAML 認証、Ingress コントローラ経由の疎通 +(kind では Service への port-forward で確認)、NFS RWX PVC での複数レプリカ運用、 +実際の共通クラスタ (pgpool の md5 認証・Sentinel 複数台でのフェイルオーバ) への接続。 diff --git a/k8s/deploy/jcgroups/manifest_template/configmap.yaml b/k8s/deploy/jcgroups/manifest_template/configmap.yaml new file mode 100644 index 00000000..e05a3cb3 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/configmap.yaml @@ -0,0 +1,14 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-configmap +data: + # 【注意】アプリの設定は環境変数から読めない。 + # `src/server/config.py` に env 読み込み機構がないため、接続先やシークレットは + # すべて server.config.toml 経由になる (Secret __DOMAIN_NAME__-secret としてマウント)。 + # ここに置けるのは Flask / Python / OS レベルの環境変数だけ。 + TZ: "Asia/Tokyo" + FLASK_ENV: "production" + FLASK_APP: "server.app" + PYTHONUNBUFFERED: "1" diff --git a/k8s/deploy/jcgroups/manifest_template/deploy-web.yaml b/k8s/deploy/jcgroups/manifest_template/deploy-web.yaml new file mode 100644 index 00000000..d5f9c7e1 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/deploy-web.yaml @@ -0,0 +1,286 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-web +spec: + replicas: __REPLICAS__ + selector: + matchLabels: + app: __DOMAIN_NAME__-nginx + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: __DOMAIN_NAME__-nginx + annotations: + # Secret / ConfigMap を作り直したときはこの値を変えて rollout させる。 + # subPath マウントは Secret の更新が Pod に反映されないため。 + config-revision: "__CONFIG_REVISION__" + spec: + # nginx イメージに焼き込まれた `upstream api_server { server web:5050; }` を + # 同一 Pod 内の web コンテナへ向ける。 + # __VHOST__ も自身に向けておく (SP が自分の handlerURL を叩く場合の保険)。 + hostAliases: + - ip: "127.0.0.1" + hostnames: + - web + - __VHOST__ + # 【必須】k8s は Namespace 内の Service ごとに Docker link 互換の環境変数を + # 注入する。Service 名 `redis` に対して REDIS_PORT=tcp://:6379 が + # 入り、weko-group-cache-db の設定フィールド REDIS_PORT (pydantic-settings が + # 環境変数から読む) と衝突して起動に失敗する: + # ValidationError: cache_groups.redis_port + # Input should be a valid integer, unable to parse string as an integer + # [input_value='tcp://10.96.201.60:6379'] + # POSTGRES_PORT / RABBITMQ_PORT も同様に注入されるため、注入自体を止める。 + enableServiceLinks: false + securityContext: + # 共有ストレージ (storage-volume) を uid 1000 の web / worker から + # 読み書きできるようにする。 + fsGroup: 1000 + containers: + # ------------------------------------------------------------------ + # nginx + Shibboleth SP + 静的 SPA + # + # nginx / shibd / shibauthorizer / shibresponder は Unix ドメイン + # ソケット (/opt/shibboleth/*.sock) で通信するため、1 コンテナ内に + # supervisord で同居させる必要がある (nginx/supervisord.conf)。 + # ここだけは 1 プロセス 1 コンテナにできない。 + # ------------------------------------------------------------------ + - name: nginx + # 【重要】このイメージには configs/app.config.ts の serverName が + # ビルド時に静的 SPA へインライン展開されている。 + # vhost ごとにビルドし直したイメージを指定すること + # (k8s/scripts/build_images.sh がその手順)。 + image: __NGINX_IMAGE__ + imagePullPolicy: __IMAGE_PULL_POLICY__ + resources: + limits: + memory: "__NGINX_MEMORY_LIMIT__" + requests: + memory: "128Mi" + ports: + - containerPort: 80 + - containerPort: 443 + volumeMounts: + # nginx のサーバ証明書 + - mountPath: /etc/nginx/server.crt + subPath: server.crt + name: __DOMAIN_NAME__-tls-volume + readOnly: true + - mountPath: /etc/nginx/server.key + subPath: server.key + name: __DOMAIN_NAME__-tls-volume + readOnly: true + # Shibboleth SP の署名 / 暗号化鍵 (nginx/Dockerfile は同じ物を流用) + - mountPath: /etc/shibboleth/server.crt + subPath: server.crt + name: __DOMAIN_NAME__-tls-volume + readOnly: true + - mountPath: /etc/shibboleth/server.key + subPath: server.key + name: __DOMAIN_NAME__-tls-volume + readOnly: true + # entityID / RequestMapper の Host 名が nginx/shibboleth2.xml では + # localhost 固定なので、vhost で置換したものに差し替える。 + - mountPath: /etc/shibboleth/shibboleth2.xml + subPath: shibboleth2.xml + name: __DOMAIN_NAME__-shibboleth2-volume + readOnly: true + # shibd の状態 (セッションキャッシュ / メタデータキャッシュ) + - mountPath: /var/cache/shibboleth + name: __DOMAIN_NAME__-shib-cache-volume + startupProbe: + httpGet: + path: / + port: 443 + scheme: HTTPS + failureThreshold: 30 + periodSeconds: 2 + readinessProbe: + httpGet: + path: / + port: 443 + scheme: HTTPS + timeoutSeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 443 + scheme: HTTPS + timeoutSeconds: 3 + periodSeconds: 20 + # ------------------------------------------------------------------ + # Flask API (uwsgi) + # ------------------------------------------------------------------ + - name: web + image: __APP_IMAGE__ + imagePullPolicy: __IMAGE_PULL_POLICY__ + # イメージ既定の CMD (supervisord -c supervisord.web.conf) は使わない。 + # prod ステージは USER root のまま supervisord が pyuser に降格する構造 + # だが、k8s では 1 コンテナ 1 プロセスにして非 root で直接起動する。 + command: [ "uwsgi", "--ini", "/code/uwsgi.ini" ] + envFrom: + - configMapRef: + name: __DOMAIN_NAME__-configmap + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + resources: + limits: + memory: "__MEMORY_LIMIT__" + requests: + memory: "__MEMORY_REQUEST__" + cpu: "200m" + ports: + - containerPort: 5050 + volumeMounts: + - mountPath: /code/configs/server.config.toml + subPath: server.config.toml + name: __DOMAIN_NAME__-config-volume + readOnly: true + # server.config.toml の [sp] crt / key (既定 ./certs/server.crt)。 + # mAP Core への OAuth トークン発行に使う。 + - mountPath: /code/certs + name: __DOMAIN_NAME__-sp-cert-volume + readOnly: true + # weko-group-cache-db が /var/mnt//server.crt|key を読む。 + - mountPath: /var/mnt + name: __DOMAIN_NAME__-institution-certs-volume + readOnly: true + # [storage.local] temporary / storage が指す先 + - mountPath: /var/tmp/jcgroups + name: __DOMAIN_NAME__-storage-volume + # uwsgi は uwsgi プロトコルを話すため HTTP プローブは使えない。 + startupProbe: + tcpSocket: + port: 5050 + failureThreshold: 30 + periodSeconds: 2 + readinessProbe: + tcpSocket: + port: 5050 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 5050 + periodSeconds: 20 + # ------------------------------------------------------------------ + # Celery worker + # + # グループキャッシュ更新タスク update_task() がここで動き、 + # WEKO が読む Redis db4 のキーを書く。 + # ------------------------------------------------------------------ + - name: worker + image: __APP_IMAGE__ + imagePullPolicy: __IMAGE_PULL_POLICY__ + # 【重要】supervisord.worker.conf の `celery -A server.celery_app worker` + # には --concurrency 指定がない。Celery はノードの CPU 数だけ prefork + # するため、CPU limit を付けてもコア数分 fork してメモリを食い潰す。 + # 必ず明示すること。 + command: + - celery + - -A + - server.celery_app + - worker + - --concurrency + - "__CELERY_CONCURRENCY__" + - --loglevel + - INFO + envFrom: + - configMapRef: + name: __DOMAIN_NAME__-configmap + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + resources: + limits: + memory: "__WORKER_MEMORY_LIMIT__" + requests: + memory: "__MEMORY_REQUEST__" + cpu: "200m" + volumeMounts: + - mountPath: /code/configs/server.config.toml + subPath: server.config.toml + name: __DOMAIN_NAME__-config-volume + readOnly: true + - mountPath: /code/certs + name: __DOMAIN_NAME__-sp-cert-volume + readOnly: true + - mountPath: /var/mnt + name: __DOMAIN_NAME__-institution-certs-volume + readOnly: true + # web がアップロードしたファイルを worker が読むので同じ物を見せる + - mountPath: /var/tmp/jcgroups + name: __DOMAIN_NAME__-storage-volume + # HTTP ポートを持たないので celery inspect ping で見る。 + startupProbe: + exec: + command: + - sh + - -c + - "celery -A server.celery_app inspect ping -d celery@$(hostname)" + failureThreshold: 30 + periodSeconds: 5 + timeoutSeconds: 15 + livenessProbe: + exec: + command: + - sh + - -c + - "celery -A server.celery_app inspect ping -d celery@$(hostname)" + periodSeconds: 60 + timeoutSeconds: 15 + failureThreshold: 3 + #__IMAGE_PULL_SECRET__imagePullSecrets: + #__IMAGE_PULL_SECRET__ - name: __IMAGE_PULL_SECRET_NAME__ + restartPolicy: Always + volumes: + - name: __DOMAIN_NAME__-config-volume + secret: + secretName: __DOMAIN_NAME__-secret + - name: __DOMAIN_NAME__-tls-volume + secret: + secretName: __DOMAIN_NAME__-tls + # shibd (_shibd ユーザ) が鍵を読む必要がある。 + # nginx/Dockerfile が chmod 644 しているのと同じ理由。 + defaultMode: 0644 + - name: __DOMAIN_NAME__-shibboleth2-volume + configMap: + name: __DOMAIN_NAME__-shibboleth2 + - name: __DOMAIN_NAME__-shib-cache-volume + emptyDir: {} + - name: __DOMAIN_NAME__-sp-cert-volume + secret: + secretName: __DOMAIN_NAME__-sp-cert + defaultMode: 0400 + - name: __DOMAIN_NAME__-institution-certs-volume + secret: + secretName: __DOMAIN_NAME__-institution-certs + defaultMode: 0400 + optional: true + # Secret のキー名に "/" は使えないので、機関ごとの + # /var/mnt//server.crt|key は items[].path で組み立てる。 + # この行は make_jcgroups_manifests.sh が + # INSTITUTION_CERTS_DIR の中身から生成した items: に差し替える。 + #__INSTITUTION_CERT_ITEMS__ + # STORAGE_TYPE に応じて make_jcgroups_manifests.sh が片方だけ残す。 + #__STORAGE_PVC__- name: __DOMAIN_NAME__-storage-volume + #__STORAGE_PVC__ persistentVolumeClaim: + #__STORAGE_PVC__ claimName: __DOMAIN_NAME__-storage-pvc + #__STORAGE_EMPTYDIR__- name: __DOMAIN_NAME__-storage-volume + #__STORAGE_EMPTYDIR__ emptyDir: {} + #__NODE_SELECTOR__nodeSelector: + #__NODE_SELECTOR__ nodeType: __NODE_TYPE__ + +status: {} diff --git a/k8s/deploy/jcgroups/manifest_template/ingress.yaml b/k8s/deploy/jcgroups/manifest_template/ingress.yaml new file mode 100644 index 00000000..88b1c0bc --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/ingress.yaml @@ -0,0 +1,38 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-ingress + annotations: + # 【重要】バックエンドは必ず HTTPS で叩く。 + # Shibboleth SP は handlerSSL="true" / cookieProps="https" で動くため、 + # Ingress で TLS を落として HTTP で Pod に渡すと SP のハンドラが動かない。 + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.org/ssl-services: "__DOMAIN_NAME__-nginx" + nginx.org/server-tokens: "off" + # SP のメタデータ / CSV インポートで大きめの body を通す。 + # (server.config.toml の [api] max_upload_size に合わせる) + nginx.ingress.kubernetes.io/proxy-body-size: "__MAX_UPLOAD_SIZE__" +spec: + tls: + - hosts: + - __VHOST__ + secretName: __DOMAIN_NAME__-cert + ingressClassName: __INGRESS_CLASS__ + rules: + - host: __VHOST__ + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: __DOMAIN_NAME__-nginx + port: + number: 443 + +# SP に TLS ごと素通しさせたい場合は、この Ingress を使わずに +# Service を type: LoadBalancer にして 443 を直接公開するか、 +# ingress-nginx の ssl-passthrough (--enable-ssl-passthrough + +# nginx.ingress.kubernetes.io/ssl-passthrough: "true") を使う。 +# その場合クライアントに見せる証明書は __DOMAIN_NAME__-tls の方になる。 diff --git a/k8s/deploy/jcgroups/manifest_template/job-db-init.yaml b/k8s/deploy/jcgroups/manifest_template/job-db-init.yaml new file mode 100644 index 00000000..49cb3b10 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/job-db-init.yaml @@ -0,0 +1,57 @@ +--- +# DB 初期化 Job +# +# テーブル作成は自動実行されない。初回デプロイ時と、モデル追加時に実行する。 +# flask db init … データベースを作る (既にあれば何もしない) +# flask db create … テーブルを作る +# +# deploy_jcgroups.sh は deploy-web.yaml より先にこの Job を完了させる。 +# 再実行するときは `kubectl delete job` してから apply すること +# (Job の spec は immutable なので上書き apply できない)。 +apiVersion: batch/v1 +kind: Job +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-db-init +spec: + backoffLimit: 4 + template: + metadata: + labels: + app: __DOMAIN_NAME__-db-init + spec: + # deploy-web.yaml と同じ理由で必須 (REDIS_PORT 等の注入を止める) + enableServiceLinks: false + restartPolicy: OnFailure + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + containers: + - name: db-init + image: __APP_IMAGE__ + imagePullPolicy: __IMAGE_PULL_POLICY__ + # FLASK_APP=server.app はイメージの ENV と ConfigMap の両方で設定済み + command: [ "sh", "-c", "flask db init && flask db create" ] + envFrom: + - configMapRef: + name: __DOMAIN_NAME__-configmap + volumeMounts: + - mountPath: /code/configs/server.config.toml + subPath: server.config.toml + name: __DOMAIN_NAME__-config-volume + readOnly: true + resources: + limits: + memory: "512Mi" + requests: + memory: "256Mi" + cpu: "100m" + #__IMAGE_PULL_SECRET__imagePullSecrets: + #__IMAGE_PULL_SECRET__ - name: __IMAGE_PULL_SECRET_NAME__ + volumes: + - name: __DOMAIN_NAME__-config-volume + secret: + secretName: __DOMAIN_NAME__-secret + #__NODE_SELECTOR__nodeSelector: + #__NODE_SELECTOR__ nodeType: __NODE_TYPE__ diff --git a/k8s/deploy/jcgroups/manifest_template/namespace.yaml b/k8s/deploy/jcgroups/manifest_template/namespace.yaml new file mode 100644 index 00000000..dfe41441 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: __NAMESPACE__ diff --git a/k8s/deploy/jcgroups/manifest_template/secret.yaml b/k8s/deploy/jcgroups/manifest_template/secret.yaml new file mode 100644 index 00000000..46b956ea --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/secret.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Secret +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-middleware +type: Opaque +stringData: + # deploy/middleware (検証用 PostgreSQL) が参照する。 + # server.config.toml の [postgres] password と同じ値が入るよう + # make_jcgroups_manifests.sh が両方へ同じ __POSTGRES_PASSWORD__ を展開する。 + POSTGRES_PASSWORD: "__POSTGRES_PASSWORD__" + +# 以下の Secret はファイルから作るため deploy_jcgroups.sh が +# `kubectl create secret` で作成する (マニフェスト化すると中身が Git に載る)。 +# +# __DOMAIN_NAME__-secret : server.config.toml +# __DOMAIN_NAME__-tls : nginx / Shibboleth SP のサーバ証明書 +# __DOMAIN_NAME__-cert : Ingress 用 TLS Secret (kubernetes.io/tls) +# __DOMAIN_NAME__-sp-cert : mAP Core 接続用クライアント証明書 +# __DOMAIN_NAME__-institution-certs : 機関別クライアント証明書 (任意) diff --git a/k8s/deploy/jcgroups/manifest_template/service.yaml b/k8s/deploy/jcgroups/manifest_template/service.yaml new file mode 100644 index 00000000..807be6f0 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/service.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-nginx +spec: + type: __SERVICE_TYPE__ + selector: + app: __DOMAIN_NAME__-nginx + # クライアント IP を保持する (Shibboleth SP のアクセス制御・ログ用)。 + # ClusterIP では指定できないフィールドなので、SERVICE_TYPE=ClusterIP のときは + # make_jcgroups_manifests.sh がこの行を削除する。 + #__EXTERNAL_TRAFFIC_POLICY__externalTrafficPolicy: Local + ports: + - name: nginx-http-port + protocol: TCP + port: 80 + targetPort: 80 + - name: nginx-https-port + protocol: TCP + port: 443 + targetPort: 443 + +# アプリ (uwsgi) 向けの Service は作らない。 +# nginx イメージには `upstream api_server { server web:5050; }` が焼き込まれて +# いる (nginx/conf.d/default.conf) が、deploy-web.yaml の hostAliases で +# `web` を 127.0.0.1 に向けているため、同一 Pod 内のアプリコンテナへ直接届く。 +# Service 経由にすると nginx が起動時に解決した ClusterIP をキャッシュし続ける +# 問題を抱えるので、同居させたこの構成のほうが素直。 diff --git a/k8s/deploy/jcgroups/manifest_template/volume-pv.yaml b/k8s/deploy/jcgroups/manifest_template/volume-pv.yaml new file mode 100644 index 00000000..67a36f71 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/volume-pv.yaml @@ -0,0 +1,26 @@ +--- +# web / worker が共有するファイル置き場の PV。 +# server.config.toml の [storage.local] temporary / storage (= /var/tmp/jcgroups) +# が指す先で、web がアップロードした CSV を Celery worker が読む。 +# +# replicas > 1 で運用するなら Pod をまたいで同じ物が見える必要があるため +# ReadWriteMany (NFS) にしている。RWX を用意できない場合は +# server.config.toml の [storage] type を object_storage に切り替えるか、 +# STORAGE_TYPE=emptydir + REPLICAS=1 で運用すること。 +apiVersion: v1 +kind: PersistentVolume +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-storage-pv + labels: + volume: __DOMAIN_NAME__-storage-nfs +spec: + capacity: + storage: __STORAGE_SIZE__ + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: __STORAGE_CLASS__ + nfs: + server: __NFS_SERVER__ + path: __NFS_PATH__ diff --git a/k8s/deploy/jcgroups/manifest_template/volume-pvc.yaml b/k8s/deploy/jcgroups/manifest_template/volume-pvc.yaml new file mode 100644 index 00000000..a65b84c9 --- /dev/null +++ b/k8s/deploy/jcgroups/manifest_template/volume-pvc.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-storage-pvc +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: __STORAGE_SIZE__ + storageClassName: "__STORAGE_CLASS__" + selector: + matchLabels: + volume: __DOMAIN_NAME__-storage-nfs diff --git a/k8s/deploy/jcgroups/params.env.example b/k8s/deploy/jcgroups/params.env.example new file mode 100644 index 00000000..adbe3436 --- /dev/null +++ b/k8s/deploy/jcgroups/params.env.example @@ -0,0 +1,166 @@ +# jairocloud-groups-manager の k8s デプロイパラメータ +# +# make_jcgroups_manifests.sh がこのファイルを読み込み (sh の . で source)、 +# manifest_template/*.yaml の __PLACEHOLDER__ を置換する。 +# 環境ごとにコピーして使うこと (params.env は .gitignore 済み)。 + +# ============================================================ +# デプロイ先 +# ============================================================ +# 公開する FQDN。Ingress の host、Shibboleth SP の entityID、 +# server.config.toml の server_name に展開される。 +VHOST=groups.example.ac.jp + +NAMESPACE=jcgroups + +# ============================================================ +# イメージ +# ============================================================ +# build_images.sh が出力したイメージを指定する。 +# 【重要】nginx イメージは VHOST ごとにビルドし直したものを使うこと +# (serverName が静的 SPA へビルド時に焼き込まれる)。 +APP_IMAGE=nrt.ocir.io/xxxxxxxx/jc-groups-manager:20260730 +NGINX_IMAGE=nrt.ocir.io/xxxxxxxx/jc-groups-manager-nginx:20260730 +IMAGE_PULL_POLICY=Always + +# プライベートレジストリの pull secret 名。空にすると imagePullSecrets を出力しない。 +IMAGE_PULL_SECRET_NAME=ocir-secret + +# nodeSelector の nodeType ラベル値。空にすると nodeSelector を出力しない。 +NODE_TYPE= + +# ============================================================ +# リソース +# ============================================================ +REPLICAS=2 +MEMORY_REQUEST=512Mi +MEMORY_LIMIT=1Gi +WORKER_MEMORY_LIMIT=2Gi +NGINX_MEMORY_LIMIT=512Mi + +# Celery の prefork 数。指定しないとノードの CPU 数だけ fork するので必須。 +CELERY_CONCURRENCY=4 + +# Secret / ConfigMap を作り直したときに増やす (Pod の rollout 用)。 +CONFIG_REVISION=1 + +# ============================================================ +# 公開方法 +# ============================================================ +# NodePort / LoadBalancer / ClusterIP +# ClusterIP のときは externalTrafficPolicy を出力しない。 +SERVICE_TYPE=NodePort +INGRESS_CLASS=nginx +# Ingress の proxy-body-size。server.config.toml の [api] max_upload_size に合わせる。 +MAX_UPLOAD_SIZE=10m + +# ============================================================ +# 共有ストレージ (/var/tmp/jcgroups) +# ============================================================ +# nfs : NFS の PV/PVC を作って ReadWriteMany で共有する (本番) +# emptydir : Pod ローカル。REPLICAS=1 の検証用に限る +STORAGE_TYPE=nfs +STORAGE_SIZE=20Gi +STORAGE_CLASS=nfs +NFS_SERVER=10.76.0.12 +NFS_PATH=/fs-jcgroups/groups.example.ac.jp + +# ============================================================ +# アプリ設定 (server.config.toml へ展開) +# ============================================================ +# 空にすると make スクリプトが openssl rand -hex 32 で生成する。 +SECRET_KEY= + +# ------------------------------------------------------------ +# PostgreSQL +# +# JAIRO Cloud 共通クラスタ (Zalando operator + pgpool) を使う場合: +# POSTGRES_HOST=pgpool.weko3pg +# ロールは事前に用意し、DB 作成は db-init Job (flask db init) に任せる。 +# そのため POSTGRES_USER には CREATEDB 権限が必要 (README 参照)。 +# ------------------------------------------------------------ +POSTGRES_HOST=pgpool.weko3pg +POSTGRES_PORT=5432 +POSTGRES_DB=jcgroups +POSTGRES_USER=jcgroups +POSTGRES_PASSWORD=CHANGE_ME + +# ------------------------------------------------------------ +# Redis +# +# RedisCache : 単体 Redis に接続する (検証用) +# RedisSentinelCache : Sentinel 経由で master に接続する +# (JAIRO Cloud 共通クラスタはこちら) +# ------------------------------------------------------------ +REDIS_CACHE_TYPE=RedisSentinelCache + +# RedisSentinelCache のとき使う。ノードは "host:port" を空白区切りで並べる。 +# 共通クラスタの Sentinel は headless Service なので、この1件で +# 全 Sentinel Pod の IP が引ける (WEKO の instance.cfg も同じ書き方)。 +REDIS_SENTINEL_NODES=weko-sentinel-service.weko3re:26379 +REDIS_SENTINEL_MASTER=mymaster + +# RedisCache のとき使う。 +REDIS_URL=redis://redis:6379 + +# 使用する Redis DB 番号。 +# +# 【重要】共通 Redis は 1 インスタンスを全機関で共有し (databases 40000)、 +# DB 番号は機関ごとに割り当て済み。空いている番号を運用側で確保してから書くこと。 +# WEKO 側で予約済みの番号: +# 0/1/2 系 … 機関ごとに CACHE_REDIS_DB / ACCOUNTS_SESSION_REDIS_DB_NO / +# CELERY_RESULT_BACKEND_DB_NO として払い出し済み +# 3 … CRAWLER_REDIS_DB (固定) +# 4 … GROUP_INFO_REDIS_DB (固定) +REDIS_DB_APP_CACHE= +REDIS_DB_ACCOUNT_STORE= +REDIS_DB_RESULT_BACKEND= + +# グループキャッシュの DB 番号。WEKO の GROUP_INFO_REDIS_DB = 4 が固定なので +# 4 のまま変えないこと (ここが WEKO との唯一の実行時結合点)。 +REDIS_DB_GROUP_CACHE=4 + +# ------------------------------------------------------------ +# RabbitMQ +# +# JAIRO Cloud 共通クラスタを使う場合は vhost を事前に作る (README 参照): +# amqp://invenio:@weko-rabbitmq.weko3ra:5672/jcgroups +# vhost 名は URL の最後のパス要素。`//` は既定 vhost "/" の意味。 +# ------------------------------------------------------------ +RABBITMQ_URL=amqp://invenio:CHANGE_ME@weko-rabbitmq.weko3ra:5672/jcgroups + +MAP_CORE_BASE_URL=https://sptest.cg.gakunin.jp + +# ============================================================ +# 証明書 +# ============================================================ +# nginx / Shibboleth SP のサーバ証明書。 +# 空にすると make スクリプトが自己署名証明書を生成する (検証用)。 +TLS_CRT= +TLS_KEY= + +# mAP Core 接続用クライアント証明書 ([sp] crt / key)。 +# 空にすると TLS_CRT / TLS_KEY と同じものを流用する。 +SP_CRT= +SP_KEY= + +# 機関別クライアント証明書のディレクトリ。 +# /<機関FQDN>/server.crt +# /<機関FQDN>/server.key +# の構成で置いておくと /var/mnt/<機関FQDN>/server.crt|key にマウントされる。 +INSTITUTION_CERTS_DIR= + +# ============================================================ +# ミドルウェア +# ============================================================ +# JAIRO Cloud 共通クラスタ (pgpool.weko3pg / weko-sentinel-service.weko3re / +# weko-rabbitmq.weko3ra) を使うので false のままにする。 +# +# true にすると検証用の単一インスタンス PostgreSQL / Redis / RabbitMQ を +# 同じ Namespace に立てる。そのときは接続先も +# POSTGRES_HOST=postgres +# REDIS_CACHE_TYPE=RedisCache / REDIS_URL=redis://redis:6379 +# RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672// +# に変えること。 +DEPLOY_MIDDLEWARE=false +PGDATA_SIZE=10Gi diff --git a/k8s/deploy/middleware/manifest_template/middleware.yaml b/k8s/deploy/middleware/manifest_template/middleware.yaml new file mode 100644 index 00000000..40f70263 --- /dev/null +++ b/k8s/deploy/middleware/manifest_template/middleware.yaml @@ -0,0 +1,194 @@ +--- +# ミドルウェア (PostgreSQL / Redis / RabbitMQ) +# +# 【注意】これは検証・動作確認用の単一インスタンス構成である。 +# 本番では chart / Operator に置き換え、DEPLOY_MIDDLEWARE=false にして +# params.env の POSTGRES_HOST / REDIS_URL / RABBITMQ_URL を +# 既存クラスタへ向けること: +# - PostgreSQL : CloudNativePG, Zalando postgres-operator +# - Redis : bitnami/redis (Sentinel 構成にするなら server.config.toml の +# cache_type を RedisSentinelCache にして [redis.sentinel] を埋める) +# - RabbitMQ : rabbitmq-cluster-operator +# +# Service 名は params.env の既定値 (postgres / redis / rabbitmq) に揃えてある。 + +apiVersion: v1 +kind: Service +metadata: + namespace: __NAMESPACE__ + name: postgres +spec: + selector: + app: __DOMAIN_NAME__-postgres + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-pgdata-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: __PGDATA_SIZE__ + # storageClassName は指定しない (クラスタ既定の StorageClass を使う) + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-postgres +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: __DOMAIN_NAME__-postgres + template: + metadata: + labels: + app: __DOMAIN_NAME__-postgres + spec: + enableServiceLinks: false + containers: + - name: postgres + image: postgres:12 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "__POSTGRES_DB__" + - name: POSTGRES_USER + value: "__POSTGRES_USER__" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: __DOMAIN_NAME__-middleware + key: POSTGRES_PASSWORD + # PVC のルートに lost+found がある環境でも初期化できるように + # サブディレクトリを使う。 + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: pgdata + readinessProbe: + exec: + command: [ "pg_isready", "-U", "__POSTGRES_USER__", "-d", "__POSTGRES_DB__" ] + periodSeconds: 10 + resources: + limits: + memory: "1Gi" + requests: + memory: "256Mi" + cpu: "100m" + volumes: + - name: pgdata + persistentVolumeClaim: + claimName: __DOMAIN_NAME__-pgdata-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + namespace: __NAMESPACE__ + name: redis +spec: + selector: + app: __DOMAIN_NAME__-redis + ports: + - port: 6379 + targetPort: 6379 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-redis +spec: + replicas: 1 + selector: + matchLabels: + app: __DOMAIN_NAME__-redis + template: + metadata: + labels: + app: __DOMAIN_NAME__-redis + spec: + enableServiceLinks: false + containers: + - name: redis + image: redis:7.4.1 + args: [ "redis-server", "--appendonly", "yes" ] + ports: + - containerPort: 6379 + readinessProbe: + exec: + command: [ "redis-cli", "ping" ] + periodSeconds: 10 + resources: + limits: + memory: "512Mi" + requests: + memory: "128Mi" + cpu: "50m" + +--- +apiVersion: v1 +kind: Service +metadata: + namespace: __NAMESPACE__ + name: rabbitmq +spec: + selector: + app: __DOMAIN_NAME__-rabbitmq + ports: + - port: 5672 + targetPort: 5672 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: __NAMESPACE__ + name: __DOMAIN_NAME__-rabbitmq +spec: + replicas: 1 + selector: + matchLabels: + app: __DOMAIN_NAME__-rabbitmq + template: + metadata: + labels: + app: __DOMAIN_NAME__-rabbitmq + spec: + enableServiceLinks: false + containers: + - name: rabbitmq + image: rabbitmq:4.0.2 + ports: + - containerPort: 5672 + env: + # compose.prod.yaml と同じ。長時間タスク用にタイムアウトを延長。 + - name: RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS + value: "-rabbit consumer_timeout 10800000" + readinessProbe: + exec: + command: [ "rabbitmq-diagnostics", "-q", "ping" ] + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 10 + resources: + limits: + memory: "1Gi" + requests: + memory: "256Mi" + cpu: "100m" diff --git a/k8s/scripts/build_images.sh b/k8s/scripts/build_images.sh new file mode 100755 index 00000000..6ef4f11a --- /dev/null +++ b/k8s/scripts/build_images.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# +# 目的: k8s へデプロイするコンテナイメージを2つビルドする +# 1. アプリイメージ (uwsgi / celery / flask CLI 兼用) +# 2. nginx イメージ (nginx + Shibboleth SP + 静的 SPA) +# +# 引数 +# 1. vhost : 公開する FQDN。Nuxt の serverName としてイメージに焼き込まれる +# 2. registry : プッシュ先レジストリ (例 nrt.ocir.io/xxxxx/jcgroups) +# 3. tag : イメージタグ +# 4. --push : 指定するとビルド後に docker push する (任意) +# +# 例 +# ./build_images.sh groups.example.ac.jp nrt.ocir.io/nrbslpthdcco/jc 20260730 --push +# +# 【重要】nginx イメージは vhost ごとにビルドし直す必要がある。 +# configs/app.config.ts の serverName は Nuxt のビルド時に静的 SPA へ +# インライン展開されるため、実行時に環境変数で差し替えられない。 + +set -e + +if [ $# -lt 3 ]; then + echo "usage : $0 vhost registry tag [--push]" + exit 3 +fi + +VHOST=$1 +REGISTRY=$2 +TAG=$3 +PUSH=${4:-} + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd) + +APP_IMAGE="$REGISTRY/jc-groups-manager:$TAG" +NGINX_IMAGE="$REGISTRY/jc-groups-manager-nginx:$TAG" + +# アプリイメージは `COPY . .` で configs/server.config.toml を焼き込む。 +# 開発者ログインが有効な TOML を焼き込むと、その値がイメージに残る。 +# 実行時は Secret でマウント上書きするので実害は出にくいが、 +# 事故を防ぐためビルド前に弾く。 +if grep -qE '^[[:space:]]*developer_login[[:space:]]*=[[:space:]]*true' \ + "$REPO_ROOT/configs/server.config.toml"; then + echo "ERROR: configs/server.config.toml で developer_login = true になっている。" >&2 + echo " このままイメージへ焼き込まれるため、コメントアウトしてからビルドすること。" >&2 + exit 5 +fi + +cd "$REPO_ROOT" + +echo "=== build $APP_IMAGE ===" +docker build --target prod -t "$APP_IMAGE" . + +echo "=== build $NGINX_IMAGE (serverName=$VHOST) ===" +docker build --target prod \ + -f nginx/Dockerfile \ + --build-arg "SERVER_NAME=$VHOST" \ + -t "$NGINX_IMAGE" . + +# serverName が実際に焼き込まれたか、生成物を grep して確認する。 +echo "=== verify serverName in the generated SPA ===" +docker run --rm --entrypoint sh "$NGINX_IMAGE" -c \ + "grep -rl '$VHOST' /usr/share/nginx/html/_nuxt/ | head -1" \ + || { echo "ERROR: serverName=$VHOST が静的 SPA に含まれていない" >&2; exit 6; } + +if [ "$PUSH" = "--push" ]; then + echo "=== push ===" + docker push "$APP_IMAGE" + docker push "$NGINX_IMAGE" +fi + +echo "" +echo "APP_IMAGE = $APP_IMAGE" +echo "NGINX_IMAGE = $NGINX_IMAGE" diff --git a/k8s/scripts/delete_jcgroups.sh b/k8s/scripts/delete_jcgroups.sh new file mode 100755 index 00000000..2fd79b9c --- /dev/null +++ b/k8s/scripts/delete_jcgroups.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# +# 目的: deploy_jcgroups.sh で作ったリソースを消す +# +# 引数 +# 1. generated_dir : make_jcgroups_manifests.sh の出力ディレクトリ +# 2. --all : Namespace / PV / PVC も消す (データが消えるので既定では残す) +# +# 例 +# ./delete_jcgroups.sh /tmp/generated/groups.example.ac.jp +# ./delete_jcgroups.sh /tmp/generated/groups.example.ac.jp --all + +set -e + +if [ $# -lt 1 ] || [ ! -d "$1" ]; then + echo "usage : $0 generated_dir [--all]" + exit 3 +fi + +GEN_DIR=$(cd "$1" && pwd) +MODE=${2:-} +MANIFEST_DIR="$GEN_DIR/manifests" + +# shellcheck disable=SC1091 +. "$GEN_DIR/params.env" +: "${NAMESPACE:=jcgroups}" +DOMAIN_NAME=$(echo "$VHOST" | tr '._' '-') +KC="kubectl -n $NAMESPACE" + +echo "=== delete $VHOST (namespace=$NAMESPACE) ===" + +$KC delete -f "$MANIFEST_DIR/deploy-web.yaml" --ignore-not-found +$KC delete job "$DOMAIN_NAME-db-init" --ignore-not-found +$KC delete -f "$MANIFEST_DIR/ingress.yaml" --ignore-not-found +$KC delete -f "$MANIFEST_DIR/service.yaml" --ignore-not-found + +if [ -f "$MANIFEST_DIR/middleware.yaml" ]; then + $KC delete -f "$MANIFEST_DIR/middleware.yaml" --ignore-not-found +fi + +$KC delete secret \ + "$DOMAIN_NAME-secret" \ + "$DOMAIN_NAME-tls" \ + "$DOMAIN_NAME-cert" \ + "$DOMAIN_NAME-sp-cert" \ + "$DOMAIN_NAME-institution-certs" \ + "$DOMAIN_NAME-middleware" --ignore-not-found +$KC delete configmap "$DOMAIN_NAME-configmap" "$DOMAIN_NAME-shibboleth2" --ignore-not-found + +if [ "$MODE" = "--all" ]; then + echo "--- PVC / PV / Namespace も削除する (データが消える)" + if [ -f "$MANIFEST_DIR/volume-pvc.yaml" ]; then + $KC delete -f "$MANIFEST_DIR/volume-pvc.yaml" --ignore-not-found + fi + $KC delete pvc "$DOMAIN_NAME-pgdata-pvc" --ignore-not-found + if [ -f "$MANIFEST_DIR/volume-pv.yaml" ]; then + kubectl delete -f "$MANIFEST_DIR/volume-pv.yaml" --ignore-not-found + fi + kubectl delete namespace "$NAMESPACE" --ignore-not-found +fi + +echo "done" diff --git a/k8s/scripts/deploy_jcgroups.sh b/k8s/scripts/deploy_jcgroups.sh new file mode 100755 index 00000000..a0908f4c --- /dev/null +++ b/k8s/scripts/deploy_jcgroups.sh @@ -0,0 +1,158 @@ +#!/bin/sh +# +# 目的: make_jcgroups_manifests.sh が生成したディレクトリを k8s へ適用する +# +# 引数 +# 1. generated_dir : / (manifests / conf / ssl / sp を含む) +# 2. --skip-db-init: DB 初期化 Job を実行しない (2回目以降のデプロイ) +# +# 例 +# ./deploy_jcgroups.sh /tmp/generated/groups.example.ac.jp +# ./deploy_jcgroups.sh /tmp/generated/groups.example.ac.jp --skip-db-init +# +# ファイル由来の Secret (server.config.toml / 各証明書) はマニフェスト化すると +# 中身が Git に載るので、ここで kubectl create secret して作る。 + +set -e + +if [ $# -lt 1 ] || [ ! -d "$1" ]; then + echo "usage : $0 generated_dir [--skip-db-init]" + exit 3 +fi + +GEN_DIR=$(cd "$1" && pwd) +SKIP_DB_INIT=${2:-} + +MANIFEST_DIR="$GEN_DIR/manifests" +CONF_DIR="$GEN_DIR/conf" +SSL_DIR="$GEN_DIR/ssl" +SP_DIR="$GEN_DIR/sp" +INST_DIR="$GEN_DIR/institutions" + +if [ ! -f "$GEN_DIR/params.env" ]; then + echo "ERROR: $GEN_DIR/params.env がない。make_jcgroups_manifests.sh の出力先を指定すること" >&2 + exit 3 +fi + +# shellcheck disable=SC1091 +. "$GEN_DIR/params.env" +: "${NAMESPACE:=jcgroups}" +DOMAIN_NAME=$(echo "$VHOST" | tr '._' '-') + +KC="kubectl -n $NAMESPACE" + +date +echo "=== deploy $VHOST (namespace=$NAMESPACE) ===" + +# ------------------------------------------------------------ +# Namespace +# ------------------------------------------------------------ +kubectl apply -f "$MANIFEST_DIR/namespace.yaml" + +# ------------------------------------------------------------ +# ファイル由来の Secret / ConfigMap +# +# 何度でも流せるように create --dry-run=client | apply の形にする。 +# ------------------------------------------------------------ +apply_secret() { + # $1 以降を kubectl create secret の引数として渡す + $KC create secret "$@" --dry-run=client -o yaml | $KC apply -f - +} + +echo "--- secrets" +apply_secret generic "$DOMAIN_NAME-secret" \ + --from-file=server.config.toml="$CONF_DIR/server.config.toml" + +# nginx / Shibboleth SP のサーバ証明書 (Pod にマウントする) +apply_secret generic "$DOMAIN_NAME-tls" \ + --from-file=server.crt="$SSL_DIR/server.crt" \ + --from-file=server.key="$SSL_DIR/server.key" + +# Ingress の TLS Secret (kubernetes.io/tls 形式が要る) +apply_secret tls "$DOMAIN_NAME-cert" \ + --cert="$SSL_DIR/server.crt" \ + --key="$SSL_DIR/server.key" + +# mAP Core 接続用クライアント証明書 ([sp] crt / key) +apply_secret generic "$DOMAIN_NAME-sp-cert" \ + --from-file=server.crt="$SP_DIR/server.crt" \ + --from-file=server.key="$SP_DIR/server.key" + +# 機関別クライアント証明書。キー名は .server.crt|key +# (make_jcgroups_manifests.sh が deploy-web.yaml の items[].path をこの規則で作る) +if [ -d "$INST_DIR" ]; then + set -- + for d in "$INST_DIR"/*; do + [ -d "$d" ] || continue + fqdn=$(basename "$d") + set -- "$@" \ + --from-file="$fqdn.server.crt=$d/server.crt" \ + --from-file="$fqdn.server.key=$d/server.key" + done + if [ $# -gt 0 ]; then + apply_secret generic "$DOMAIN_NAME-institution-certs" "$@" + fi +fi + +echo "--- configmaps" +$KC create configmap "$DOMAIN_NAME-shibboleth2" \ + --from-file=shibboleth2.xml="$CONF_DIR/shibboleth2.xml" \ + --dry-run=client -o yaml | $KC apply -f - + +# ------------------------------------------------------------ +# マニフェスト (Deployment 以外) +# ------------------------------------------------------------ +echo "--- manifests" +kubectl apply -f "$MANIFEST_DIR/configmap.yaml" +# 検証用ミドルウェアを立てるときだけ生成される (PostgreSQL のパスワード) +if [ -f "$MANIFEST_DIR/secret.yaml" ]; then + kubectl apply -f "$MANIFEST_DIR/secret.yaml" +fi +# STORAGE_TYPE=emptydir では生成されない +if [ -f "$MANIFEST_DIR/volume-pv.yaml" ]; then + kubectl apply -f "$MANIFEST_DIR/volume-pv.yaml" +fi +if [ -f "$MANIFEST_DIR/volume-pvc.yaml" ]; then + kubectl apply -f "$MANIFEST_DIR/volume-pvc.yaml" +fi +kubectl apply -f "$MANIFEST_DIR/service.yaml" +kubectl apply -f "$MANIFEST_DIR/ingress.yaml" + +if [ -f "$MANIFEST_DIR/middleware.yaml" ]; then + echo "--- middleware (検証用)" + kubectl apply -f "$MANIFEST_DIR/middleware.yaml" + $KC wait --for=condition=available --timeout=300s \ + "deployment/$DOMAIN_NAME-postgres" \ + "deployment/$DOMAIN_NAME-redis" \ + "deployment/$DOMAIN_NAME-rabbitmq" +fi + +# ------------------------------------------------------------ +# DB 初期化 Job +# +# Job の spec は immutable なので、作り直す前に消す。 +# ------------------------------------------------------------ +if [ "$SKIP_DB_INIT" = "--skip-db-init" ]; then + echo "--- db-init: スキップ" +else + echo "--- db-init" + $KC delete job "$DOMAIN_NAME-db-init" --ignore-not-found + kubectl apply -f "$MANIFEST_DIR/job-db-init.yaml" + if ! $KC wait --for=condition=complete --timeout=300s "job/$DOMAIN_NAME-db-init"; then + echo "ERROR: db-init Job が完了しなかった。ログ:" >&2 + $KC logs "job/$DOMAIN_NAME-db-init" --tail=50 >&2 || true + exit 7 + fi +fi + +# ------------------------------------------------------------ +# 本体 (nginx + web + worker) +# ------------------------------------------------------------ +echo "--- deploy-web" +kubectl apply -f "$MANIFEST_DIR/deploy-web.yaml" +$KC rollout status "deployment/$DOMAIN_NAME-web" --timeout=600s + +echo "" +$KC get pod -l "app=$DOMAIN_NAME-nginx" -o wide +$KC get svc "$DOMAIN_NAME-nginx" +date diff --git a/k8s/scripts/make_jcgroups_manifests.sh b/k8s/scripts/make_jcgroups_manifests.sh new file mode 100755 index 00000000..6556527d --- /dev/null +++ b/k8s/scripts/make_jcgroups_manifests.sh @@ -0,0 +1,515 @@ +#!/bin/sh +# +# 目的: params.env を読み、manifest_template/*.yaml の __PLACEHOLDER__ を +# 置換した実マニフェストと、環境用の設定ファイル / 証明書を生成する +# +# 引数 +# 1. params_file : パラメータファイル (deploy/jcgroups/params.env.example を参照) +# 2. output_dir : 生成先ディレクトリ +# +# 例 +# ./make_jcgroups_manifests.sh ../deploy/jcgroups/params.env /tmp/generated +# +# 生成物 +# //manifests/*.yaml … kubectl apply するマニフェスト +# //conf/server.config.toml +# //conf/shibboleth2.xml +# //ssl/server.crt|key … nginx / SP のサーバ証明書 +# //sp/server.crt|key … mAP Core 接続用クライアント証明書 +# //institutions/... … 機関別クライアント証明書 +# //params.env … 使ったパラメータの控え +# +# 【注意】生成物には secret_key・DB パスワード・秘密鍵が入る。Git に入れないこと。 + +set -e + +if [ $# != 2 ]; then + echo "usage : $0 params_file output_dir" + exit 3 +fi + +if [ ! -f "$1" ]; then + echo "usage : $0 params_file output_dir" + echo " params_file が見つからない: $1" + exit 3 +fi + +PARAMS_FILE=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") +OUTPUT_DIR=$2 + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd) +TEMPLATE_DIR="$REPO_ROOT/k8s/deploy/jcgroups/manifest_template" +MIDDLEWARE_TEMPLATE_DIR="$REPO_ROOT/k8s/deploy/middleware/manifest_template" + +# ------------------------------------------------------------ +# パラメータ読み込みと既定値 +# ------------------------------------------------------------ +# shellcheck disable=SC1090 +. "$PARAMS_FILE" + +: "${NAMESPACE:=jcgroups}" +: "${IMAGE_PULL_POLICY:=Always}" +: "${REPLICAS:=2}" +: "${MEMORY_REQUEST:=512Mi}" +: "${MEMORY_LIMIT:=1Gi}" +: "${WORKER_MEMORY_LIMIT:=2Gi}" +: "${NGINX_MEMORY_LIMIT:=512Mi}" +: "${CELERY_CONCURRENCY:=4}" +: "${CONFIG_REVISION:=1}" +: "${SERVICE_TYPE:=NodePort}" +: "${INGRESS_CLASS:=nginx}" +: "${MAX_UPLOAD_SIZE:=10m}" +: "${STORAGE_TYPE:=nfs}" +: "${STORAGE_SIZE:=20Gi}" +: "${STORAGE_CLASS:=nfs}" +: "${POSTGRES_HOST:=postgres}" +: "${POSTGRES_PORT:=5432}" +: "${POSTGRES_DB:=jcgroups}" +: "${POSTGRES_USER:=jcgroups}" +: "${REDIS_CACHE_TYPE:=RedisSentinelCache}" +: "${REDIS_URL:=redis://redis:6379}" +: "${REDIS_SENTINEL_NODES:=}" +: "${REDIS_SENTINEL_MASTER:=mymaster}" +: "${REDIS_DB_APP_CACHE:=}" +: "${REDIS_DB_ACCOUNT_STORE:=}" +: "${REDIS_DB_RESULT_BACKEND:=}" +: "${REDIS_DB_GROUP_CACHE:=4}" +: "${RABBITMQ_URL:=amqp://guest:guest@rabbitmq:5672//}" +: "${MAP_CORE_BASE_URL:=https://sptest.cg.gakunin.jp}" +: "${DEPLOY_MIDDLEWARE:=false}" +: "${PGDATA_SIZE:=10Gi}" +: "${NODE_TYPE:=}" +: "${IMAGE_PULL_SECRET_NAME:=}" +: "${INSTITUTION_CERTS_DIR:=}" + +for v in VHOST APP_IMAGE NGINX_IMAGE POSTGRES_PASSWORD; do + eval "value=\$$v" + if [ -z "$value" ]; then + echo "ERROR: params.env の $v が空" >&2 + exit 4 + fi +done + +if [ "$STORAGE_TYPE" != "nfs" ] && [ "$STORAGE_TYPE" != "emptydir" ]; then + echo "ERROR: STORAGE_TYPE は nfs か emptydir" >&2 + exit 4 +fi + +if [ "$STORAGE_TYPE" = "emptydir" ] && [ "$REPLICAS" != "1" ]; then + echo "ERROR: STORAGE_TYPE=emptydir では Pod 間でファイルを共有できないため" >&2 + echo " REPLICAS=1 にすること (現在 $REPLICAS)" >&2 + exit 4 +fi + +if [ "$STORAGE_TYPE" = "nfs" ]; then + for v in NFS_SERVER NFS_PATH; do + eval "value=\$$v" + if [ -z "$value" ]; then + echo "ERROR: STORAGE_TYPE=nfs では $v が必須" >&2 + exit 4 + fi + done +fi + +if [ "$REDIS_CACHE_TYPE" != "RedisCache" ] && [ "$REDIS_CACHE_TYPE" != "RedisSentinelCache" ]; then + echo "ERROR: REDIS_CACHE_TYPE は RedisCache か RedisSentinelCache" >&2 + exit 4 +fi + +if [ "$REDIS_CACHE_TYPE" = "RedisSentinelCache" ]; then + if [ -z "$REDIS_SENTINEL_NODES" ]; then + echo "ERROR: REDIS_CACHE_TYPE=RedisSentinelCache では" >&2 + echo " REDIS_SENTINEL_NODES (host:port の空白区切り) が必須" >&2 + exit 4 + fi + # 共有 Redis では DB 番号が機関ごとに払い出し済みなので、 + # リポジトリ既定値 (0/1/2) を暗黙に使わせない。 + for v in REDIS_DB_APP_CACHE REDIS_DB_ACCOUNT_STORE REDIS_DB_RESULT_BACKEND; do + eval "value=\$$v" + if [ -z "$value" ]; then + echo "ERROR: 共有 Redis を使うので $v を明示すること" >&2 + echo " (空き番号を運用側で確保してから指定する。README 参照)" >&2 + exit 4 + fi + done +fi + +# DB 番号の妥当性 (数値・重複なし) +REDIS_DBS="" +for v in REDIS_DB_APP_CACHE REDIS_DB_ACCOUNT_STORE REDIS_DB_RESULT_BACKEND REDIS_DB_GROUP_CACHE; do + eval "value=\$$v" + [ -z "$value" ] && continue + case "$value" in + ''|*[!0-9]*) + echo "ERROR: $v は 0 以上の整数で指定すること (現在 '$value')" >&2 + exit 4 ;; + esac + for used in $REDIS_DBS; do + if [ "$used" = "$value" ]; then + echo "ERROR: Redis DB 番号 $value が重複している ($v)" >&2 + exit 4 + fi + done + REDIS_DBS="$REDIS_DBS $value" +done + +if [ -z "${SECRET_KEY:-}" ]; then + SECRET_KEY=$(openssl rand -hex 32) + echo "SECRET_KEY を生成した (params.env に控えを残すこと)" +fi + +# リソース名の接頭辞。FQDN の . と _ を - に置換する (weko-k8s と同じ流儀)。 +DOMAIN_NAME=$(echo "$VHOST" | tr '._' '-') + +OUT="$OUTPUT_DIR/$VHOST" +MANIFEST_DIR="$OUT/manifests" +CONF_DIR="$OUT/conf" +SSL_DIR="$OUT/ssl" +SP_DIR="$OUT/sp" +INST_DIR="$OUT/institutions" + +rm -rf "$OUT" +mkdir -p "$MANIFEST_DIR" "$CONF_DIR" "$SSL_DIR" "$SP_DIR" + +echo "create $VHOST (namespace=$NAMESPACE, prefix=$DOMAIN_NAME)" + +# ------------------------------------------------------------ +# 機関別証明書の items: ブロックを組み立てる +# +# Secret のキー名に "/" は使えないので、機関ごとの +# /var/mnt//server.crt|key は items[].path で作る。 +# キー名は .server.crt の形にする (deploy_jcgroups.sh も同じ規則で作る)。 +# ------------------------------------------------------------ +ITEMS_FILE=$(mktemp) +trap 'rm -f "$ITEMS_FILE"' EXIT + +if [ -n "$INSTITUTION_CERTS_DIR" ]; then + if [ ! -d "$INSTITUTION_CERTS_DIR" ]; then + echo "ERROR: INSTITUTION_CERTS_DIR が存在しない: $INSTITUTION_CERTS_DIR" >&2 + exit 4 + fi + mkdir -p "$INST_DIR" + echo " items:" >> "$ITEMS_FILE" + found=0 + for d in "$INSTITUTION_CERTS_DIR"/*; do + [ -d "$d" ] || continue + fqdn=$(basename "$d") + if [ ! -f "$d/server.crt" ] || [ ! -f "$d/server.key" ]; then + echo "WARN: $fqdn に server.crt / server.key が揃っていないので飛ばす" >&2 + continue + fi + mkdir -p "$INST_DIR/$fqdn" + cp "$d/server.crt" "$d/server.key" "$INST_DIR/$fqdn/" + printf ' - key: %s.server.crt\n path: %s/server.crt\n' \ + "$fqdn" "$fqdn" >> "$ITEMS_FILE" + printf ' - key: %s.server.key\n path: %s/server.key\n' \ + "$fqdn" "$fqdn" >> "$ITEMS_FILE" + found=$((found + 1)) + done + echo " 機関別証明書: $found 件" + if [ "$found" -eq 0 ]; then + : > "$ITEMS_FILE" + fi +fi + +# ------------------------------------------------------------ +# マニフェスト生成 +# ------------------------------------------------------------ +render() { + # $1: テンプレートファイル, $2: 出力先 + sed -e "s|__NAMESPACE__|$NAMESPACE|g" \ + -e "s|__DOMAIN_NAME__|$DOMAIN_NAME|g" \ + -e "s|__VHOST__|$VHOST|g" \ + -e "s|__APP_IMAGE__|$APP_IMAGE|g" \ + -e "s|__NGINX_IMAGE__|$NGINX_IMAGE|g" \ + -e "s|__IMAGE_PULL_POLICY__|$IMAGE_PULL_POLICY|g" \ + -e "s|__IMAGE_PULL_SECRET_NAME__|$IMAGE_PULL_SECRET_NAME|g" \ + -e "s|__NODE_TYPE__|$NODE_TYPE|g" \ + -e "s|__REPLICAS__|$REPLICAS|g" \ + -e "s|__MEMORY_REQUEST__|$MEMORY_REQUEST|g" \ + -e "s|__MEMORY_LIMIT__|$MEMORY_LIMIT|g" \ + -e "s|__WORKER_MEMORY_LIMIT__|$WORKER_MEMORY_LIMIT|g" \ + -e "s|__NGINX_MEMORY_LIMIT__|$NGINX_MEMORY_LIMIT|g" \ + -e "s|__CELERY_CONCURRENCY__|$CELERY_CONCURRENCY|g" \ + -e "s|__CONFIG_REVISION__|$CONFIG_REVISION|g" \ + -e "s|__SERVICE_TYPE__|$SERVICE_TYPE|g" \ + -e "s|__INGRESS_CLASS__|$INGRESS_CLASS|g" \ + -e "s|__MAX_UPLOAD_SIZE__|$MAX_UPLOAD_SIZE|g" \ + -e "s|__STORAGE_SIZE__|$STORAGE_SIZE|g" \ + -e "s|__STORAGE_CLASS__|$STORAGE_CLASS|g" \ + -e "s|__NFS_SERVER__|$NFS_SERVER|g" \ + -e "s|__NFS_PATH__|$NFS_PATH|g" \ + -e "s|__POSTGRES_DB__|$POSTGRES_DB|g" \ + -e "s|__POSTGRES_USER__|$POSTGRES_USER|g" \ + -e "s|__POSTGRES_PASSWORD__|$POSTGRES_PASSWORD|g" \ + -e "s|__PGDATA_SIZE__|$PGDATA_SIZE|g" \ + "$1" > "$2" + + # ---- マーカー行の処理 ---- + # `#__NAME__` 付きの行は「条件付きで出力する行」。 + # 有効化するときはマーカーだけを削り、無効化するときは行ごと消す。 + if [ -n "$IMAGE_PULL_SECRET_NAME" ]; then + sed -i -e "s|#__IMAGE_PULL_SECRET__||" "$2" + else + sed -i -e "/#__IMAGE_PULL_SECRET__/d" "$2" + fi + + if [ -n "$NODE_TYPE" ]; then + sed -i -e "s|#__NODE_SELECTOR__||" "$2" + else + sed -i -e "/#__NODE_SELECTOR__/d" "$2" + fi + + if [ "$STORAGE_TYPE" = "nfs" ]; then + sed -i -e "s|#__STORAGE_PVC__||" -e "/#__STORAGE_EMPTYDIR__/d" "$2" + else + sed -i -e "s|#__STORAGE_EMPTYDIR__||" -e "/#__STORAGE_PVC__/d" "$2" + fi + + if [ "$SERVICE_TYPE" = "ClusterIP" ]; then + sed -i -e "/#__EXTERNAL_TRAFFIC_POLICY__/d" "$2" + else + sed -i -e "s|#__EXTERNAL_TRAFFIC_POLICY__||" "$2" + fi + + # 機関別証明書の items: を差し込む (無ければマーカー行を消すだけ) + if [ -s "$ITEMS_FILE" ]; then + sed -i -e "/#__INSTITUTION_CERT_ITEMS__/r $ITEMS_FILE" \ + -e "/#__INSTITUTION_CERT_ITEMS__/d" "$2" + else + sed -i -e "/#__INSTITUTION_CERT_ITEMS__/d" "$2" + fi +} + +for file in "$TEMPLATE_DIR"/*.yaml; do + base=$(basename "$file") + + # emptydir 運用では PV / PVC を作らない + if [ "$STORAGE_TYPE" = "emptydir" ]; then + case "$base" in + volume-pv.yaml|volume-pvc.yaml) continue ;; + esac + fi + + # secret.yaml の中身は検証用ミドルウェアの PostgreSQL パスワードだけなので、 + # 共通ミドルウェアを使うときは作らない (使われない Secret にパスワードを残さない) + if [ "$DEPLOY_MIDDLEWARE" != "true" ]; then + case "$base" in + secret.yaml) continue ;; + esac + fi + + render "$file" "$MANIFEST_DIR/$base" +done + +if [ "$DEPLOY_MIDDLEWARE" = "true" ]; then + render "$MIDDLEWARE_TEMPLATE_DIR/middleware.yaml" "$MANIFEST_DIR/middleware.yaml" + echo " ミドルウェア (検証用 PostgreSQL / Redis / RabbitMQ) も生成した" +fi + +# 置換漏れの検出 +if grep -rn '__[A-Z0-9_]*__' "$MANIFEST_DIR"; then + echo "ERROR: 置換されていないプレースホルダが残っている" >&2 + exit 5 +fi + +# ------------------------------------------------------------ +# server.config.toml +# +# リポジトリの configs/server.config.toml を唯一の正とし、環境依存の値だけを +# 上書きする (テンプレートを複製すると本体の設定追加に追従できないため)。 +# [develop] セクションは本番へ持ち込まないよう丸ごと落とす。 +# ------------------------------------------------------------ +# Sentinel ノードの [[redis.sentinel.nodes]] ブロックを組み立てる。 +# リポジトリの TOML には空の2件が置いてあるが、実際の台数に合わせて差し替える。 +SENTINEL_BLOCK="" +for node in $REDIS_SENTINEL_NODES; do + case "$node" in + *:*) node_host=${node%:*}; node_port=${node##*:} ;; + *) node_host=$node; node_port=26379 ;; + esac + case "$node_port" in + ''|*[!0-9]*) + echo "ERROR: REDIS_SENTINEL_NODES のポートが不正: $node" >&2 + exit 4 ;; + esac + SENTINEL_BLOCK="$SENTINEL_BLOCK$(printf '[[redis.sentinel.nodes]]\nhost = "%s"\nport = %s\n\n' \ + "$node_host" "$node_port") +" +done + +awk -v vhost="$VHOST" \ + -v secret_key="$SECRET_KEY" \ + -v pg_user="$POSTGRES_USER" \ + -v pg_pass="$POSTGRES_PASSWORD" \ + -v pg_host="$POSTGRES_HOST" \ + -v pg_port="$POSTGRES_PORT" \ + -v pg_db="$POSTGRES_DB" \ + -v redis_cache_type="$REDIS_CACHE_TYPE" \ + -v redis_url="$REDIS_URL" \ + -v sentinel_master="$REDIS_SENTINEL_MASTER" \ + -v sentinel_block="$SENTINEL_BLOCK" \ + -v db_app_cache="$REDIS_DB_APP_CACHE" \ + -v db_account_store="$REDIS_DB_ACCOUNT_STORE" \ + -v db_result_backend="$REDIS_DB_RESULT_BACKEND" \ + -v db_group_cache="$REDIS_DB_GROUP_CACHE" \ + -v rabbitmq_url="$RABBITMQ_URL" \ + -v map_core="$MAP_CORE_BASE_URL" ' + # セクション追跡。 + # [develop] / [[develop.accounts]] … 出力しない + # [[redis.sentinel.nodes]] … 生成したブロックで置き換える + /^\[/ { + section = $0 + if (section ~ /^\[\[?develop/) { + skip = 1 + } else if (section == "[[redis.sentinel.nodes]]" && sentinel_block != "") { + skip = 1 + if (!nodes_emitted) { + printf "%s", sentinel_block + nodes_emitted = 1 + } + } else { + skip = 0 + } + } + skip { next } + + section == "" && /^server_name[[:space:]]*=/ { print "server_name = \"" vhost "\""; next } + section == "" && /^secret_key[[:space:]]*=/ { print "secret_key = \"" secret_key "\""; next } + + section == "[sp]" && /^entity_id[[:space:]]*=/ { + print "entity_id = \"https://" vhost "/shibboleth-sp\""; next + } + + section == "[map_core]" && /^base_url[[:space:]]*=/ { print "base_url = \"" map_core "\""; next } + + section == "[postgres]" && /^user[[:space:]]*=/ { print "user = \"" pg_user "\""; next } + section == "[postgres]" && /^password[[:space:]]*=/ { print "password = \"" pg_pass "\""; next } + section == "[postgres]" && /^host[[:space:]]*=/ { print "host = \"" pg_host "\""; next } + section == "[postgres]" && /^port[[:space:]]*=/ { print "port = " pg_port; next } + section == "[postgres]" && /^db[[:space:]]*=/ { print "db = \"" pg_db "\""; next } + + section == "[redis]" && /^cache_type[[:space:]]*=/ { + print "cache_type = \"" redis_cache_type "\""; next + } + + section == "[redis.database]" && db_app_cache != "" && /^app_cache[[:space:]]*=/ { + print "app_cache = " db_app_cache; next + } + section == "[redis.database]" && db_account_store != "" && /^account_store[[:space:]]*=/ { + print "account_store = " db_account_store; next + } + section == "[redis.database]" && db_result_backend != "" && /^result_backend[[:space:]]*=/ { + print "result_backend = " db_result_backend; next + } + section == "[redis.database]" && db_group_cache != "" && /^group_cache[[:space:]]*=/ { + print "group_cache = " db_group_cache; next + } + + section == "[redis.single]" && /^base_url[[:space:]]*=/ { print "base_url = \"" redis_url "\""; next } + + section == "[redis.sentinel]" && /^master_name[[:space:]]*=/ { + print "master_name = \"" sentinel_master "\""; next + } + + section == "[rabbitmq]" && /^url[[:space:]]*=/ { print "url = \"" rabbitmq_url "\""; next } + + { print } +' "$REPO_ROOT/configs/server.config.toml" > "$CONF_DIR/server.config.toml" + +# 想定通り置換されたかを確認する (awk のマッチ漏れを黙って通さない) +assert_conf() { + if ! grep -qF "$1" "$CONF_DIR/server.config.toml"; then + echo "ERROR: server.config.toml に '$1' が入っていない" >&2 + exit 6 + fi +} + +assert_conf "server_name = \"$VHOST\"" +assert_conf "entity_id = \"https://$VHOST/shibboleth-sp\"" +assert_conf "host = \"$POSTGRES_HOST\"" +assert_conf "db = \"$POSTGRES_DB\"" +assert_conf "url = \"$RABBITMQ_URL\"" +assert_conf "base_url = \"$MAP_CORE_BASE_URL\"" +assert_conf "cache_type = \"$REDIS_CACHE_TYPE\"" +assert_conf "group_cache = $REDIS_DB_GROUP_CACHE" + +if [ "$REDIS_CACHE_TYPE" = "RedisSentinelCache" ]; then + assert_conf "master_name = \"$REDIS_SENTINEL_MASTER\"" + assert_conf "app_cache = $REDIS_DB_APP_CACHE" + assert_conf "account_store = $REDIS_DB_ACCOUNT_STORE" + assert_conf "result_backend = $REDIS_DB_RESULT_BACKEND" + for node in $REDIS_SENTINEL_NODES; do + case "$node" in + *:*) node_host=${node%:*} ;; + *) node_host=$node ;; + esac + assert_conf "host = \"$node_host\"" + done + # 空の [[redis.sentinel.nodes]] が残っていないこと + if grep -q '^host = ""' "$CONF_DIR/server.config.toml"; then + echo "ERROR: 空の [[redis.sentinel.nodes]] が残っている" >&2 + exit 6 + fi +else + assert_conf "base_url = \"$REDIS_URL\"" +fi + +if grep -q '^\[\[\?develop' "$CONF_DIR/server.config.toml"; then + echo "ERROR: server.config.toml から [develop] を落とせていない" >&2 + exit 6 +fi + +# ------------------------------------------------------------ +# shibboleth2.xml +# +# nginx/shibboleth2.xml は entityID と RequestMapper の Host 名が +# localhost 固定なので、vhost に差し替えたものを ConfigMap にする。 +# ------------------------------------------------------------ +sed -e "s|entityID=\"https://localhost/shibboleth-sp\"|entityID=\"https://$VHOST/shibboleth-sp\"|" \ + -e "s| "$CONF_DIR/shibboleth2.xml" + +for expected in \ + "entityID=\"https://$VHOST/shibboleth-sp\"" \ + "&2 + exit 6 + fi +done + +# ------------------------------------------------------------ +# 証明書 +# ------------------------------------------------------------ +if [ -n "${TLS_CRT:-}" ] && [ -n "${TLS_KEY:-}" ]; then + cp "$TLS_CRT" "$SSL_DIR/server.crt" + cp "$TLS_KEY" "$SSL_DIR/server.key" +else + echo " TLS_CRT / TLS_KEY が未指定なので自己署名証明書を生成する (検証用)" + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout "$SSL_DIR/server.key" \ + -out "$SSL_DIR/server.crt" \ + -subj "/CN=$VHOST/O=$VHOST" \ + -addext "subjectAltName=DNS:$VHOST,DNS:localhost,IP:127.0.0.1" > /dev/null 2>&1 +fi + +if [ -n "${SP_CRT:-}" ] && [ -n "${SP_KEY:-}" ]; then + cp "$SP_CRT" "$SP_DIR/server.crt" + cp "$SP_KEY" "$SP_DIR/server.key" +else + echo " SP_CRT / SP_KEY が未指定なのでサーバ証明書を流用する" + cp "$SSL_DIR/server.crt" "$SP_DIR/server.crt" + cp "$SSL_DIR/server.key" "$SP_DIR/server.key" +fi + +chmod 600 "$SSL_DIR/server.key" "$SP_DIR/server.key" + +cp "$PARAMS_FILE" "$OUT/params.env" +# 生成物には秘密鍵・パスワードが入る +chmod -R go-rwx "$OUT" + +echo "" +echo "generated: $OUT" +echo " 次に: ./deploy_jcgroups.sh $OUT" diff --git a/nginx/Dockerfile b/nginx/Dockerfile index b9e523fb..cc05ffda 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -50,6 +50,15 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm i --frozen-lockfile COPY --chown=node:node . . + +# `configs/app.config.ts` is inlined into the generated SPA at build time, +# so the hostname has to be fixed here rather than at runtime. +ARG SERVER_NAME=localhost +RUN sed -i \ + "s|^const serverName = '.*' as string$|const serverName = '${SERVER_NAME}' as string|" \ + configs/app.config.ts && \ + grep -q "const serverName = '${SERVER_NAME}' as string" configs/app.config.ts + RUN NODE_ENV=production pnpm nuxt generate