Merge pull request #86 from svc-design/codex/split-modules-for-alicloud-landingzone
feat: align pulumi modules with alicloud landing zone
This commit is contained in:
commit
5fee357239
62
.github/workflows/iac-pipeline-alicloud-landingzone-baseline.yaml
vendored
Normal file
62
.github/workflows/iac-pipeline-alicloud-landingzone-baseline.yaml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: Alicloud Landing Zone Baseline
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'iac_modules/pulumi/**'
|
||||
- 'config/alicloud/**'
|
||||
- '.github/workflows/iac-pipeline-alicloud-landingzone-baseline.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PULUMI_CI: 'true'
|
||||
CONFIG_PATH: config/alicloud
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
name: Preview baseline changes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Pulumi preview
|
||||
uses: pulumi/actions@v4
|
||||
with:
|
||||
command: preview
|
||||
stack-name: alicloud/baseline-dev
|
||||
work-dir: iac_modules/pulumi
|
||||
env:
|
||||
ALICLOUD_ACCESS_KEY_ID: ${{ secrets.ALICLOUD_ACCESS_KEY_ID }}
|
||||
ALICLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUD_ACCESS_KEY_SECRET }}
|
||||
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
|
||||
|
||||
apply:
|
||||
name: Apply to production stack
|
||||
needs: preview
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Pulumi up
|
||||
uses: pulumi/actions@v4
|
||||
with:
|
||||
command: up
|
||||
stack-name: alicloud/baseline-prod
|
||||
work-dir: iac_modules/pulumi
|
||||
env:
|
||||
ALICLOUD_ACCESS_KEY_ID: ${{ secrets.ALICLOUD_ACCESS_KEY_ID }}
|
||||
ALICLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUD_ACCESS_KEY_SECRET }}
|
||||
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
|
||||
8
config/alicloud/audit.yaml
Normal file
8
config/alicloud/audit.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
audit:
|
||||
actiontrail:
|
||||
enabled: true
|
||||
name: lz-mvp-actiontrail
|
||||
oss_bucket_ref: lz-mvp-actiontrail-logs
|
||||
oss_key_prefix: actiontrail
|
||||
trail_region: cn-hangzhou
|
||||
event_rw: All
|
||||
5
config/alicloud/base.yaml
Normal file
5
config/alicloud/base.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
alicloud:
|
||||
region: cn-hangzhou
|
||||
default_tags:
|
||||
project: landingzone-mvp
|
||||
owner: your-github-handle
|
||||
27
config/alicloud/config-service.yaml
Normal file
27
config/alicloud/config-service.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
config_service:
|
||||
recorder:
|
||||
name: lz-config-recorder
|
||||
resource_types:
|
||||
- ACS::ECS::Instance
|
||||
- ACS::OSS::Bucket
|
||||
- ACS::VPC::VSwitch
|
||||
delivery_channel:
|
||||
name: lz-config-delivery
|
||||
display_name: LandingZoneBaseline
|
||||
type: OSS
|
||||
target_arn: acs:oss:cn-hangzhou:${AliUid}:lz-mvp-actiontrail-logs
|
||||
assume_role_arn: acs:ram::${AliUid}:role/aliyunconfigdefaultrole
|
||||
description: Deliver baseline compliance evaluations to OSS
|
||||
status: 1
|
||||
rules:
|
||||
- name: lz-required-env-tag
|
||||
description: Ensure env tag exists on core resources
|
||||
source_identifier: ecs-instance-required-tag
|
||||
source_owner: ALIYUN
|
||||
risk_level: 2
|
||||
trigger_types: ConfigurationItemChangeNotification
|
||||
resource_types_scopes:
|
||||
- ACS::ECS::Instance
|
||||
input_parameters:
|
||||
tagKey: env
|
||||
maximum_execution_frequency: TwentyFour_Hours
|
||||
28
config/alicloud/identity.yaml
Normal file
28
config/alicloud/identity.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
identity:
|
||||
users:
|
||||
- name: ops-automation
|
||||
display_name: Landing Zone Automation
|
||||
comments: Dedicated RAM user for IaC pipelines
|
||||
policies:
|
||||
- name: AliyunOSSFullAccess
|
||||
type: System
|
||||
- name: AliyunVPCFullAccess
|
||||
type: System
|
||||
- name: AliyunConfigFullAccess
|
||||
type: System
|
||||
- name: audit-viewer
|
||||
display_name: Landing Zone Auditor
|
||||
comments: Read-only access for monitoring
|
||||
policies:
|
||||
- name: ReadOnlyAccess
|
||||
type: System
|
||||
groups:
|
||||
- name: ops-admins
|
||||
comments: Baseline operations team
|
||||
policies:
|
||||
- name: AliyunConfigFullAccess
|
||||
type: System
|
||||
- name: AliyunVPCFullAccess
|
||||
type: System
|
||||
users:
|
||||
- ops-automation
|
||||
18
config/alicloud/network.yaml
Normal file
18
config/alicloud/network.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
network:
|
||||
vpcs:
|
||||
- name: lz-main-vpc
|
||||
cidr_block: 10.10.0.0/16
|
||||
description: Landing zone baseline VPC
|
||||
tags:
|
||||
env: shared
|
||||
vswitches:
|
||||
- name: lz-prod-subnet
|
||||
cidr_block: 10.10.1.0/24
|
||||
zone_id: cn-hangzhou-h
|
||||
tags:
|
||||
env: prod
|
||||
- name: lz-test-subnet
|
||||
cidr_block: 10.10.2.0/24
|
||||
zone_id: cn-hangzhou-h
|
||||
tags:
|
||||
env: test
|
||||
18
config/alicloud/security.yaml
Normal file
18
config/alicloud/security.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
security:
|
||||
groups:
|
||||
- name: lz-base-sg
|
||||
vpc: lz-main-vpc
|
||||
description: Baseline security group allowing outbound traffic only
|
||||
tags:
|
||||
env: shared
|
||||
ingress:
|
||||
- protocol: tcp
|
||||
port_range: "22/22"
|
||||
cidr_ip: 0.0.0.0/0
|
||||
description: Temporary SSH access for break-glass
|
||||
policy: accept
|
||||
egress:
|
||||
- protocol: all
|
||||
port_range: "-1/-1"
|
||||
cidr_ip: 0.0.0.0/0
|
||||
policy: accept
|
||||
17
config/alicloud/storage.yaml
Normal file
17
config/alicloud/storage.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
storage:
|
||||
oss_buckets:
|
||||
- name: lz-mvp-actiontrail-logs
|
||||
bucket: lz-mvp-actiontrail-logs
|
||||
storage_class: Standard
|
||||
versioning:
|
||||
status: Enabled
|
||||
lifecycle_rules:
|
||||
- id: archive-audit-logs
|
||||
enabled: true
|
||||
transitions:
|
||||
- storage_class: IA
|
||||
days: 180
|
||||
- storage_class: Archive
|
||||
days: 365
|
||||
tags:
|
||||
env: prod
|
||||
@ -1,42 +1,55 @@
|
||||
# ☁️ Pulumi 多模块 AWS IaaS 模板
|
||||
# ☁️ Pulumi Alicloud Landing Zone Baseline
|
||||
|
||||
该目录基于 Pulumi 构建,支持以下模块:
|
||||
该目录提供了与《docs/landingzone/alicloud-landingzone-mvp-single-account.md》一致的 Pulumi Python 实现,用于在单账号阿里云环境中快速落地身份、审计、配置合规、网络与安全基线。
|
||||
|
||||
## ✅ 模块支持
|
||||
## ✅ 模块拆分
|
||||
|
||||
- VPC + 子网(自动分配 CIDR,支持 enabled 控制)
|
||||
- 安全组(通过 firewall.yaml 控制 ingress/egress)
|
||||
- EC2 实例(支持 spot、AMI keyword、user_data、自动标签)
|
||||
- AMI 自动识别(支持 `Ubuntu 22.04`, `Rocky Linux 8.10` 等)
|
||||
- Pulumi Credentials 自动加载 ~/.aws/profile
|
||||
- 环境配置文件支持多目录(如 `config/sit/`, `config/prod/`)
|
||||
| 模块 | 说明 |
|
||||
| --- | --- |
|
||||
| `modules/identity/ram.py` | 创建 RAM 用户、用户组以及策略绑定,覆盖 `ops-automation`、`audit-viewer` 等身份。 |
|
||||
| `modules/storage/oss.py` | 管理 OSS 日志桶,支持版本控制与生命周期策略,用于 ActionTrail & Pulumi 状态。 |
|
||||
| `modules/audit/actiontrail.py` | 启用 ActionTrail,将操作日志投递到指定 OSS Bucket。 |
|
||||
| `modules/config_service/baseline.py` | 初始化 Cloud Config Recorder、Delivery Channel 与基础规则。 |
|
||||
| `modules/network/vpc.py` | 构建单 VPC + 双可用区交换机的网络基线。 |
|
||||
| `modules/security/security_groups.py` | 创建默认安全组及入站/出站规则,默认仅放行出站流量。 |
|
||||
|
||||
## 📂 配置结构
|
||||
|
||||
`config/alicloud/` 目录提供示例配置,按照 Landing Zone 设计拆分:
|
||||
|
||||
- `base.yaml`:区域与全局标签
|
||||
- `identity.yaml`:RAM 用户/用户组与策略
|
||||
- `storage.yaml`:ActionTrail 日志桶(版本控制+生命周期)
|
||||
- `network.yaml`:VPC / 交换机拓扑
|
||||
- `security.yaml`:安全组与规则
|
||||
- `audit.yaml`:ActionTrail 开关与目标 OSS
|
||||
- `config-service.yaml`:Cloud Config 基线配置
|
||||
|
||||
> ⚠️ 其中 `target_arn`、`assume_role_arn` 等字段需替换为实际账号 ID(`${AliUid}`)。
|
||||
|
||||
## 🚀 使用方式
|
||||
|
||||
## 🚀 快速部署
|
||||
```bash
|
||||
初始化并部署 bash scripts/run.sh sit up
|
||||
|
||||
## 📂 配置说明
|
||||
|
||||
# config/sit/instances.yaml
|
||||
|
||||
```yaml
|
||||
instances:
|
||||
- name: master-1
|
||||
ami: Ubuntu 22.04
|
||||
type: t3.micro
|
||||
subnet: public-subnet-1
|
||||
disk_size_gb: 20
|
||||
lifecycle: spot
|
||||
ttl: 1h
|
||||
```yaml
|
||||
|
||||
## 🧹 清理资源
|
||||
|
||||
删除资源 + 刷新状态
|
||||
- bash scripts/run.sh sit down
|
||||
- pulumi refresh --yes
|
||||
|
||||
## 📦 依赖
|
||||
Python >= 3.8
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
AWS CLI 已配置 ~/.aws/credentials
|
||||
|
||||
# 设置配置目录(默认读取 config/,此处指向示例配置)
|
||||
export CONFIG_PATH=config/alicloud
|
||||
|
||||
# Pulumi 登录(可选:使用 OSS backend 或 Pulumi Service)
|
||||
pulumi login
|
||||
|
||||
# 预览或部署
|
||||
pulumi preview --cwd iac_modules/pulumi
|
||||
pulumi up --cwd iac_modules/pulumi
|
||||
```
|
||||
|
||||
## 🔒 GitHub Actions 自动化
|
||||
|
||||
新增 `.github/workflows/iac-pipeline-alicloud-landingzone-baseline.yaml`,结合 `pulumi/actions@v4` 实现 Preview + 主干自动部署,使用 Secrets 管理 `ALICLOUD_ACCESS_KEY_ID/SECRET` 与 `PULUMI_ACCESS_TOKEN`。
|
||||
|
||||
## 🧩 扩展建议
|
||||
|
||||
- 根据生产需求扩展 Cloud Config 规则或引入企业版聚合器。
|
||||
- 在安全组模块中追加环境专属规则(Prod/Test)。
|
||||
- 利用 `pulumi stack` 拆分 dev/prod 状态,配合 GitHub Environments 审批。
|
||||
|
||||
@ -1,155 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
|
||||
import pulumi
|
||||
import pulumi_aws as aws
|
||||
import boto3
|
||||
from botocore.exceptions import ProfileNotFound, NoCredentialsError
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
from modules import (
|
||||
create_oss_buckets,
|
||||
create_ram_identity,
|
||||
create_security_groups,
|
||||
create_vpc_topology,
|
||||
enable_actiontrail,
|
||||
enable_config_baseline,
|
||||
)
|
||||
from utils.config_loader import load_merged_config
|
||||
from modules.vpc.vpc import create_vpcs
|
||||
from modules.security_group.sg import create_security_group
|
||||
from modules.ec2.ec2_instance import create_instances
|
||||
|
||||
# ✅ 加载配置
|
||||
config_dir = os.environ.get("CONFIG_PATH", "config")
|
||||
config = load_merged_config(config_dir)
|
||||
|
||||
aws_conf = config.get("aws", {})
|
||||
region = aws_conf.get("region", "us-east-1")
|
||||
profile = aws_conf.get("profile", "default")
|
||||
key_pairs = aws_conf.get("key_pairs", [])
|
||||
|
||||
# ✅ 设置 AWS 配置
|
||||
aws.config.region = region
|
||||
aws.config.profile = profile
|
||||
pulumi.runtime.set_config("aws:region", region)
|
||||
|
||||
# ✅ 检查 AWS 凭证
|
||||
try:
|
||||
session = boto3.Session(profile_name=profile)
|
||||
credentials = session.get_credentials()
|
||||
if not credentials:
|
||||
raise NoCredentialsError()
|
||||
except (ProfileNotFound, NoCredentialsError):
|
||||
pulumi.log.error(f"❌ AWS profile '{profile}' 无效或找不到凭证")
|
||||
sys.exit(1)
|
||||
else:
|
||||
pulumi.log.info(f"✅ AWS credentials loaded (profile: {profile}, region: {region})")
|
||||
|
||||
# ✅ 初始化资源容器
|
||||
global_dependencies = []
|
||||
vpc = None
|
||||
subnets = {}
|
||||
sg = None
|
||||
key_pair = None
|
||||
|
||||
# ========================
|
||||
# ✅ [模块] VPC + Subnets
|
||||
vpc_confs = config.get("vpcs", [])
|
||||
if vpc_confs:
|
||||
vpc_results = create_vpcs(vpc_confs, region)
|
||||
all_subnets = {}
|
||||
for vpc_name, result in vpc_results.items():
|
||||
pulumi.log.info(f"✅ VPC {vpc_name} 已创建")
|
||||
global_dependencies.append(result["vpc"])
|
||||
global_dependencies.extend(result["subnets"].values())
|
||||
all_subnets.update(result["subnets"])
|
||||
subnets = all_subnets
|
||||
else:
|
||||
pulumi.log.warn("⏭️ 跳过 VPC 创建")
|
||||
|
||||
# ========================
|
||||
# ✅ [模块] 多个 Security Group
|
||||
# ========================
|
||||
|
||||
# ✅ 存储 VPC 结果(名字 → 资源)
|
||||
vpc_map = {vpc_name: result["vpc"] for vpc_name, result in vpc_results.items()}
|
||||
|
||||
firewall_rules = config.get("firewall_rules", [])
|
||||
security_groups = {}
|
||||
|
||||
if firewall_rules and config.get("security_group", {}).get("enabled", True):
|
||||
for rule in firewall_rules:
|
||||
if not rule.get("enabled", True):
|
||||
pulumi.log.warn(f"⏭️ 跳过未启用的 SG: {rule.get('name')}")
|
||||
continue
|
||||
|
||||
vpc_name = rule.get("vpc_name")
|
||||
if not vpc_name or vpc_name not in vpc_map:
|
||||
pulumi.log.warn(f"❌ 未找到指定 VPC: {vpc_name},跳过 {rule.get('name')}")
|
||||
continue
|
||||
|
||||
vpc_resource = vpc_map[vpc_name]
|
||||
|
||||
sg = create_security_group(vpc_resource.id, rule)
|
||||
name = rule.get("name", "sg-unnamed")
|
||||
security_groups[name] = sg
|
||||
global_dependencies.append(sg)
|
||||
|
||||
# 确保 SG 创建等待 VPC 完成
|
||||
pulumi.log.info(f"✅ Security Group '{name}' 已绑定 VPC: {vpc_name}")
|
||||
|
||||
pulumi.export("security_groups", {k: sg.id for k, sg in security_groups.items()})
|
||||
else:
|
||||
pulumi.log.warn("⏭️ 跳过 Security Group 创建")
|
||||
|
||||
|
||||
# ========================
|
||||
# ✅ [模块] SSH Key Pair
|
||||
# ========================
|
||||
if key_pairs:
|
||||
key_cfg = key_pairs[0]
|
||||
public_key_path = os.path.expanduser(key_cfg["key_file"])
|
||||
if not os.path.exists(public_key_path):
|
||||
raise FileNotFoundError(f"❌ SSH 公钥文件不存在: {public_key_path}")
|
||||
with open(public_key_path) as f:
|
||||
public_key = f.read().strip()
|
||||
key_pair = aws.ec2.KeyPair("main-key",
|
||||
key_name=key_cfg["name"],
|
||||
public_key=public_key
|
||||
)
|
||||
global_dependencies.append(key_pair)
|
||||
pulumi.log.info("✅ SSH KeyPair 已创建")
|
||||
else:
|
||||
pulumi.log.warn("⏭️ 跳过 KeyPair 创建")
|
||||
def main() -> None:
|
||||
config_dir = os.environ.get("CONFIG_PATH", "config/alicloud")
|
||||
config = load_merged_config(config_dir)
|
||||
|
||||
# ========================
|
||||
# ✅ [模块] EC2 实例部署
|
||||
# ========================
|
||||
alicloud_conf: Dict[str, object] = config.get("alicloud", {}) # type: ignore[assignment]
|
||||
region = alicloud_conf.get("region")
|
||||
profile = alicloud_conf.get("profile")
|
||||
default_tags = alicloud_conf.get("default_tags", {})
|
||||
|
||||
# ========================
|
||||
# ✅ [模块] EC2 实例部署
|
||||
# ========================
|
||||
instances_conf = config.get("instances", [])
|
||||
ec2_outputs = {}
|
||||
if region:
|
||||
alicloud.config.region = region
|
||||
pulumi.export("region", region)
|
||||
if profile:
|
||||
alicloud.config.profile = profile
|
||||
|
||||
if instances_conf and config.get("ec2", {}).get("enabled", True):
|
||||
# ✅ 遍历每个实例,按 sg_names 匹配对应 Security Group ID 列表
|
||||
def resolve_security_group_ids(instance_conf, sg_map):
|
||||
sg_ids = []
|
||||
for name in instance_conf.get("sg_names", []):
|
||||
sg = sg_map.get(name)
|
||||
if sg:
|
||||
sg_ids.append(sg.id)
|
||||
else:
|
||||
pulumi.log.warn(f"⚠️ 实例 {instance_conf['name']} 引用了未知 SG: {name}")
|
||||
return sg_ids
|
||||
|
||||
# ✅ 批量传入所有实例配置
|
||||
ec2_outputs = create_instances(
|
||||
instances_conf,
|
||||
subnets,
|
||||
security_groups, # ✅ 多 SG 映射 sg_name → resource
|
||||
key_pair.key_name if key_pair else None,
|
||||
depends_on=global_dependencies
|
||||
pulumi.log.info(
|
||||
"Loaded Alicloud configuration",
|
||||
)
|
||||
|
||||
pulumi.log.info("✅ EC2 实例已创建")
|
||||
else:
|
||||
pulumi.log.warn("⏭️ 跳过 EC2 实例部署")
|
||||
identity_results = create_ram_identity(config.get("identity", {}))
|
||||
|
||||
# ========================
|
||||
# ✅ 导出所有实例信息
|
||||
# ========================
|
||||
for name, ip in ec2_outputs.items():
|
||||
pulumi.export(f"{name}", ip)
|
||||
buckets = create_oss_buckets(config.get("storage", {}), default_tags)
|
||||
|
||||
network_results = create_vpc_topology(config.get("network", {}), default_tags)
|
||||
vpcs = network_results.get("vpcs", {})
|
||||
|
||||
security_groups = create_security_groups(
|
||||
config.get("security", {}), vpcs, default_tags
|
||||
)
|
||||
|
||||
enable_actiontrail(config.get("audit", {}), buckets)
|
||||
|
||||
enable_config_baseline(config.get("config_service", {}), buckets)
|
||||
|
||||
pulumi.export("ram_user_count", len(identity_results.get("users", {})))
|
||||
pulumi.export("security_group_count", len(security_groups))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
17
iac_modules/pulumi/modules/alicloud/__init__.py
Normal file
17
iac_modules/pulumi/modules/alicloud/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .audit.actiontrail import enable_actiontrail
|
||||
from .config_service.baseline import enable_config_baseline
|
||||
from .identity.ram import create_ram_identity
|
||||
from .network.vpc import create_vpc_topology
|
||||
from .security.security_groups import create_security_groups
|
||||
from .storage.oss import create_oss_buckets
|
||||
|
||||
__all__ = [
|
||||
"enable_actiontrail",
|
||||
"enable_config_baseline",
|
||||
"create_ram_identity",
|
||||
"create_vpc_topology",
|
||||
"create_security_groups",
|
||||
"create_oss_buckets",
|
||||
]
|
||||
53
iac_modules/pulumi/modules/alicloud/audit/actiontrail.py
Normal file
53
iac_modules/pulumi/modules/alicloud/audit/actiontrail.py
Normal file
@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Mapping, Optional
|
||||
|
||||
import pulumi
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
|
||||
def enable_actiontrail(
|
||||
audit_conf: Mapping[str, object],
|
||||
buckets: Mapping[str, pulumi.Resource],
|
||||
) -> Optional[pulumi.Resource]:
|
||||
trail_conf = audit_conf.get("actiontrail") if audit_conf else None
|
||||
if not trail_conf:
|
||||
pulumi.log.info("ActionTrail configuration not provided; skipping setup")
|
||||
return None
|
||||
|
||||
if not trail_conf.get("enabled", True):
|
||||
pulumi.log.info("ActionTrail disabled via configuration; skipping setup")
|
||||
return None
|
||||
|
||||
bucket_name = trail_conf.get("oss_bucket_name")
|
||||
bucket_reference = trail_conf.get("oss_bucket_ref")
|
||||
if not bucket_name and bucket_reference:
|
||||
bucket = buckets.get(bucket_reference)
|
||||
if bucket is None:
|
||||
pulumi.log.warn(
|
||||
f"ActionTrail bucket reference '{bucket_reference}' could not be resolved"
|
||||
)
|
||||
else:
|
||||
bucket_name = bucket.bucket
|
||||
|
||||
if not bucket_name:
|
||||
pulumi.log.warn("No OSS bucket specified for ActionTrail; skipping trail creation")
|
||||
return None
|
||||
|
||||
name = trail_conf.get("name", "landingzone-actiontrail")
|
||||
trail = alicloud.actiontrail.Trail(
|
||||
name,
|
||||
trail_name=trail_conf.get("trail_name", name),
|
||||
event_rw=trail_conf.get("event_rw", "All"),
|
||||
oss_bucket_name=bucket_name,
|
||||
oss_key_prefix=trail_conf.get("oss_key_prefix"),
|
||||
trail_region=trail_conf.get("trail_region"),
|
||||
is_organization_trail=trail_conf.get("is_organization_trail"),
|
||||
oss_write_role_arn=trail_conf.get("oss_write_role_arn"),
|
||||
sls_project_arn=trail_conf.get("sls_project_arn"),
|
||||
sls_write_role_arn=trail_conf.get("sls_write_role_arn"),
|
||||
status=trail_conf.get("status"),
|
||||
)
|
||||
|
||||
pulumi.export("actiontrail_trail", trail.trail_name)
|
||||
return trail
|
||||
33
iac_modules/pulumi/modules/alicloud/common/tags.py
Normal file
33
iac_modules/pulumi/modules/alicloud/common/tags.py
Normal file
@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Mapping, Optional
|
||||
|
||||
import pulumi
|
||||
|
||||
|
||||
def merge_tags(*tag_sets: Optional[Mapping[str, str]]) -> Optional[Dict[str, str]]:
|
||||
"""Merge multiple tag dictionaries while filtering out falsy values."""
|
||||
merged: Dict[str, str] = {}
|
||||
for tags in tag_sets:
|
||||
if not tags:
|
||||
continue
|
||||
for key, value in tags.items():
|
||||
if value is None:
|
||||
continue
|
||||
merged[str(key)] = str(value)
|
||||
return merged or None
|
||||
|
||||
|
||||
def taggable_args(
|
||||
default_tags: Optional[Mapping[str, str]],
|
||||
resource_tags: Optional[Mapping[str, str]] = None,
|
||||
) -> Dict[str, object]:
|
||||
"""Helper to build Pulumi args with merged tags."""
|
||||
tags = merge_tags(default_tags, resource_tags)
|
||||
return {"tags": tags} if tags else {}
|
||||
|
||||
|
||||
def annotate_resource_with_tags(resource: pulumi.CustomResource, tags: Optional[Mapping[str, str]]) -> None:
|
||||
"""Attach merged tags to the Pulumi resource options after creation."""
|
||||
if tags:
|
||||
pulumi.export(f"tags::{resource._name}", tags)
|
||||
@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Mapping
|
||||
|
||||
import pulumi
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
|
||||
def enable_config_baseline(
|
||||
config_conf: Mapping[str, object],
|
||||
buckets: Mapping[str, pulumi.Resource],
|
||||
) -> Dict[str, object]:
|
||||
if not config_conf:
|
||||
pulumi.log.info("Cloud Config configuration not provided; skipping setup")
|
||||
return {}
|
||||
|
||||
resources: Dict[str, object] = {}
|
||||
|
||||
recorder_conf = config_conf.get("recorder")
|
||||
recorder = None
|
||||
if recorder_conf:
|
||||
recorder = alicloud.cfg.ConfigurationRecorder(
|
||||
recorder_conf.get("name", "config-recorder"),
|
||||
enterprise_edition=recorder_conf.get("enterprise_edition"),
|
||||
resource_types=recorder_conf.get("resource_types"),
|
||||
)
|
||||
resources["recorder"] = recorder
|
||||
|
||||
delivery_conf = config_conf.get("delivery_channel")
|
||||
delivery_channel = None
|
||||
if delivery_conf:
|
||||
target_arn = delivery_conf.get("target_arn")
|
||||
bucket_ref = delivery_conf.get("oss_bucket_ref")
|
||||
if not target_arn and bucket_ref:
|
||||
bucket = buckets.get(bucket_ref)
|
||||
if bucket:
|
||||
target_arn = delivery_conf.get("target_arn_fallback")
|
||||
pulumi.log.info(
|
||||
"Delivery channel target ARN not provided explicitly; using fallback"
|
||||
)
|
||||
else:
|
||||
pulumi.log.warn(
|
||||
f"Delivery channel bucket reference '{bucket_ref}' could not be resolved"
|
||||
)
|
||||
if target_arn:
|
||||
delivery_channel = alicloud.cfg.DeliveryChannel(
|
||||
delivery_conf.get("name", "config-delivery-channel"),
|
||||
delivery_channel_name=delivery_conf.get("display_name"),
|
||||
description=delivery_conf.get("description"),
|
||||
delivery_channel_type=delivery_conf.get("type", "OSS"),
|
||||
delivery_channel_target_arn=target_arn,
|
||||
delivery_channel_assume_role_arn=delivery_conf.get("assume_role_arn"),
|
||||
delivery_channel_condition=delivery_conf.get("condition"),
|
||||
status=delivery_conf.get("status"),
|
||||
)
|
||||
resources["delivery_channel"] = delivery_channel
|
||||
else:
|
||||
pulumi.log.warn("Cloud Config delivery channel requires a target ARN; skipping")
|
||||
|
||||
for rule_conf in config_conf.get("rules", []) or []:
|
||||
required_fields = ["name", "source_identifier"]
|
||||
if any(field not in rule_conf for field in required_fields):
|
||||
pulumi.log.warn(
|
||||
f"Skipping Cloud Config rule definition due to missing fields: {rule_conf}"
|
||||
)
|
||||
continue
|
||||
|
||||
rule_args = {
|
||||
"rule_name": rule_conf["name"],
|
||||
"description": rule_conf.get("description"),
|
||||
"risk_level": rule_conf.get("risk_level", 2),
|
||||
"source_owner": rule_conf.get("source_owner", "ALIYUN"),
|
||||
"source_identifier": rule_conf["source_identifier"],
|
||||
"config_rule_trigger_types": rule_conf.get(
|
||||
"trigger_types", "ConfigurationItemChangeNotification"
|
||||
),
|
||||
"resource_types_scopes": rule_conf.get("resource_types_scopes"),
|
||||
"region_ids_scope": rule_conf.get("region_ids_scope"),
|
||||
"resource_group_ids_scope": rule_conf.get("resource_group_ids_scope"),
|
||||
"tag_key_scope": rule_conf.get("tag_key_scope"),
|
||||
"tag_value_scope": rule_conf.get("tag_value_scope"),
|
||||
"input_parameters": rule_conf.get("input_parameters"),
|
||||
"maximum_execution_frequency": rule_conf.get("maximum_execution_frequency"),
|
||||
"status": rule_conf.get("status"),
|
||||
}
|
||||
rule_args = {key: value for key, value in rule_args.items() if value is not None}
|
||||
if delivery_channel:
|
||||
rule_args["delivery_channel_id"] = delivery_channel.id
|
||||
if recorder:
|
||||
rule_args["configuration_recorder_id"] = recorder.id
|
||||
|
||||
rule = alicloud.cfg.Rule(rule_conf["name"], **rule_args)
|
||||
resources.setdefault("rules", {})[rule_conf["name"]] = rule
|
||||
|
||||
return resources
|
||||
89
iac_modules/pulumi/modules/alicloud/identity/ram.py
Normal file
89
iac_modules/pulumi/modules/alicloud/identity/ram.py
Normal file
@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import Dict, Iterable, Mapping, Optional
|
||||
|
||||
import pulumi
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
|
||||
PolicyConfig = Mapping[str, str]
|
||||
UserConfig = Mapping[str, object]
|
||||
GroupConfig = Mapping[str, object]
|
||||
|
||||
|
||||
def _normalize_policies(policies: Optional[Iterable[PolicyConfig]]) -> Iterable[PolicyConfig]:
|
||||
return policies or []
|
||||
|
||||
|
||||
def create_ram_identity(
|
||||
identity_conf: Mapping[str, object],
|
||||
) -> Dict[str, Dict[str, pulumi.Resource]]:
|
||||
"""Create RAM users, groups, and policy attachments based on configuration."""
|
||||
users_conf = identity_conf.get("users", []) or []
|
||||
groups_conf = identity_conf.get("groups", []) or []
|
||||
|
||||
users: Dict[str, pulumi.Resource] = {}
|
||||
groups: Dict[str, pulumi.Resource] = {}
|
||||
|
||||
for user_conf in users_conf:
|
||||
name = user_conf["name"]
|
||||
args = {
|
||||
"name": name,
|
||||
"display_name": user_conf.get("display_name"),
|
||||
"email": user_conf.get("email"),
|
||||
"mobile": user_conf.get("mobile"),
|
||||
"comments": user_conf.get("comments"),
|
||||
"force": user_conf.get("force_destroy"),
|
||||
}
|
||||
args = {k: v for k, v in args.items() if v is not None}
|
||||
user = alicloud.ram.User(name, **args)
|
||||
users[name] = user
|
||||
|
||||
for index, policy in enumerate(_normalize_policies(user_conf.get("policies"))):
|
||||
alicloud.ram.UserPolicyAttachment(
|
||||
f"{name}-policy-{index}",
|
||||
policy_name=policy["name"],
|
||||
policy_type=policy.get("type", "System"),
|
||||
user_name=name,
|
||||
opts=pulumi.ResourceOptions(depends_on=[user]),
|
||||
)
|
||||
|
||||
for group_conf in groups_conf:
|
||||
name = group_conf["name"]
|
||||
args = {
|
||||
"group_name": name,
|
||||
"comments": group_conf.get("comments"),
|
||||
"force": group_conf.get("force_destroy"),
|
||||
}
|
||||
args = {k: v for k, v in args.items() if v is not None}
|
||||
group = alicloud.ram.Group(name, **args)
|
||||
groups[name] = group
|
||||
|
||||
for index, policy in enumerate(_normalize_policies(group_conf.get("policies"))):
|
||||
alicloud.ram.GroupPolicyAttachment(
|
||||
f"{name}-policy-{index}",
|
||||
group_name=name,
|
||||
policy_name=policy["name"],
|
||||
policy_type=policy.get("type", "System"),
|
||||
opts=pulumi.ResourceOptions(depends_on=[group]),
|
||||
)
|
||||
|
||||
members = group_conf.get("users") or []
|
||||
missing_members = [user for user in members if user not in users]
|
||||
if missing_members:
|
||||
pulumi.log.warn(
|
||||
f"RAM group '{name}' references users not defined in configuration: {', '.join(missing_members)}"
|
||||
)
|
||||
if members:
|
||||
alicloud.ram.GroupMembership(
|
||||
f"{name}-membership",
|
||||
group_name=name,
|
||||
user_names=[user for user in members if user in users],
|
||||
opts=pulumi.ResourceOptions(depends_on=[group] + [users[user] for user in members if user in users]),
|
||||
)
|
||||
|
||||
pulumi.export("ram_users", {name: user.name for name, user in users.items()})
|
||||
pulumi.export("ram_groups", {name: group.group_name for name, group in groups.items()})
|
||||
|
||||
return {"users": users, "groups": groups}
|
||||
51
iac_modules/pulumi/modules/alicloud/network/vpc.py
Normal file
51
iac_modules/pulumi/modules/alicloud/network/vpc.py
Normal file
@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Mapping, Optional
|
||||
|
||||
import pulumi
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
from ..common.tags import merge_tags
|
||||
|
||||
|
||||
def create_vpc_topology(
|
||||
network_conf: Mapping[str, object],
|
||||
default_tags: Optional[Mapping[str, str]] = None,
|
||||
) -> Dict[str, Dict[str, pulumi.Resource]]:
|
||||
"""Create VPCs and VSwitches as described in the network configuration."""
|
||||
vpcs_conf = network_conf.get("vpcs", []) or []
|
||||
|
||||
vpcs: Dict[str, pulumi.Resource] = {}
|
||||
vswitches: Dict[str, pulumi.Resource] = {}
|
||||
|
||||
for vpc_conf in vpcs_conf:
|
||||
name = vpc_conf["name"]
|
||||
vpc_tags = merge_tags(default_tags, vpc_conf.get("tags"))
|
||||
vpc = alicloud.vpc.Network(
|
||||
name,
|
||||
vpc_name=vpc_conf.get("display_name", name),
|
||||
cidr_block=vpc_conf["cidr_block"],
|
||||
description=vpc_conf.get("description"),
|
||||
**({"tags": vpc_tags} if vpc_tags else {}),
|
||||
)
|
||||
vpcs[name] = vpc
|
||||
|
||||
for switch_conf in vpc_conf.get("vswitches", []) or []:
|
||||
switch_name = switch_conf["name"]
|
||||
switch_tags = merge_tags(vpc_tags, switch_conf.get("tags"))
|
||||
vswitch = alicloud.vpc.Switch(
|
||||
switch_name,
|
||||
vswitch_name=switch_conf.get("display_name", switch_name),
|
||||
vpc_id=vpc.id,
|
||||
cidr_block=switch_conf["cidr_block"],
|
||||
zone_id=switch_conf["zone_id"],
|
||||
description=switch_conf.get("description"),
|
||||
**({"tags": switch_tags} if switch_tags else {}),
|
||||
opts=pulumi.ResourceOptions(depends_on=[vpc]),
|
||||
)
|
||||
vswitches[switch_name] = vswitch
|
||||
|
||||
pulumi.export("vpc_ids", {name: vpc.id for name, vpc in vpcs.items()})
|
||||
pulumi.export("vswitch_ids", {name: sw.id for name, sw in vswitches.items()})
|
||||
|
||||
return {"vpcs": vpcs, "vswitches": vswitches}
|
||||
@ -0,0 +1,86 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Mapping, Optional
|
||||
|
||||
import pulumi
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
from ..common.tags import merge_tags
|
||||
|
||||
|
||||
def create_security_groups(
|
||||
security_conf: Mapping[str, object],
|
||||
vpcs: Mapping[str, pulumi.Resource],
|
||||
default_tags: Optional[Mapping[str, str]] = None,
|
||||
) -> Dict[str, pulumi.Resource]:
|
||||
groups_conf = security_conf.get("groups", []) or []
|
||||
security_groups: Dict[str, pulumi.Resource] = {}
|
||||
|
||||
for group_conf in groups_conf:
|
||||
name = group_conf["name"]
|
||||
vpc_name = group_conf.get("vpc")
|
||||
vpc = vpcs.get(vpc_name) if vpc_name else None
|
||||
if vpc is None:
|
||||
pulumi.log.warn(f"Skip security group '{name}' because VPC '{vpc_name}' was not found")
|
||||
continue
|
||||
|
||||
tags = merge_tags(default_tags, group_conf.get("tags"))
|
||||
sg = alicloud.ecs.SecurityGroup(
|
||||
name,
|
||||
security_group_name=group_conf.get("display_name", name),
|
||||
description=group_conf.get("description"),
|
||||
security_group_type=group_conf.get("type", "normal"),
|
||||
vpc_id=vpc.id,
|
||||
**({"tags": tags} if tags else {}),
|
||||
opts=pulumi.ResourceOptions(depends_on=[vpc]),
|
||||
)
|
||||
security_groups[name] = sg
|
||||
|
||||
for index, rule in enumerate(group_conf.get("ingress", []) or []):
|
||||
_create_rule(sg, rule, "ingress", index)
|
||||
|
||||
for index, rule in enumerate(group_conf.get("egress", []) or []):
|
||||
_create_rule(sg, rule, "egress", index)
|
||||
|
||||
pulumi.export("security_group_ids", {name: sg.id for name, sg in security_groups.items()})
|
||||
return security_groups
|
||||
|
||||
|
||||
def _create_rule(
|
||||
sg: pulumi.Resource,
|
||||
rule_conf: Mapping[str, object],
|
||||
rule_type: str,
|
||||
index: int,
|
||||
) -> None:
|
||||
protocol = rule_conf.get("protocol", "all")
|
||||
cidr_ip = rule_conf.get("cidr_ip")
|
||||
ipv6_cidr_ip = rule_conf.get("ipv6_cidr_ip")
|
||||
source_sg = rule_conf.get("source_security_group_id")
|
||||
prefix_list_id = rule_conf.get("prefix_list_id")
|
||||
|
||||
if not any([cidr_ip, ipv6_cidr_ip, source_sg, prefix_list_id]):
|
||||
pulumi.log.warn(
|
||||
f"Security group {sg._name} {rule_type} rule #{index} does not define a source/destination; skipping"
|
||||
)
|
||||
return
|
||||
|
||||
args = {
|
||||
"security_group_id": sg.id,
|
||||
"type": rule_type,
|
||||
"ip_protocol": protocol,
|
||||
"port_range": rule_conf.get("port_range", "-1/-1"),
|
||||
"cidr_ip": cidr_ip,
|
||||
"ipv6_cidr_ip": ipv6_cidr_ip,
|
||||
"source_security_group_id": source_sg,
|
||||
"prefix_list_id": prefix_list_id,
|
||||
"policy": rule_conf.get("policy", "accept"),
|
||||
"description": rule_conf.get("description"),
|
||||
"priority": rule_conf.get("priority"),
|
||||
"nic_type": rule_conf.get("nic_type"),
|
||||
}
|
||||
args = {key: value for key, value in args.items() if value is not None}
|
||||
alicloud.ecs.SecurityGroupRule(
|
||||
f"{sg._name}-{rule_type}-{index}",
|
||||
**args,
|
||||
opts=pulumi.ResourceOptions(depends_on=[sg]),
|
||||
)
|
||||
103
iac_modules/pulumi/modules/alicloud/storage/oss.py
Normal file
103
iac_modules/pulumi/modules/alicloud/storage/oss.py
Normal file
@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Mapping, Optional
|
||||
|
||||
import pulumi
|
||||
import pulumi_alicloud as alicloud
|
||||
|
||||
from ..common.tags import merge_tags
|
||||
|
||||
|
||||
LifecycleConfig = Mapping[str, object]
|
||||
|
||||
|
||||
def create_oss_buckets(
|
||||
storage_conf: Mapping[str, object],
|
||||
default_tags: Optional[Mapping[str, str]] = None,
|
||||
) -> Dict[str, pulumi.Resource]:
|
||||
buckets_conf = storage_conf.get("oss_buckets", []) or []
|
||||
buckets: Dict[str, pulumi.Resource] = {}
|
||||
|
||||
for bucket_conf in buckets_conf:
|
||||
name = bucket_conf["name"]
|
||||
tags = merge_tags(default_tags, bucket_conf.get("tags"))
|
||||
lifecycle_rules = [_build_lifecycle_rule(rule) for rule in bucket_conf.get("lifecycle_rules", [])]
|
||||
lifecycle_rules = [rule for rule in lifecycle_rules if rule is not None]
|
||||
|
||||
bucket = alicloud.oss.Bucket(
|
||||
name,
|
||||
bucket=bucket_conf.get("bucket", name),
|
||||
storage_class=bucket_conf.get("storage_class", "Standard"),
|
||||
acl=bucket_conf.get("acl"),
|
||||
force_destroy=bucket_conf.get("force_destroy", False),
|
||||
logging=_build_logging(bucket_conf.get("logging")),
|
||||
versioning=_build_versioning(bucket_conf.get("versioning")),
|
||||
lifecycle_rules=lifecycle_rules or None,
|
||||
**({"tags": tags} if tags else {}),
|
||||
)
|
||||
buckets[name] = bucket
|
||||
|
||||
pulumi.export("oss_bucket_names", {name: bucket.bucket for name, bucket in buckets.items()})
|
||||
return buckets
|
||||
|
||||
|
||||
def _build_versioning(config: Optional[Mapping[str, object]]) -> Optional[alicloud.oss.BucketVersioningArgs]:
|
||||
if not config:
|
||||
return None
|
||||
if isinstance(config, str):
|
||||
status = config
|
||||
else:
|
||||
status = config.get("status", "Enabled")
|
||||
return alicloud.oss.BucketVersioningArgs(status=status)
|
||||
|
||||
|
||||
def _build_logging(config: Optional[Mapping[str, object]]) -> Optional[alicloud.oss.BucketLoggingArgs]:
|
||||
if not config:
|
||||
return None
|
||||
target_bucket = config.get("target_bucket")
|
||||
if not target_bucket:
|
||||
return None
|
||||
return alicloud.oss.BucketLoggingArgs(
|
||||
target_bucket=target_bucket,
|
||||
target_prefix=config.get("target_prefix"),
|
||||
)
|
||||
|
||||
|
||||
def _build_lifecycle_rule(config: LifecycleConfig) -> Optional[alicloud.oss.BucketLifecycleRuleArgs]:
|
||||
if not config:
|
||||
return None
|
||||
|
||||
transitions = [
|
||||
alicloud.oss.BucketLifecycleRuleTransitionArgs(
|
||||
storage_class=transition["storage_class"],
|
||||
days=transition.get("days"),
|
||||
created_before_date=transition.get("created_before_date"),
|
||||
is_access_time=transition.get("is_access_time"),
|
||||
return_to_std_when_visit=transition.get("return_to_standard_when_visited"),
|
||||
)
|
||||
for transition in config.get("transitions", [])
|
||||
if "storage_class" in transition
|
||||
]
|
||||
|
||||
expiration_cfg = config.get("expiration")
|
||||
if expiration_cfg is None and config.get("expiration_days"):
|
||||
expiration_cfg = {"days": config["expiration_days"]}
|
||||
|
||||
expirations: List[alicloud.oss.BucketLifecycleRuleExpirationArgs] = []
|
||||
if expiration_cfg:
|
||||
expirations.append(
|
||||
alicloud.oss.BucketLifecycleRuleExpirationArgs(
|
||||
days=expiration_cfg.get("days"),
|
||||
date=expiration_cfg.get("date"),
|
||||
created_before_date=expiration_cfg.get("created_before_date"),
|
||||
expired_object_delete_marker=expiration_cfg.get("expired_object_delete_marker"),
|
||||
)
|
||||
)
|
||||
|
||||
return alicloud.oss.BucketLifecycleRuleArgs(
|
||||
id=config.get("id"),
|
||||
enabled=config.get("enabled", True),
|
||||
prefix=config.get("prefix"),
|
||||
transitions=transitions or None,
|
||||
expirations=expirations or None,
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user