feat: add Vultr landing zone baseline

This commit is contained in:
shenlan 2025-09-29 18:50:37 +08:00
parent 02c30229ca
commit c71a76c0d4
16 changed files with 431 additions and 0 deletions

View File

@ -0,0 +1,80 @@
name: Vultr Landing Zone Baseline
on:
push:
paths:
- 'iac_modules/pulumi/vultr/**'
- 'config/vultr/**'
- '.github/workflows/iac-pipeline-vultr-landingzone-baseline.yaml'
pull_request:
branches: [main]
workflow_dispatch:
inputs:
deploy_action:
description: "Deployment action to execute"
type: choice
options:
- init
- upgrade
- backup
- restore
- destroy
default: upgrade
deploy_dry_run:
description: "Run deployment steps in dry-run mode"
type: choice
options:
- 'true'
- 'false'
default: 'true'
env:
PULUMI_CI: 'true'
CONFIG_PATH: config/vultr
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: vultr/baseline-dev
work-dir: iac_modules/pulumi/vultr
env:
VULTR_API_KEY: ${{ secrets.VULTR_API_KEY }}
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: vultr/baseline-prod
work-dir: iac_modules/pulumi/vultr
env:
VULTR_API_KEY: ${{ secrets.VULTR_API_KEY }}
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

5
config/vultr/base.yaml Normal file
View File

@ -0,0 +1,5 @@
vultr:
region: ewr
default_tags:
environment: baseline
project: modern-container-app

16
config/vultr/compute.yaml Normal file
View File

@ -0,0 +1,16 @@
compute:
instances:
- name: baseline-bastion
plan: vc2-1c-1gb
region: ewr
os_id: 1743
hostname: baseline-bastion
label: baseline-bastion
enable_ipv6: false
backups: disabled
firewall_group: baseline-fw
vpcs:
- baseline-vpc
tags:
- bastion
- baseline

View File

@ -0,0 +1,7 @@
network:
vpcs:
- name: baseline-vpc
description: Baseline landing zone VPC
region: ewr
v4_subnet: 10.50.0.0
v4_subnet_mask: 16

View File

@ -0,0 +1,16 @@
security:
firewall_groups:
- name: baseline-fw
description: Baseline perimeter firewall rules
rules:
- name: allow-ssh
protocol: tcp
ip_type: v4
cidr: 0.0.0.0/0
port: "22"
notes: Allow SSH for operations
- name: allow-icmp
protocol: icmp
ip_type: v4
cidr: 0.0.0.0/0
notes: Allow ICMP diagnostics

View File

@ -0,0 +1,3 @@
name: vultr-landingzone-baseline
runtime: python
description: Pulumi baseline project for provisioning Vultr landing zone resources

View File

@ -0,0 +1 @@
"""Vultr landing zone baseline Pulumi modules."""

View File

@ -0,0 +1,53 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Any, Dict
import pulumi
import ediri_vultr as vultr
# 允许导入共享的工具模块
PROJECT_DIR = Path(__file__).resolve().parent
PULUMI_ROOT = PROJECT_DIR.parent
REPO_ROOT = PULUMI_ROOT.parent.parent
sys.path.append(str(PULUMI_ROOT))
from utils.config_loader import load_merged_config # noqa: E402
from vultr.modules import create_firewall_groups, create_instances, create_vpcs # noqa: E402
def main() -> None:
default_config_dir = REPO_ROOT / "config" / "vultr"
config_dir = os.environ.get("CONFIG_PATH", str(default_config_dir))
config = load_merged_config(config_dir)
vultr_conf: Dict[str, Any] = config.get("vultr", {}) # type: ignore[assignment]
region = vultr_conf.get("region")
default_tags = vultr_conf.get("default_tags", {})
if region:
vultr.config.region = region
pulumi.export("region", region)
pulumi.log.info("Loaded Vultr configuration")
network_results = create_vpcs(config.get("network", {}), region)
firewall_results = create_firewall_groups(config.get("security", {}))
instance_results = 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))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,11 @@
from __future__ import annotations
from .network.vpc import create_vpcs
from .security.firewall import create_firewall_groups
from .compute.instances import create_instances
__all__ = [
"create_vpcs",
"create_firewall_groups",
"create_instances",
]

View File

@ -0,0 +1,105 @@
from __future__ import annotations
from typing import Any, Dict, Mapping
import pulumi
import ediri_vultr as vultr
def _merge_tags(default_tags: Mapping[str, str] | None, instance_tags: Any) -> list[str] | None:
tags = []
if default_tags:
tags.extend(f"{key}:{value}" for key, value in default_tags.items())
if isinstance(instance_tags, list):
tags.extend(str(tag) for tag in instance_tags)
elif isinstance(instance_tags, Mapping):
tags.extend(f"{key}:{value}" for key, value in instance_tags.items())
return tags or None
def create_instances(
compute_config: Mapping[str, Any],
default_region: str | None,
default_tags: Mapping[str, str] | None,
firewall_groups: Mapping[str, Dict[str, object]],
vpcs: Mapping[str, vultr.Vpc],
) -> Dict[str, vultr.Instance]:
"""Provision baseline compute instances."""
results: Dict[str, vultr.Instance] = {}
instances = compute_config.get("instances", [])
if not isinstance(instances, list):
pulumi.log.warn("compute.instances 配置不是列表,将跳过实例创建")
return results
for index, inst_conf in enumerate(instances):
if not isinstance(inst_conf, Mapping):
pulumi.log.warn(f"忽略索引 {index} 的实例配置,因其不是字典结构")
continue
name = inst_conf.get("name") or f"instance-{index}"
region = inst_conf.get("region", default_region)
plan = inst_conf.get("plan")
if not (region and plan):
raise ValueError(f"实例 {name} 缺少必要的 region 或 plan 参数")
resource_name = inst_conf.get("resource_name", name.replace("_", "-"))
instance_args: Dict[str, Any] = {
"region": region,
"plan": plan,
}
optional_fields = [
"os_id",
"image_id",
"hostname",
"label",
"enable_ipv6",
"backups",
"user_data",
"activation_email",
"ddos_protection",
"disable_public_ipv4",
"reserved_ip_id",
"script_id",
"snapshot_id",
"user_scheme",
]
for field in optional_fields:
if field in inst_conf:
instance_args[field] = inst_conf[field]
if ssh_keys := inst_conf.get("ssh_key_ids"):
instance_args["ssh_key_ids"] = ssh_keys
tags = _merge_tags(default_tags, inst_conf.get("tags"))
if tags:
instance_args["tags"] = tags
firewall_group_name = inst_conf.get("firewall_group")
if firewall_group_name:
group = firewall_groups.get(firewall_group_name)
if not group:
raise KeyError(
f"实例 {name} 引用了未定义的防火墙组 '{firewall_group_name}'"
)
instance_args["firewall_group_id"] = group["group"].id
vpc_names = inst_conf.get("vpcs", [])
if vpc_names:
resolved_vpc_ids = []
for vpc_name in vpc_names:
vpc_resource = vpcs.get(vpc_name)
if not vpc_resource:
raise KeyError(
f"实例 {name} 引用了未定义的 VPC '{vpc_name}'"
)
resolved_vpc_ids.append(vpc_resource.id)
instance_args["vpc_ids"] = resolved_vpc_ids
instance = vultr.Instance(resource_name, **instance_args)
results[name] = instance
pulumi.export(f"instance::{name}::id", instance.id)
return results

View File

@ -0,0 +1,51 @@
from __future__ import annotations
from typing import Any, Dict, Mapping
import pulumi
import ediri_vultr as vultr
def create_vpcs(
network_config: Mapping[str, Any],
default_region: str | None,
) -> Dict[str, vultr.Vpc]:
"""Create Vultr VPC networks from configuration."""
vpc_results: Dict[str, vultr.Vpc] = {}
vpcs = network_config.get("vpcs", [])
if not isinstance(vpcs, list):
pulumi.log.warn("network.vpcs 配置不是列表,将跳过 VPC 创建")
return vpc_results
for index, vpc_conf in enumerate(vpcs):
if not isinstance(vpc_conf, Mapping):
pulumi.log.warn(f"忽略索引 {index} 的 VPC 配置,因其不是字典结构")
continue
name = vpc_conf.get("name") or f"vpc-{index}"
region = vpc_conf.get("region", default_region)
if not region:
raise ValueError(f"VPC '{name}' 缺少 region 参数且未设置默认 region")
v4_subnet = vpc_conf.get("v4_subnet")
v4_subnet_mask = vpc_conf.get("v4_subnet_mask")
if not (v4_subnet and v4_subnet_mask is not None):
raise ValueError(
f"VPC '{name}' 需要提供 v4_subnet 与 v4_subnet_mask 用于定义网络范围"
)
description = vpc_conf.get("description", f"VPC network for {name}")
resource_name = vpc_conf.get("resource_name", name.replace("_", "-"))
vpc = vultr.Vpc(
resource_name,
region=region,
description=description,
v4_subnet=v4_subnet,
v4_subnet_mask=int(v4_subnet_mask),
)
vpc_results[name] = vpc
pulumi.export(f"vpc::{name}::id", vpc.id)
return vpc_results

View File

@ -0,0 +1,82 @@
from __future__ import annotations
import ipaddress
from typing import Any, Dict, List, Mapping
import pulumi
import ediri_vultr as vultr
def create_firewall_groups(
security_config: Mapping[str, Any],
) -> Dict[str, Dict[str, object]]:
"""Create Vultr firewall groups and rules."""
results: Dict[str, Dict[str, object]] = {}
groups = security_config.get("firewall_groups", [])
if not isinstance(groups, list):
pulumi.log.warn("security.firewall_groups 配置不是列表,将跳过防火墙创建")
return results
for index, group_conf in enumerate(groups):
if not isinstance(group_conf, Mapping):
pulumi.log.warn(f"忽略索引 {index} 的防火墙配置,因其不是字典结构")
continue
name = group_conf.get("name") or f"firewall-{index}"
description = group_conf.get("description", f"Baseline firewall group {name}")
resource_name = group_conf.get("resource_name", name.replace("_", "-"))
firewall_group = vultr.FirewallGroup(resource_name, description=description)
rules_conf = group_conf.get("rules", [])
if not isinstance(rules_conf, list):
pulumi.log.warn(f"防火墙 {name} 的 rules 配置不是列表,将跳过规则创建")
rules_conf = []
rules: List[vultr.FirewallRule] = []
for rule_index, rule_conf in enumerate(rules_conf):
if not isinstance(rule_conf, Mapping):
pulumi.log.warn(
f"忽略防火墙 {name} 中索引 {rule_index} 的规则配置,因其不是字典结构"
)
continue
cidr = rule_conf.get("cidr")
ip_type = rule_conf.get("ip_type", "v4")
if not cidr:
pulumi.log.warn(
f"防火墙 {name} 规则 {rule_conf.get('name', rule_index)} 缺少 cidr将跳过"
)
continue
network = ipaddress.ip_network(cidr, strict=False)
if (ip_type == "v6" and network.version != 6) or (
ip_type == "v4" and network.version != 4
):
pulumi.log.warn(
f"防火墙 {name} 规则 {rule_conf.get('name', rule_index)} 的 cidr 与 ip_type 不匹配"
)
continue
rule_name = rule_conf.get("name") or f"rule-{rule_index}"
rule_resource_name = f"{resource_name}-{rule_name}".replace("_", "-")
rule = vultr.FirewallRule(
rule_resource_name,
firewall_group_id=firewall_group.id,
protocol=rule_conf.get("protocol", "tcp"),
ip_type=ip_type,
subnet=str(network.network_address),
subnet_size=network.prefixlen,
port=rule_conf.get("port"),
notes=rule_conf.get("notes"),
source=rule_conf.get("source"),
)
rules.append(rule)
results[name] = {"group": firewall_group, "rules": rules}
pulumi.export(f"firewall::{name}::id", firewall_group.id)
pulumi.export(f"firewall::{name}::rule_count", len(rules))
return results

View File

@ -4,5 +4,6 @@ pulumi-aws
pulumi-gcp pulumi-gcp
pulumi-azure-native pulumi-azure-native
pulumi-alicloud pulumi-alicloud
ediri_vultr
PyYAML PyYAML
jinja2 jinja2