ci(deploy-iac): fetch secrets from Vault KV via GitHub OIDC

Replace GitHub Actions Secrets with HashiCorp Vault (https://vault.svc.plus):
- permissions: id-token: write; auth via hashicorp/vault-action@v2 (method=jwt,
  role=github-actions-xworkspace-console, audience=vault) — no static token.
- Each job loads only the keys it needs from kv/data/github-actions/xworkspace-console
  (VULTR_API_KEY, INFRA_REPO_TOKEN, ANSIBLE_SSH_KEY, CLOUDFLARE_API_TOKEN,
  DEEPSEEK/NVIDIA/OLLAMA_API_KEY, optional TF_STATE_*).
- Backend gating now keys off the Vault output (steps.vault.outputs.TF_STATE_BUCKET).
- Drop unused 'playbook' input (deploy is on-host bootstrap).

Pattern mirrors xworkmate-app/.github/workflows/build-and-release.yml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Haitao Pan 2026-06-24 15:17:46 +08:00
parent e74f2334e3
commit 75d3098d1c

View File

@ -14,14 +14,16 @@ name: Deploy AI Workspace (IaC + Ansible + Cloudflare)
#
# 数据契约 cmdb.json 由 ai-workspace-infra 的 generate.py 产出,贯穿三个 job。
#
# 需要在仓库 Settings → Secrets and variables → Actions 配置的 Secrets
# 密钥管理:不使用 GitHub Actions Secrets统一从 HashiCorp Vault
# (https://vault.svc.plus) KV 安全获取,认证走 GitHub OIDCJWT无静态 token
# - Vault 角色: github-actions-xworkspace-console (jwt auth, audience=vault)
# - KV 路径: kv/data/github-actions/xworkspace-console
# - 需在该 KV 写入的键:
# VULTR_API_KEY Vultr API Key→ TF_VAR_vultr_api_key
# INFRA_REPO_TOKEN 可读 ai-workspace-infra 的 PAT私有仓库时必需
# ANSIBLE_SSH_KEY 与 hosts.yaml 中公钥配对的 SSH 私钥(连主机用)
# ANSIBLE_SSH_KEY 与 hosts.yaml 公钥配对的 SSH 私钥(连主机用)
# CLOUDFLARE_API_TOKEN Cloudflare DNS 编辑权限 token
# DEEPSEEK_API_KEY \
# NVIDIA_API_KEY > LLM provider keys注入部署目标
# OLLAMA_API_KEY /
# DEEPSEEK_API_KEY / NVIDIA_API_KEY / OLLAMA_API_KEY LLM provider keys
# 可选(远端 TF stateS3 兼容 / Vultr 对象存储):
# TF_STATE_ENDPOINT TF_STATE_BUCKET TF_STATE_ACCESS_KEY TF_STATE_SECRET_KEY TF_STATE_REGION
# =============================================================================
@ -34,11 +36,6 @@ on:
required: false
default: "main"
type: string
playbook:
description: "部署用的 playbook相对 playbooks/"
required: false
default: "setup-ai-workspace-all-in-one.yml"
type: string
terraform_action:
description: "apply 创建/更新destroy 销毁"
required: false
@ -46,7 +43,7 @@ on:
type: choice
options: [apply, destroy]
run_deploy:
description: "provision 后是否执行 Ansible 部署"
description: "provision 后是否执行 on-host 引导部署"
required: false
default: true
type: boolean
@ -56,14 +53,19 @@ on:
default: true
type: boolean
# id-token: write 用于 Vault 的 GitHub OIDC(JWT) 认证contents: read 拉代码
permissions:
contents: read
id-token: write
concurrency:
group: deploy-ai-workspace-iac
cancel-in-progress: false
env:
VAULT_ADDR: https://vault.svc.plus
VAULT_ROLE: github-actions-xworkspace-console
VAULT_KV: kv/data/github-actions/xworkspace-console
INFRA_REPO: ${{ github.repository_owner }}/ai-workspace-infra
# vultr-vps 根(共享 scripts/ templates/ config/ENV_DIR 为 terraform 运行目录(workdir)
VPS_ROOT: infra/iac_modules/terraform-hcl-standard/vultr-vps
@ -75,18 +77,34 @@ jobs:
provision:
name: Provision (terraform + render CMDB)
runs-on: ubuntu-latest
env:
HAS_BACKEND: ${{ secrets.TF_STATE_BUCKET != '' }}
outputs:
hosts: ${{ steps.matrix.outputs.hosts }}
count: ${{ steps.matrix.outputs.count }}
steps:
- name: Load Vault secrets (OIDC)
id: vault
uses: hashicorp/vault-action@v2
with:
url: ${{ env.VAULT_ADDR }}
method: jwt
role: ${{ env.VAULT_ROLE }}
jwtGithubAudience: vault
ignoreNotFound: true
secrets: |
${{ env.VAULT_KV }} VULTR_API_KEY | VULTR_API_KEY ;
${{ env.VAULT_KV }} INFRA_REPO_TOKEN | INFRA_REPO_TOKEN ;
${{ env.VAULT_KV }} TF_STATE_ENDPOINT | TF_STATE_ENDPOINT ;
${{ env.VAULT_KV }} TF_STATE_BUCKET | TF_STATE_BUCKET ;
${{ env.VAULT_KV }} TF_STATE_ACCESS_KEY | TF_STATE_ACCESS_KEY ;
${{ env.VAULT_KV }} TF_STATE_SECRET_KEY | TF_STATE_SECRET_KEY ;
${{ env.VAULT_KV }} TF_STATE_REGION | TF_STATE_REGION
- name: Checkout infra (iac_modules + playbooks)
uses: actions/checkout@v4
with:
repository: ${{ env.INFRA_REPO }}
ref: ${{ github.event.inputs.infra_ref || 'main' }}
token: ${{ secrets.INFRA_REPO_TOKEN || github.token }}
token: ${{ steps.vault.outputs.INFRA_REPO_TOKEN || github.token }}
path: infra
- uses: hashicorp/setup-terraform@v3
@ -101,7 +119,7 @@ jobs:
run: pip install --quiet pyyaml jinja2
- name: Configure remote backend (optional)
if: ${{ env.HAS_BACKEND == 'true' }}
if: ${{ steps.vault.outputs.TF_STATE_BUCKET != '' }}
working-directory: ${{ env.ENV_DIR }}
run: |
set -euo pipefail
@ -124,25 +142,28 @@ jobs:
- name: Terraform init
working-directory: ${{ env.ENV_DIR }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }}
AWS_ACCESS_KEY_ID: ${{ steps.vault.outputs.TF_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ steps.vault.outputs.TF_STATE_SECRET_KEY }}
TF_STATE_ENDPOINT: ${{ steps.vault.outputs.TF_STATE_ENDPOINT }}
TF_STATE_BUCKET: ${{ steps.vault.outputs.TF_STATE_BUCKET }}
TF_STATE_REGION: ${{ steps.vault.outputs.TF_STATE_REGION }}
run: |
set -euo pipefail
if [ -n "${{ secrets.TF_STATE_BUCKET }}" ]; then
if [ -n "${TF_STATE_BUCKET}" ]; then
terraform init -input=false \
-backend-config="endpoint=${{ secrets.TF_STATE_ENDPOINT }}" \
-backend-config="bucket=${{ secrets.TF_STATE_BUCKET }}" \
-backend-config="endpoint=${TF_STATE_ENDPOINT}" \
-backend-config="bucket=${TF_STATE_BUCKET}" \
-backend-config="key=ai-workspace/terraform.tfstate" \
-backend-config="region=${{ secrets.TF_STATE_REGION || 'us-east-1' }}"
-backend-config="region=${TF_STATE_REGION:-us-east-1}"
else
echo "::warning::未配置远端 state,使用本地 state仅适合一次性演示destroy 需同一次运行)"
echo "::warning::未配置远端 state(Vault 无 TF_STATE_BUCKET),使用本地 state仅适合一次性演示destroy 需同一次运行)"
terraform init -input=false
fi
- name: Terraform ${{ github.event.inputs.terraform_action || 'apply' }}
working-directory: ${{ env.ENV_DIR }}
env:
TF_VAR_vultr_api_key: ${{ secrets.VULTR_API_KEY }}
TF_VAR_vultr_api_key: ${{ steps.vault.outputs.VULTR_API_KEY }}
run: |
set -euo pipefail
terraform ${{ github.event.inputs.terraform_action || 'apply' }} -auto-approve -input=false
@ -188,6 +209,20 @@ jobs:
# 自动走离线包加速)。从 runner 远程跑 all-in-one 会撞 roles/agent_skills 的
# delegate_to: localhost写 runner 本地 /root故 deploy 改为 ssh 到主机本地
# 跑官方引导脚本——与用户 self-host 的 curl|bash 完全同一路径。
- name: Load Vault secrets (OIDC)
id: vault
uses: hashicorp/vault-action@v2
with:
url: ${{ env.VAULT_ADDR }}
method: jwt
role: ${{ env.VAULT_ROLE }}
jwtGithubAudience: vault
secrets: |
${{ env.VAULT_KV }} ANSIBLE_SSH_KEY | ANSIBLE_SSH_KEY ;
${{ env.VAULT_KV }} DEEPSEEK_API_KEY | DEEPSEEK_API_KEY ;
${{ env.VAULT_KV }} NVIDIA_API_KEY | NVIDIA_API_KEY ;
${{ env.VAULT_KV }} OLLAMA_API_KEY | OLLAMA_API_KEY
- name: Download CMDB (host IP source)
uses: actions/download-artifact@v4
with:
@ -198,7 +233,7 @@ jobs:
run: |
set -euo pipefail
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.ANSIBLE_SSH_KEY }}" > ~/.ssh/id_ed25519
printf '%s\n' "${{ steps.vault.outputs.ANSIBLE_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Wait for host SSH
@ -214,9 +249,9 @@ jobs:
- name: Run on-host bootstrap (curl | bash, local-mode install)
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }}
DEEPSEEK_API_KEY: ${{ steps.vault.outputs.DEEPSEEK_API_KEY }}
NVIDIA_API_KEY: ${{ steps.vault.outputs.NVIDIA_API_KEY }}
OLLAMA_API_KEY: ${{ steps.vault.outputs.OLLAMA_API_KEY }}
run: |
set -euo pipefail
ip="$(jq -r '.["${{ matrix.host }}"].ip' cmdb/cmdb.json)"
@ -239,12 +274,25 @@ jobs:
if: ${{ needs.provision.outputs.count != '0' && (github.event.inputs.run_dns == 'true' || github.event.inputs.run_dns == null) }}
runs-on: ubuntu-latest
steps:
- name: Load Vault secrets (OIDC)
id: vault
uses: hashicorp/vault-action@v2
with:
url: ${{ env.VAULT_ADDR }}
method: jwt
role: ${{ env.VAULT_ROLE }}
jwtGithubAudience: vault
ignoreNotFound: true
secrets: |
${{ env.VAULT_KV }} INFRA_REPO_TOKEN | INFRA_REPO_TOKEN ;
${{ env.VAULT_KV }} CLOUDFLARE_API_TOKEN | CLOUDFLARE_API_TOKEN
- name: Checkout infra (playbooks)
uses: actions/checkout@v4
with:
repository: ${{ env.INFRA_REPO }}
ref: ${{ github.event.inputs.infra_ref || 'main' }}
token: ${{ secrets.INFRA_REPO_TOKEN || github.token }}
token: ${{ steps.vault.outputs.INFRA_REPO_TOKEN || github.token }}
path: infra
- name: Download CMDB + inventory
@ -263,7 +311,7 @@ jobs:
- name: Reconcile Cloudflare DNS from inventory
working-directory: ${{ env.PLAYBOOKS_DIR }}
env:
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_DNS_API_TOKEN: ${{ steps.vault.outputs.CLOUDFLARE_API_TOKEN }}
run: |
set -euo pipefail
# 只为本次新建的 ai_workspace 组主机同步 A 记录(域名取各主机