feat: add Vultr landing zone baseline
This commit is contained in:
parent
02c30229ca
commit
c71a76c0d4
80
.github/workflows/iac-pipeline-vultr-landingzone-baseline.yaml
vendored
Normal file
80
.github/workflows/iac-pipeline-vultr-landingzone-baseline.yaml
vendored
Normal 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
5
config/vultr/base.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
vultr:
|
||||
region: ewr
|
||||
default_tags:
|
||||
environment: baseline
|
||||
project: modern-container-app
|
||||
16
config/vultr/compute.yaml
Normal file
16
config/vultr/compute.yaml
Normal 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
|
||||
7
config/vultr/network.yaml
Normal file
7
config/vultr/network.yaml
Normal 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
|
||||
16
config/vultr/security.yaml
Normal file
16
config/vultr/security.yaml
Normal 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
|
||||
3
iac_modules/pulumi/vultr/Pulumi.yaml
Normal file
3
iac_modules/pulumi/vultr/Pulumi.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
name: vultr-landingzone-baseline
|
||||
runtime: python
|
||||
description: Pulumi baseline project for provisioning Vultr landing zone resources
|
||||
1
iac_modules/pulumi/vultr/__init__.py
Normal file
1
iac_modules/pulumi/vultr/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Vultr landing zone baseline Pulumi modules."""
|
||||
53
iac_modules/pulumi/vultr/__main__.py
Normal file
53
iac_modules/pulumi/vultr/__main__.py
Normal 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()
|
||||
11
iac_modules/pulumi/vultr/modules/__init__.py
Normal file
11
iac_modules/pulumi/vultr/modules/__init__.py
Normal 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",
|
||||
]
|
||||
105
iac_modules/pulumi/vultr/modules/compute/instances.py
Normal file
105
iac_modules/pulumi/vultr/modules/compute/instances.py
Normal 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
|
||||
51
iac_modules/pulumi/vultr/modules/network/vpc.py
Normal file
51
iac_modules/pulumi/vultr/modules/network/vpc.py
Normal 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
|
||||
82
iac_modules/pulumi/vultr/modules/security/firewall.py
Normal file
82
iac_modules/pulumi/vultr/modules/security/firewall.py
Normal 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
|
||||
@ -4,5 +4,6 @@ pulumi-aws
|
||||
pulumi-gcp
|
||||
pulumi-azure-native
|
||||
pulumi-alicloud
|
||||
ediri_vultr
|
||||
PyYAML
|
||||
jinja2
|
||||
|
||||
Loading…
Reference in New Issue
Block a user