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-gcp
|
||||||
pulumi-azure-native
|
pulumi-azure-native
|
||||||
pulumi-alicloud
|
pulumi-alicloud
|
||||||
|
ediri_vultr
|
||||||
PyYAML
|
PyYAML
|
||||||
jinja2
|
jinja2
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user