feat(iac): Refactor structure and support multi-environment config loading

- Add config/sit and other environment-specific config directories
- Refactor deploy.py to support CONFIG_PATH environment variable
- Enable automatic merging of config/*/*.yaml files
- Enhance run.sh with Pulumi/Ansible/Terraform initialization checks
- Add inventory.py to dynamically generate Ansible hosts
- Improve ec2_instance.py with modular instance creation
- Organize base.yaml, vpc.yaml and related config files"
This commit is contained in:
Haitao Pan 2025-03-29 11:08:07 +08:00
parent cb57cb6782
commit c2020da184
12 changed files with 587 additions and 1 deletions

52
.gitmessage.txt Normal file
View File

@ -0,0 +1,52 @@
# 💡 提交说明模板 (Git Commit Message Template)
#
# 📌 标准格式:
# <type>(<scope>): <简要描述>
#
# 📖 示例:
# feat(iac): 新增支持多环境配置加载
# feat(deploy): Add support for multi-environment config loading
#
# 可选结构:
# - 中文描述(团队成员理解方便)
# - 英文描述CI/CD / PR 审阅更规范)
#
# 🧱 支持类型:
# feat 💡 新功能 / Feature
# fix 🐛 修复 bug / Bug fix
# docs 📚 文档变更 / Documentation
# style 🎨 代码格式 / Style only
# refactor 🔨 重构 / Refactor (无功能变更)
# perf 🚀 性能优化 / Performance
# test 🧪 测试相关 / Add or update tests
# chore 🔧 构建、工具、依赖更新 / Chores
#
# ⏱️ 每次提交只关注一类改动
# ---------------------- COMMIT MESSAGE START ----------------------
feat(iac): 重构目录结构并支持多环境配置加载
feat(iac): Refactor structure and support multi-environment config loading
- 新增 config/sit 等多环境配置目录结构
- Add config/sit and other environment-specific config directories
- 重构 deploy.py 适配 CONFIG_PATH 环境变量
- Refactor deploy.py to support CONFIG_PATH environment variable
- 支持自动合并 config/*/*.yaml 配置
- Enable automatic merging of config/*/*.yaml files
- 增强 run.sh 脚本,集成 Pulumi/Ansible/Terraform 初始化检查
- Enhance run.sh with Pulumi/Ansible/Terraform initialization checks
- 新增 inventory.py 动态生成 Ansible 主机列表
- Add inventory.py to dynamically generate Ansible hosts
- 整理 base.yaml、vpc.yaml 等配置文件
- Organize base.yaml, vpc.yaml and related config files
# ----------------------- COMMIT MESSAGE END -----------------------
# 📝 注意:提交时只保留实际变更部分,其余注释会被 Git 忽略

View File

@ -1,11 +1,41 @@
# Modern Container Application Reference Architecture # Modern Container Application Reference Architecture
Welcome to the repository for the Modern Container Application Reference Architecture. This repository contains a comprehensive guide and reference architecture for building scalable, portable, resilient, and agile containerized applications. Welcome to the repository for the Modern Container Application Reference Architecture. This repository contains a comprehensive guide and reference architecture for building scalable, portable, resilient, and agile containerized applications. 一个基于 Pulumi + Ansible 的基础设施自动化项目模板支持多环境部署dev / staging / prod实现从基础设施创建到主机配置的全流程自动化管理。
---
## Overview ## Overview
The project aims to create a multi-cloud environment that leverages containers for deploying modern applications. The key objective is to set up a unified authentication system using **OIDC** via **Keycloak** for **AWS**, **GCP**, **Azure**, **GitHub**, **Harbor ** and **Grafana **. The project aims to create a multi-cloud environment that leverages containers for deploying modern applications. The key objective is to set up a unified authentication system using **OIDC** via **Keycloak** for **AWS**, **GCP**, **Azure**, **GitHub**, **Harbor ** and **Grafana **.
## 🚀 项目功能
- 使用 PulumiPython创建 AWS 基础设施VPC、子网、安全组、EC2
- 配置结构模块化:`base.yaml`, `vpc.yaml`, `firewall.yaml`, `instances.yaml`
- 支持 Spot / On-Demand 实例,支持 TTL 标签
- 自动输出 EC2 IP动态生成 Ansible Inventory
- 使用 Ansible Playbook 远程安装软件或部署服务
- 支持多环境 stackdev/staging/prod
## 项目结构
├── config/ # 多环境配置
│ ├── base.yaml
│ ├── vpc.yaml
│ ├── firewall.yaml
│ └── instances.yaml
├── iac_modules/
│ └── pulumi/
│ ├── deploy.py # Pulumi 主入口
│ ├── modules/ # VPC/SG/EC2 模块
│ ├── utils/config_loader.py
│ └── requirements.txt
├── scripts/
│ ├── infra.sh # 一键部署脚本
│ └── inventory.py # 动态 Ansible inventory
├── ansible/
│ └── playbooks/
│ └── setup.yml # 应用部署 playbook
## Phase 1: Implementing OIDC Login ## Phase 1: Implementing OIDC Login
In this first phase, we focus on implementing OpenID Connect (OIDC) login functionality for the following platforms: In this first phase, we focus on implementing OpenID Connect (OIDC) login functionality for the following platforms:

7
config/sit/base.yaml Normal file
View File

@ -0,0 +1,7 @@
aws:
access_key: YOUR_ACCESS_KEY
secret_key: YOUR_SECRET_KEY
region: us-east-1
key_pairs:
- name: dev_key
key_file: keys/dev_ssh.pub

6
config/sit/firewall.yaml Normal file
View File

@ -0,0 +1,6 @@
firewall_rules:
- name: allow-ssh-web
allow:
- protocol: tcp
ports: ["22", "80", "443"]
source_ranges: ["0.0.0.0/0"]

32
config/sit/instances.yaml Normal file
View File

@ -0,0 +1,32 @@
instances:
- name: master-1
ami: ami-0c2b8ca1dad447f8a
type: t3.micro
disk_size_gb: 20
subnet: public-subnet-1
lifecycle: spot # 可选: ondemand默认或 spot
ttl: 1h # 可选: 自动标记 TTL仅作为标识不自动销毁
- name: slave-1
ami: ami-0c2b8ca1dad447f8a
type: t3.micro
disk_size_gb: 20
subnet: private-subnet-1
lifecycle: spot
ttl: 1h
- name: agent-1
ami: ami-0c2b8ca1dad447f8a
type: t3.micro
disk_size_gb: 20
subnet: private-subnet-1
lifecycle: spot
ttl: 1h
- name: agent-2
ami: ami-0c2b8ca1dad447f8a
type: t3.micro
disk_size_gb: 20
subnet: private-subnet-1
lifecycle: spot
ttl: 1h

24
config/sit/vpc.yaml Normal file
View File

@ -0,0 +1,24 @@
vpc:
name: dev-vpc
cidr_block: 10.0.0.0/16
subnets:
- name: public-subnet-1
cidr_block: 10.0.1.0/24
availability_zone: us-east-1a
type: public
- name: private-subnet-1
cidr_block: 10.0.101.0/24
availability_zone: us-east-1a
type: private
routes:
- name: public-route
destination_cidr_block: 0.0.0.0/0
subnet_type: public
gateway: internet_gateway
peering:
enabled: false
peer_vpc_id: null
peer_region: null
auto_accept: false

View File

@ -0,0 +1,63 @@
import os
import pulumi
import pulumi_aws as aws
from utils.config_loader import load_merged_config
from modules.vpc.vpc import create_vpc
from modules.security_group.sg import create_security_group
from modules.ec2.ec2_instance import create_instances
# ✅ 自动从环境变量获取配置路径,默认为 "config/"
config_dir = os.environ.get("CONFIG_PATH", "config")
config = load_merged_config(config_dir)
# ✅ 提取配置项(如为空跳过)
aws_conf = config.get("aws")
vpc_conf = config.get("vpc")
instances_conf = config.get("instances", [])
firewall_rules = config.get("firewall_rules", [])
if not aws_conf or not vpc_conf:
pulumi.log.warn(f"❌ 配置不完整,缺少 aws 或 vpc 段终止部署。CONFIG_PATH={config_dir}")
exit(0)
# ✅ 配置 AWS 凭据
aws.config.region = aws_conf["region"]
aws.config.access_key = aws_conf["access_key"]
aws.config.secret_key = aws_conf["secret_key"]
# ✅ 创建 VPC 与子网
vpc_result = create_vpc(vpc_conf, aws_conf["region"])
vpc = vpc_result["vpc"]
subnets = vpc_result["subnets"]
# ✅ 创建安全组(取第一组规则)
if not firewall_rules:
pulumi.log.warn("⚠️ 未定义 firewall_rules默认跳过安全组配置")
sg_id = None
else:
sg = create_security_group(vpc.id, firewall_rules[0])
sg_id = sg.id
# ✅ SSH 密钥对
key_cfg = aws_conf["key_pairs"][0]
public_key_path = key_cfg["key_file"]
if not os.path.exists(public_key_path):
raise FileNotFoundError(f"❌ SSH 公钥文件不存在: {public_key_path}")
with open(public_key_path, "r") as f:
public_key = f.read().strip()
key_pair = aws.ec2.KeyPair("main-key",
key_name=key_cfg["name"],
public_key=public_key
)
# ✅ 创建实例(自动匹配子网)
if not instances_conf:
pulumi.log.warn("⚠️ 未配置任何 EC2 实例,跳过实例部署")
outputs = {}
else:
outputs = create_instances(instances_conf, subnets, sg_id, key_pair.key_name)
# ✅ 导出所有实例的公网 IP
for name, ip in outputs.items():
pulumi.export(f"{name}_ip", ip)

View File

@ -0,0 +1,54 @@
import pulumi_aws as aws
def create_instances(instances_config, subnets_dict, sg_id, key_name):
outputs = {}
for instance_cfg in instances_config:
name = instance_cfg["name"]
subnet_name = instance_cfg["subnet"]
subnet_id = subnets_dict[subnet_name].id
ami = instance_cfg["ami"]
instance_type = instance_cfg["type"]
disk_size = instance_cfg["disk_size_gb"]
# 读取可选字段
lifecycle = instance_cfg.get("lifecycle", "ondemand") # 默认按需
ttl = instance_cfg.get("ttl", "none") # 默认无 TTL
# 设置 EC2 标签
tags = {
"Name": name,
"Lifecycle": lifecycle,
"TTL": ttl,
}
# 如果是 Spot 实例,设置市场选项(不设 max_price → 自动出价)
instance_market_options = None
if lifecycle == "spot":
instance_market_options = aws.ec2.InstanceInstanceMarketOptionsArgs(
market_type="spot",
spot_options=aws.ec2.InstanceInstanceMarketOptionsSpotOptionsArgs(
instance_interruption_behavior="terminate",
spot_instance_type="one-time"
)
)
# 创建 EC2 实例
ec2 = aws.ec2.Instance(name,
ami=ami,
instance_type=instance_type,
key_name=key_name,
subnet_id=subnet_id,
vpc_security_group_ids=[sg_id],
associate_public_ip_address=True,
root_block_device={
"volume_size": disk_size,
"volume_type": "gp2"
},
instance_market_options=instance_market_options,
tags=tags
)
outputs[name] = ec2.public_ip
return outputs

View File

@ -0,0 +1,3 @@
pulumi
pulumi-aws
PyYAML

View File

@ -0,0 +1,35 @@
import os
import glob
import yaml
from collections.abc import Mapping
def deep_merge(dict1, dict2):
result = dict1.copy()
for k, v in dict2.items():
if k in result and isinstance(result[k], dict) and isinstance(v, Mapping):
result[k] = deep_merge(result[k], v)
elif k in result and isinstance(result[k], list) and isinstance(v, list):
result[k] += v
else:
result[k] = v
return result
def load_merged_config(config_dir=None):
config_dir = config_dir or os.environ.get("CONFIG_PATH", "config")
if not os.path.isdir(config_dir):
raise FileNotFoundError(f"❌ 配置目录不存在: {config_dir}")
merged = {}
files = sorted(glob.glob(os.path.join(config_dir, "*.yaml")) + glob.glob(os.path.join(config_dir, "*.yml")))
if not files:
raise FileNotFoundError(f"⚠️ 未找到任何 YAML 配置文件于: {config_dir}")
for file in files:
with open(file) as f:
part = yaml.safe_load(f) or {}
merged = deep_merge(merged, part)
merged["__config_path__"] = config_dir # 可选调试字段
return merged

73
scripts/inventory.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
import json
import subprocess
import os
import yaml
from collections import defaultdict
def get_pulumi_outputs():
output = subprocess.check_output(["pulumi", "stack", "output", "--json"])
return json.loads(output)
def merge_instance_config(config_dir="config"):
merged = {}
for fname in os.listdir(config_dir):
if fname.endswith(".yaml"):
with open(os.path.join(config_dir, fname)) as f:
data = yaml.safe_load(f)
if isinstance(data, dict):
merged.update(data)
return merged.get("instances", [])
def build_inventory(pulumi_outputs, instance_cfgs):
inventory = {"_meta": {"hostvars": {}}}
groups = defaultdict(list)
for inst in instance_cfgs:
name = inst["name"]
public_ip = pulumi_outputs.get(f"{name}_ip")
if not public_ip:
continue # skip not created instances
# 默认分组all
groups["all"].append(name)
# 根据 subnet 或 lifecycle 添加分组
if "subnet" in inst:
groups[inst["subnet"]].append(name)
if "lifecycle" in inst:
groups[inst["lifecycle"]].append(name)
# hostvars
inventory["_meta"]["hostvars"][name] = {
"ansible_host": public_ip,
"ansible_user": "ubuntu",
"instance_type": inst.get("type"),
"ttl": inst.get("ttl", "none"),
"lifecycle": inst.get("lifecycle", "ondemand"),
}
# 将分组注入 inventory
for group, hosts in groups.items():
inventory[group] = {"hosts": hosts}
return inventory
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--list', action='store_true')
args = parser.parse_args()
if args.list:
pulumi_data = get_pulumi_outputs()
instance_cfgs = merge_instance_config()
inventory = build_inventory(pulumi_data, instance_cfgs)
print(json.dumps(inventory, indent=2))
else:
print(json.dumps({}))
if __name__ == "__main__":
main()

207
scripts/run.sh Normal file
View File

@ -0,0 +1,207 @@
#!/bin/bash
set -e
# 项目根目录(从任意位置运行都有效)
cd "$(dirname "$0")/.."
# ========== 参数解析 ==========
DEFAULT_ENV="dev"
DEFAULT_CONFIG="config"
if [[ -n "$1" && "$1" != up && "$1" != down && "$1" != delete && "$1" != export && "$1" != import && "$1" != init && "$1" != ansible && "$1" != help ]]; then
STACK_ENV="$1"
ACTION="${2:-up}"
else
STACK_ENV="${STACK_ENV:-$DEFAULT_ENV}"
ACTION="${1:-help}"
fi
STACK_NAME="${STACK_NAME:-$STACK_ENV}"
CONFIG_PATH="${CONFIG_PATH:-config/$STACK_ENV}"
# ========== 模块目录 ==========
PULUMI_DIR="iac_modules/pulumi"
TERRAFORM_DIR="iac_modules/terraform"
ANSIBLE_DIR="ansible"
# ========== 帮助信息 ==========
print_help() {
echo ""
echo "🧰 iac_cli - 多环境自动化管理器 (IaC + Ansible + GitOps)"
echo ""
echo "用法:"
echo " ./scripts/run.sh [env] [命令]"
echo " STACK_ENV=prod CONFIG_PATH=config/prod ./scripts/run.sh up"
echo ""
echo "🌍 当前环境: $STACK_ENV"
echo "📁 当前配置路径: $CONFIG_PATH"
echo ""
echo "支持命令:"
echo " up 🚀 部署资源"
echo " down 🔥 销毁资源"
echo " delete 🗑️ 删除 stack"
echo " export 📤 导出 stack 状态"
echo " import 📥 导入 stack 状态"
echo " init ⚙️ 初始化依赖"
echo " ansible 🧪 执行 ansible-playbook"
echo " help 📖 显示帮助"
echo ""
}
# ========== 检查 Pulumi ==========
ensure_pulumi() {
if ! command -v pulumi &> /dev/null; then
echo "📦 未检测到 Pulumi正在自动安装..."
case "$(uname | tr '[:upper:]' '[:lower:]')" in
linux)
curl -fsSL https://get.pulumi.com | sh
export PATH="$HOME/.pulumi/bin:$PATH"
;;
darwin)
brew install pulumi || (curl -fsSL https://get.pulumi.com | sh && export PATH="$HOME/.pulumi/bin:$PATH")
;;
msys*|mingw*|cygwin*)
echo "👉 Windows 用户请手动安装 Pulumihttps://www.pulumi.com/docs/get-started/install/"
exit 1
;;
*)
echo "❌ 当前平台不支持自动安装 Pulumi"
exit 1
;;
esac
fi
echo "✅ Pulumi 版本: $(pulumi version)"
}
# ========== 检查 Ansible ==========
ensure_ansible() {
if ! command -v ansible &> /dev/null; then
echo "❌ 未检测到 Ansible请手动安装"
case "$(uname | tr '[:upper:]' '[:lower:]')" in
linux)
echo "👉 Ubuntu/Debian: sudo apt install ansible"
echo "👉 RHEL/CentOS: sudo yum install ansible"
;;
darwin)
echo "👉 macOS: brew install ansible"
;;
msys*|mingw*|cygwin*)
echo "👉 Windows 用户请参考官方安装指南https://docs.ansible.com/"
;;
*)
echo "👉 其他平台请参考https://docs.ansible.com/"
;;
esac
exit 1
else
echo "✅ Ansible 已安装: $(ansible --version | head -n 1)"
fi
}
# ========== 检查 Terraform ==========
ensure_terraform() {
if ! command -v terraform &> /dev/null; then
echo "❌ 未检测到 Terraform请手动安装"
echo "👉 https://developer.hashicorp.com/terraform/install"
exit 1
fi
echo "✅ Terraform 已安装: $(terraform version | head -n1)"
}
# ========== 环境初始化检查 ==========
init_env() {
echo "⚙️ 初始化 Pulumi + Ansible 环境..."
# 1⃣ 检查 Pulumi
ensure_pulumi
# 2⃣ 安装 Python 依赖
if [ -f "$PULUMI_DIR/requirements.txt" ]; then
echo "📦 安装 Python 依赖..."
pip3 install -r "$PULUMI_DIR/requirements.txt"
fi
# 3⃣ 检查 Ansible
ensure_ansible
# 4⃣ 检查 Terraform可选
if [ -d "$TERRAFORM_DIR" ]; then
ensure_terraform
fi
# 5⃣ 初始化 Pulumi Stack
cd "$PULUMI_DIR"
pulumi login --local > /dev/null
if ! pulumi stack ls | grep -q "$STACK_NAME"; then
echo "📂 创建 Pulumi Stack: $STACK_NAME"
pulumi stack init "$STACK_NAME"
else
echo "✅ Stack 已存在:$STACK_NAME"
fi
echo "✅ 初始化完成 ✅"
}
# ========== 执行 Pulumi ==========
pulumi_run() {
cd "$PULUMI_DIR"
case "$ACTION" in
up)
if [ ! -d "$CONFIG_PATH" ] || [ -z "$(ls -A $CONFIG_PATH/*.yaml 2>/dev/null)" ]; then
echo "⚠️ 配置目录为空:$CONFIG_PATH,跳过部署"
exit 0
fi
echo "🚀 正在部署 stack: $STACK_NAME"
pulumi up --yes
;;
down)
echo "🔥 正在销毁 stack: $STACK_NAME"
pulumi destroy --yes
;;
delete)
echo "🗑️ 删除 Stack: $STACK_NAME"
pulumi stack rm "$STACK_NAME" --yes
;;
export)
echo "📤 导出 stack 状态"
pulumi stack export --file stack-export.json
;;
import)
echo "📥 导入 stack 状态"
pulumi stack import --file stack-export.json
;;
init)
init_env
;;
*)
print_help
;;
esac
}
# ========== 执行 Ansible ==========
run_ansible() {
if [ ! -f scripts/inventory.py ]; then
echo "❌ 未找到 scripts/inventory.py"
exit 1
fi
echo "🧪 执行 Ansible Playbook"
ansible-playbook -i scripts/inventory.py "$ANSIBLE_DIR/playbooks/setup.yml"
}
# ========== 分发 ==========
case "$ACTION" in
up|down|delete|export|import|init)
export CONFIG_PATH
export STACK_ENV
pulumi_run
;;
ansible)
run_ansible
;;
help|*)
print_help
;;
esac