diff --git a/iac_modules/pulumi/README.md b/iac_modules/pulumi/README.md new file mode 100644 index 00000000..7bc6027f --- /dev/null +++ b/iac_modules/pulumi/README.md @@ -0,0 +1,90 @@ +# Pulumi IaC 本地调试指南 + +该目录包含基于 Pulumi Python 的多云基础设施模板,支持阿里云、AWS 以及 Vultr。本文档介绍本地调试所需的前置条件、环境变量以及辅助脚本的使用方式,便于与 GitHub Actions 流水线保持一致。 + +## 1. 前置条件 + +在开始之前,请确保本地环境已经安装以下工具: + +- Python 3.9 及以上版本 +- Pulumi CLI(推荐使用最新版,可通过 `curl -fsSL https://get.pulumi.com | sh` 安装) +- AWS CLI(用于访问 S3 backend 与 AWS 资源) +- 已准备好仓库根目录的依赖: + ```bash + pip install -r requirements.txt + ``` + +> 建议使用虚拟环境(如 `python -m venv .venv`)隔离依赖,并在执行 Pulumi 命令前激活它。 + +## 2. 必需环境变量 + +GitHub Actions 与本地调试共享相同的一组环境变量。根据目标云厂商选择性地设置凭据,但 **Pulumi 状态管理必须使用 S3 backend**。 + +| 变量名 | 说明 | +| --- | --- | +| `PULUMI_ACCESS_TOKEN` | 使用 Pulumi Service 时需要的访问令牌。若仅使用 S3 backend,可留空。 | +| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | 用于访问 AWS 资源和 S3 状态桶的凭据。 | +| `ALICLOUD_ACCESS_KEY` / `ALICLOUD_SECRET_KEY` | 部署阿里云资源时所需的访问密钥。 | +| `VULTR_API_KEY` | 部署 Vultr 资源时使用的 API Key。 | +| `IAC_STATE_BACKEND` | Pulumi 后端地址,**必须**为 `s3:///` 形式,以确保状态文件全部存储在 S3。 | +| `PULUMI_STACK` | 当前操作的 Stack 名称,例如 `dev`、`prod`。 | +| `CONFIG_PATH` | (可选)指定配置目录,默认根据云厂商选择 `config/`。 | + +> 兼容变量:脚本同样支持 `IAC_State_backend` 形式的环境变量,以便与现有流水线兼容。 + +S3 backend 的 Bucket 需提前创建,并为 Pulumi 访问角色授予读写权限。例如: + +```bash +export IAC_STATE_BACKEND="s3://my-pulumi-state-bucket/modern-app" +``` + +## 3. `iac_run.sh` 辅助脚本 + +为方便本地调试,目录下新增 `iac_run.sh`,与 GitHub Actions 的命令约定保持一致。执行前请确保以上环境变量均已配置。 + +```bash +cd iac_modules/pulumi +./iac_run.sh <命令> +``` + +支持的命令如下: + +| 命令 | 对应操作 | +| --- | --- | +| `init` | 登录 S3 backend 并初始化(或选择)指定的 Stack。 | +| `create` | 使用 `pulumi up --yes --skip-preview` 创建/更新资源。 | +| `migrate` | 调用 `pulumi refresh --yes`,同步实际资源状态到后端。 | +| `upgrade` | 执行常规 `pulumi up --yes` 以滚动更新。 | +| `backup` | 通过 `pulumi stack export` 导出状态文件到 `backups/` 目录。可通过 `PULUMI_BACKUP_DIR` 覆盖输出路径。 | +| `restore <文件>` | 从指定备份文件执行 `pulumi stack import`。也可设置 `BACKUP_FILE` 环境变量。 | +| `destroy` | 运行 `pulumi destroy --yes` 清理当前 Stack 资源。 | + +查看帮助信息: + +```bash +./iac_run.sh --help +``` + +## 4. 配置目录与多云支持 + +Pulumi 入口脚本会根据配置文件中的根节点自动选择部署目标: + +- `alicloud`:执行阿里云 Landing Zone 模块。 +- `aws`:执行 AWS 基线模块。 +- `vultr`:执行 Vultr Landing Zone 模块。 + +可通过设置 `CONFIG_PATH` 指向自定义目录,例如: + +```bash +export CONFIG_PATH="config/vultr/dev" +``` + +随后运行 `./iac_run.sh init` 与 `./iac_run.sh create` 即可完成 Vultr 基线的部署与更新。 + +## 5. 常见问题 + +1. **Pulumi 提示未登录**:确认是否正确设置了 `IAC_STATE_BACKEND`(或 `IAC_State_backend`)并具备相应的 S3 访问权限。 +2. **凭据不足**:依赖的云厂商访问密钥需具备创建/查询基础资源的权限,建议在 IAM 中专门为 IaC 运行创建角色或子账号。 +3. **Python 依赖缺失**:确保已经在仓库根目录执行 `pip install -r requirements.txt`,Pulumi 才能正确加载各个云厂商的 SDK。 + +如需更多关于资源结构与模块的细节,请查阅 `modules/` 目录或仓库中的云基线设计文档。 diff --git a/iac_modules/pulumi/__main__.py b/iac_modules/pulumi/__main__.py index 0b97a26d..130c4918 100644 --- a/iac_modules/pulumi/__main__.py +++ b/iac_modules/pulumi/__main__.py @@ -6,9 +6,11 @@ from typing import Dict import pulumi import pulumi_alicloud as alicloud import pulumi_aws as aws +import ediri_vultr as vultr from modules import alicloud as alicloud_modules from modules import aws as aws_modules +from modules import vultr as vultr_modules from utils.config_loader import load_merged_config @@ -95,6 +97,39 @@ def deploy_aws(config: Dict[str, object]) -> None: pulumi.export("s3_bucket_count", len(buckets)) +def deploy_vultr(config: Dict[str, object]) -> None: + vultr_conf: Dict[str, object] = config.get("vultr", {}) # type: ignore[assignment] + region = vultr_conf.get("region") + default_tags = vultr_conf.get("default_tags", {}) + + if region: + vultr.config.region = region # type: ignore[assignment] + pulumi.export("region", region) + + pulumi.log.info("Loaded Vultr configuration") + + network_results = vultr_modules.create_vpcs( + config.get("network", {}), + region, + ) + + firewall_results = vultr_modules.create_firewall_groups( + config.get("security", {}), + ) + + instance_results = vultr_modules.create_instances( + config.get("compute", {}), + region, + default_tags, + firewall_results, + network_results, + ) + + pulumi.export("vpc_count", len(network_results)) + pulumi.export("firewall_group_count", len(firewall_results)) + pulumi.export("instance_count", len(instance_results)) + + def main() -> None: config_dir = os.environ.get("CONFIG_PATH", "config/alicloud") config = load_merged_config(config_dir) @@ -103,9 +138,11 @@ def main() -> None: deploy_alicloud(config) elif "aws" in config: deploy_aws(config) + elif "vultr" in config: + deploy_vultr(config) else: raise ValueError( - "Unsupported landing zone configuration. Expected either 'alicloud' or 'aws' section." + "Unsupported landing zone configuration. Expected a 'alicloud', 'aws', or 'vultr' section." ) diff --git a/iac_modules/pulumi/iac_run.sh b/iac_modules/pulumi/iac_run.sh new file mode 100755 index 00000000..2a6808a1 --- /dev/null +++ b/iac_modules/pulumi/iac_run.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$PROJECT_DIR" + +PULUMI_BIN=${PULUMI_BIN:-pulumi} +STACK_NAME=${PULUMI_STACK:-${STACK_NAME:-${STACK:-}}} +BACKEND_URL=${IAC_STATE_BACKEND:-${IAC_State_backend:-}} +BACKUPS_DIR=${PULUMI_BACKUP_DIR:-backups} + +usage() { + cat <<'USAGE' +用法: ./iac_run.sh <命令> [参数] + +可用命令: + init 初始化 Pulumi 后端与 Stack(需要 IAC_STATE_BACKEND 和 PULUMI_STACK) + create 创建或更新资源(pulumi up --yes --skip-preview) + migrate 同步当前资源状态到 S3 后端(pulumi refresh --yes) + upgrade 执行常规的基础设施更新(pulumi up --yes) + backup 导出 Stack 状态到本地文件(默认保存到 backups/ 目录) + restore <文件路径> 从指定备份文件恢复 Stack 状态 + destroy 销毁当前 Stack 中的所有资源 + +环境变量: + PULUMI_STACK Pulumi Stack 名称(必需) + IAC_STATE_BACKEND Pulumi backend 地址(必须为 s3:// 前缀) + CONFIG_PATH 自定义配置目录,可选 + PULUMI_BACKUP_DIR 备份输出目录,默认为 ./backups + +若使用 GitHub Actions 对齐流水线,请确保预先设置云厂商访问密钥与 Pulumi 访问令牌。 +USAGE +} + +require_backend() { + if [[ -z "${BACKEND_URL}" ]]; then + echo "[错误] 未设置 IAC_STATE_BACKEND(或 IAC_State_backend)环境变量,无法连接到 S3 backend." >&2 + exit 1 + fi + if [[ "${BACKEND_URL}" != s3://* ]]; then + echo "[错误] IAC_STATE_BACKEND 必须为 s3:// 开头的 Pulumi 后端地址." >&2 + exit 1 + fi + "${PULUMI_BIN}" login "${BACKEND_URL}" >/dev/null +} + +require_stack() { + if [[ -z "${STACK_NAME}" ]]; then + echo "[错误] 未设置 PULUMI_STACK 环境变量." >&2 + exit 1 + fi + if ! "${PULUMI_BIN}" stack select "${STACK_NAME}" >/dev/null 2>&1; then + "${PULUMI_BIN}" stack init "${STACK_NAME}" + fi +} + +command_init() { + require_backend + require_stack + echo "Pulumi backend 已配置: ${BACKEND_URL}" + echo "Pulumi stack 已就绪: ${STACK_NAME}" +} + +command_create() { + require_backend + require_stack + "${PULUMI_BIN}" up --stack "${STACK_NAME}" --yes --skip-preview +} + +command_migrate() { + require_backend + require_stack + "${PULUMI_BIN}" refresh --stack "${STACK_NAME}" --yes +} + +command_upgrade() { + require_backend + require_stack + "${PULUMI_BIN}" up --stack "${STACK_NAME}" --yes +} + +command_backup() { + require_backend + require_stack + mkdir -p "${BACKUPS_DIR}" + timestamp=$(date +"%Y%m%d%H%M%S") + backup_file="${BACKUPS_DIR}/${STACK_NAME}-${timestamp}.json" + "${PULUMI_BIN}" stack export --stack "${STACK_NAME}" >"${backup_file}" + echo "Pulumi stack 已备份到 ${backup_file}" +} + +command_restore() { + require_backend + require_stack + local backup_file=${1:-${BACKUP_FILE:-}} + if [[ -z "${backup_file}" ]]; then + echo "[错误] restore 命令需要提供备份文件路径作为参数或通过 BACKUP_FILE 环境变量传入." >&2 + exit 1 + fi + if [[ ! -f "${backup_file}" ]]; then + echo "[错误] 找不到备份文件 ${backup_file}." >&2 + exit 1 + fi + "${PULUMI_BIN}" stack import --stack "${STACK_NAME}" <"${backup_file}" +} + +command_destroy() { + require_backend + require_stack + "${PULUMI_BIN}" destroy --stack "${STACK_NAME}" --yes +} + +main() { + if [[ $# -lt 1 ]]; then + usage + exit 1 + fi + + local command=$1 + shift + + case "${command}" in + init) + command_init "$@" + ;; + create) + command_create "$@" + ;; + migrate) + command_migrate "$@" + ;; + upgrade) + command_upgrade "$@" + ;; + backup) + command_backup "$@" + ;; + restore) + command_restore "$@" + ;; + destroy) + command_destroy "$@" + ;; + -h|--help|help) + usage + ;; + *) + echo "[错误] 未知命令: ${command}" >&2 + usage + exit 1 + ;; + esac +} + +main "$@"