第 13b 章 クライアント、グループウェアと CLI 管理画面: #pConfigFile

構成のコード化: エクスポート、プラン、適用、Git パイプライン

サーバーの状態全体を Git の YAML ファイルとして管理: テナント、ドメイン (DKIM セレクター、MTA-STS、ルーティング)、エイリアス・転送・自動応答を含むメールボックス、配布リスト、共有メールボックス、会議室、GAL とリソース ルール、共有と委任、SMTP、スマート ホスト、ルート、トランスポート ルール、スパム・ウイルス対策、ファイアウォールと自動禁止、TLS、ID ソース、API キー (メタデータのみ)、Webhook、通知、運用アラート、POP3、ファイル、予約、アーカイブ、DNS フェイルオーバー。nextxch-cli config plan は terraform plan のように、何が作成、変更、削除されるかを表示します。apply はまずすべてを検証し、決まった順序で適用します。管理画面: システム > ファイルとしての構成。

運用のヒントとアーキテクチャに関する注記

シークレットはファイルに含めず、エクスポートもされません。パスワード、トークン、キーは secretRef: {env: NAME} または secretRef: {file: path} として記述し、適用時に解決されます (CI ランナー上では nextxch-cli が解決し、サーバー上では NEXTXCH_SECRET_* 変数と <data>/secrets/ 配下のファイルのみ)。DKIM キー、証明書、パスワード ハッシュはサーバーに残ります。API v1 と同様に Business の機能です。JSON Schema は無料です。

1. ファイル形式: apiVersion、kind、metadata.name、spec

複数のドキュメント (--- で区切り) を含む 1 つのファイル、または領域ごとに 1 ファイルを置くフォルダー (config export --split)。ファイルにないフィールドはサーバー上の値のまま維持されます。リスト (エイリアス、メンバー) は全体として扱われます。

リファレンス / 構成
# yaml-language-server: $schema=https://nextxch.com/schemas/nextxch-config-v1.schema.json
apiVersion: nextxch/v1
kind: Manifest            # optional: areas in which --prune may delete
metadata: {name: nextxch}
spec: {areas: [domains, accounts, sharing, transport]}
---
apiVersion: nextxch/v1
kind: Domain
metadata: {name: company.com}
spec:
  tenant: default
  catchAll: info@company.com
  dkim: {mode: dual, rsaSelector: s2026, ed25519Selector: ed2026}   # keys stay on the server
  mtaSts: {mode: enforce, maxAgeSeconds: 604800, mxHosts: [mail.company.com]}
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: anna.smith@company.com}
spec:
  displayName: Anna Smith
  role: User                      # User, TenantAdmin, ServerAdmin, ComplianceAuditor
  quotaMb: 10240
  aliases: [anna@company.com]
  forwarding: {address: anna@private.example, keepCopy: true}
  outOfOffice: {enabled: false}
  password: {secretRef: {env: ANNA_PASSWORD}}   # never the password itself
---
apiVersion: nextxch/v1
kind: Account
metadata: {name: info@company.com}
spec: {type: shared, displayName: Info mailbox}   # user, shared, room, equipment
---
apiVersion: nextxch/v1
kind: Share
metadata: {name: "info@company.com/mailbox -> anna.smith@company.com"}
spec: {permissions: [read, write, sendAs]}
---
apiVersion: nextxch/v1
kind: SmtpSettings
metadata: {name: server}             # server-wide settings are always named "server"
spec:
  outboundSmartHost: smtp.provider.example
  outboundSmartPort: 587
  smartHostPassword: {secretRef: {file: secrets/smarthost.txt}}

2. nextxch-cli によるワークフロー

まずエクスポートしてコミットし、その後はプル リクエストで変更します。plan の出力例:

リファレンス / 構成
export NEXTXCH_URL=https://mail.company.com
export NEXTXCH_API_KEY=smk_...            # key with config.read or config.write
nextxch-cli config export --split nextxch/
nextxch-cli config validate -f nextxch/    # offline: kinds and fields
nextxch-cli config plan -f nextxch/

  ~ Domain company.com
      mtaSts.mode: "testing" -> "enforce"
  ~ Account anna.smith@company.com
      quotaMb: 10240 -> 20480
      aliases: ["anna@company.com"] -> ["a.smith@company.com","anna@company.com"]
      ~ password: (secret changed)
  + Account info@company.com
      type: "shared"
      displayName: "Info mailbox"

Not in the file (1), deleted only with --prune:
    Account old@company.com

Plan: 1 to create, 2 to update, 0 to delete, 41 unchanged.
# exit code 2 = changes

nextxch-cli config apply -f nextxch/ --prune --yes
nextxch-cli config history

3. GitHub Actions: プル リクエストで plan、マージで apply

コメント ステップを含む完全なファイル: リポジトリ内の deploy/gitops/github-actions.yml。API キーはファイルからではなく、必ずリポジトリ シークレットから取得します。

リファレンス / 構成
name: nextxch config
on:
  pull_request: {paths: ["nextxch/**"]}
  push: {branches: [main], paths: ["nextxch/**"]}
permissions: {contents: read, pull-requests: write}
concurrency: {group: nextxch-config, cancel-in-progress: false}
env: {NEXTXCH_IMAGE: "nextxch/server:latest"}
jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Plan
        env:
          NEXTXCH_URL: ${{ vars.NEXTXCH_URL }}
          NEXTXCH_API_KEY: ${{ secrets.NEXTXCH_CONFIG_READ_KEY }}
        run: |
          set +e
          docker run --rm -e NEXTXCH_URL -e NEXTXCH_API_KEY -e NO_COLOR=1 -v "$PWD:/work" -w /work \
            --entrypoint nextxch-cli "$NEXTXCH_IMAGE" config plan -f nextxch/ --prune > plan.txt 2>&1
          code=$?; cat plan.txt; [ "$code" -ne 1 ]
      # danach: actions/github-script postet plan.txt als Kommentar (siehe deploy/gitops)
  apply:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - env:
          NEXTXCH_URL: ${{ vars.NEXTXCH_URL }}
          NEXTXCH_API_KEY: ${{ secrets.NEXTXCH_CONFIG_WRITE_KEY }}
          SMARTHOST_PASSWORD: ${{ secrets.SMARTHOST_PASSWORD }}
        run: |
          docker run --rm -e NEXTXCH_URL -e NEXTXCH_API_KEY -e SMARTHOST_PASSWORD -v "$PWD:/work" -w /work \
            --entrypoint nextxch-cli "$NEXTXCH_IMAGE" config apply -f nextxch/ --prune --yes

4. GitLab CI: プランをマージ リクエストのノートとして投稿し、既定のブランチで apply

完全なファイル: deploy/gitops/gitlab-ci.yml。キーはマスクされ保護された CI/CD 変数です。resource_group により 2 つの apply が同時に実行されることを防ぎます。

リファレンス / 構成
stages: [validate, plan, apply]
variables: {CONFIG_DIR: nextxch, NO_COLOR: "1"}
.nextxch-cli:
  image: {name: "nextxch/server:latest", entrypoint: [""]}
validate:
  extends: .nextxch-cli
  stage: validate
  script: [nextxch-cli config validate -f "$CONFIG_DIR"]
plan:
  extends: .nextxch-cli
  stage: plan
  rules: [{if: '$CI_PIPELINE_SOURCE == "merge_request_event"'}]
  variables: {NEXTXCH_API_KEY: $NEXTXCH_CONFIG_READ_KEY}
  script:
    - set +e
    - nextxch-cli config plan -f "$CONFIG_DIR" --prune > plan.txt 2>&1; code=$?
    - cat plan.txt; test "$code" -ne 1
  artifacts: {when: always, paths: [plan.txt]}
# plan-comment: curl auf /merge_requests/:iid/notes mit plan.txt (siehe deploy/gitops)
apply:
  extends: .nextxch-cli
  stage: apply
  resource_group: nextxch-config
  environment: production
  rules: [{if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'}]
  variables: {NEXTXCH_API_KEY: $NEXTXCH_CONFIG_WRITE_KEY}
  script: [nextxch-cli config apply -f "$CONFIG_DIR" --prune --yes]

5. エディターでの JSON Schema と REST 呼び出し

VS Code (Red Hat の "YAML" 拡張機能) と JetBrains の IDE は、入力中にファイルをチェックします。各サーバーも /api/v1/config/schema でスキーマを提供します。

リファレンス / 構成
# .vscode/settings.json
{
  "yaml.schemas": {
    "https://nextxch.com/schemas/nextxch-config-v1.schema.json": ["nextxch/*.yaml"]
  }
}

# Plan ohne CLI, direkt per REST (YAML als Body):
curl -s -X POST "$NEXTXCH_URL/api/v1/config/plan?prune=true" \
  -H "Authorization: Bearer $NEXTXCH_API_KEY" \
  -H "Content-Type: application/yaml" --data-binary @nextxch.yaml

6. リスト内のシークレットと再起動不要の ACME

認証ヘッダーとパイプラインからの HMAC シークレットを持つ MTA フック、Webhook シークレット付きのアラート送信先、DNS-01 による Let's Encrypt への切り替え:1 回の適用で、再起動は不要です。

リファレンス / 構成
apiVersion: nextxch/v1
kind: MtaFilters
metadata: {name: server}
spec:
  hooks:
    - name: spamcheck
      url: https://hooks.company.com/check
      stages: [data]
      authHeaderName: Authorization
      authHeaderValue: {secretRef: {env: HOOK_TOKEN}}   # in the plan: hooks[spamcheck].authHeaderValue
      hmacSecret: {secretRef: {file: secrets/hook-hmac.txt}}
---
apiVersion: nextxch/v1
kind: OpsAlerts
metadata: {name: server}
spec:
  targets:
    - name: pager
      type: webhook
      address: https://pager.company.com/hook
      secret: {secretRef: {env: PAGER_SECRET}}
---
apiVersion: nextxch/v1
kind: TlsSettings
metadata: {name: server}
spec:
  acme:
    enabled: true
    directoryUrl: https://acme-v02.api.letsencrypt.org/directory
    contactEmail: postmaster@company.com
    challenge: Dns01
    renewBeforeExpiryDays: 30

設定、既定値 & ベスト プラクティス

設定 / 場所 説明 既定値 本番環境のベスト プラクティス
config export [--area a,b] [--split ordner/] [-o datei] サーバーの状態を決定論的にエクスポートします: ドキュメント順序は固定、リストはソート済み、タイムスタンプなし。2 回エクスポートしても同じバイト列になり、Git の差分には実際の変更だけが表示されます。 alle Bereiche リポジトリでは --split を使用してください (領域ごとに 1 ファイル: domains.yaml、accounts.yaml ...)。小さなプル リクエストは読みやすいままに保てます。
config plan -f datei|ordner [--prune] オブジェクトごとに + 作成、~ 更新 (フィールド: 旧 -> 新)、- 削除を表示します。終了コード 0 = 変更なし、2 = 変更あり、1 = エラー。シークレットは "(secret changed)" としてのみ表示されます。 ohne Löschen プル リクエストで実行し、結果をコメントとして投稿します (例は下記)。
config apply -f ... [--prune] [--yes] [--force] [--dry-run] 最初にすべてをチェックし (フィールド、参照、シークレット、安全ルール)、その後決まった順序で作成と更新を行い (テナント、ドメイン、アカウント、リスト、ルール、共有、トランスポート ...)、削除は逆の順序で行います。いずれかのステップが失敗すると、完了済みのステップは元に戻されます。すべての変更は監査ログに記録されます。 fragt nach パイプラインでは常に --yes を指定してください。指定しないと、端末がない環境では apply が拒否されます。--dry-run は plan と同じです。
--prune und Manifest 削除は --prune を指定した場合にのみ、かつ Manifest ドキュメントで指定された領域 (Manifest がない場合はファイル内に存在する種類のみ) でのみ行われます。サーバー設定が削除されることはありません。 aus エクスポートでは Manifest が書き出されます。一部のみを管理する場合は、対象領域を絞り込んでください。
Schutz: letzter Admin, Domain mit Postfächern 有効なサーバー管理者が残らなくなるプランや、メールボックスが残っているドメインの削除は拒否されます。--force で強制できます (その場合、ドメインは削除されずに無効化されます)。自分自身の API キーで自身を失効させることはできません。 aktiv パイプラインに --force をハードコードしないでください。
secretRef: {env: NAME} | {file: pfad} 値そのものではなくシークレットへの参照。nextxch-cli がランナー上で解決します (任意の変数、構成ファイルからの相対パスのファイル)。サーバー自体が読み取るのは NEXTXCH_SECRET_* と <data>/secrets/ 配下のファイルのみです。同じ値を再適用しても変更にはなりません。 - シークレットは GitHub/GitLab のシークレット ストアから環境変数としてジョブに渡してください。
Scopes config.read / config.write REST:GET /api/v1/config/export、POST /api/v1/config/plan、POST /api/v1/config/apply、GET /api/v1/config/history、GET /api/v1/config/schema(無料)。キー:テナント統合キー。作成者が管理者である間のみ有効です。サーバー管理者のキーはサーバー全体を、テナント管理者のキーはそのテナントのみを参照できます。エクスポートと計画には config.read で十分ですが、適用には config.write が必要です。 - キーは 2 つ: プル リクエストでのプラン用に config.read、main 上の apply ジョブ専用に config.write を使い、可能であればランナーの IP に制限します。
Mandanten-Admins テナント管理者(セッションまたはその管理者のキー)は、自分のテナントのオブジェクトのみをエクスポート、計画、適用できます:Domain(変更のみ、作成と削除は不可)、Account、DistributionList、Share、GalVisibilityRule、ResourceAccessRule、Webhook。サーバー設定、テナント、ID ソース、API キーはサーバー管理者の管轄のままです。ロールが ServerAdmin または ComplianceAuditor のアカウントは対象外です。--prune は自分のテナント内でのみ削除し、history には自分の適用のみが表示されます。 - リセラーや部門向け:各テナントは独自のキーで独自のリポジトリを保持します。"tenant" フィールドは省略可能で、自分のテナントが設定されます。
secretRef in Listen リストの項目も secretRef でシークレットを受け取ります:MtaFilters hooks[].authHeaderValue と hooks[].hmacSecret、OpsAlerts targets[].secret(Webhook 送信先の HMAC)。計画では項目を id または名前で示します(例:hooks[spamcheck].authHeaderValue)。リスト内の平文の値は拒否されます。 - id のない項目には最初の適用時に id が付与されます。エクスポート後はファイル内の id を保持してください。そうすれば、リストの順序を変えてもシークレットは正しい項目に残ります。
ACME zur Laufzeit TlsSettings.acme(enabled、directoryUrl、contactEmail、チャレンジ Http01/TlsAlpn01/Dns01、renewBeforeExpiryDays、reusePrivateKeyForDane)は再起動なしで適用されます。appsettings.json は基本設定のままで、差分は <data>/acme-settings.json に保存され、クラスター全体に適用されます。ディレクトリ URL を変更すると、次回の発行時に新しい ACME アカウントが作成されます。 appsettings.json まず Let's Encrypt のステージングディレクトリでテストし、その後本番に切り替えてください。
Arten (kinds) Tenant、Domain、Account、DistributionList、GalVisibilityRule、ResourceAccessRule、Share、SmtpSettings、Route、TransportRules、SpamFilter、MtaFilters、FirewallRule、TlsSettings、IdentitySource、ApiKey、Webhook、Notifications、OpsAlerts、DnsFailover、Pop3、FileStorage、Booking、ArchivePolicy。全フィールド: JSON Schema。 - API キーはファイルから作成されません(そのシークレットは一度しか存在しないため)。ファイルではラベルとスコープを変更でき、--prune で失効させます。会議室と予約の設定はすべてのクラスターノードで同一です。