diff --git a/iac_modules/pulumi/modules/__init__.py b/iac_modules/pulumi/modules/alicloud/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/__init__.py rename to iac_modules/pulumi/modules/alicloud/__init__.py diff --git a/iac_modules/pulumi/modules/audit/__init__.py b/iac_modules/pulumi/modules/alicloud/audit/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/audit/__init__.py rename to iac_modules/pulumi/modules/alicloud/audit/__init__.py diff --git a/iac_modules/pulumi/modules/audit/actiontrail.py b/iac_modules/pulumi/modules/alicloud/audit/actiontrail.py similarity index 100% rename from iac_modules/pulumi/modules/audit/actiontrail.py rename to iac_modules/pulumi/modules/alicloud/audit/actiontrail.py diff --git a/iac_modules/pulumi/modules/common/__init__.py b/iac_modules/pulumi/modules/alicloud/common/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/common/__init__.py rename to iac_modules/pulumi/modules/alicloud/common/__init__.py diff --git a/iac_modules/pulumi/modules/common/tags.py b/iac_modules/pulumi/modules/alicloud/common/tags.py similarity index 100% rename from iac_modules/pulumi/modules/common/tags.py rename to iac_modules/pulumi/modules/alicloud/common/tags.py diff --git a/iac_modules/pulumi/modules/config_service/__init__.py b/iac_modules/pulumi/modules/alicloud/config_service/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/config_service/__init__.py rename to iac_modules/pulumi/modules/alicloud/config_service/__init__.py diff --git a/iac_modules/pulumi/modules/config_service/baseline.py b/iac_modules/pulumi/modules/alicloud/config_service/baseline.py similarity index 100% rename from iac_modules/pulumi/modules/config_service/baseline.py rename to iac_modules/pulumi/modules/alicloud/config_service/baseline.py diff --git a/iac_modules/pulumi/modules/identity/__init__.py b/iac_modules/pulumi/modules/alicloud/identity/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/identity/__init__.py rename to iac_modules/pulumi/modules/alicloud/identity/__init__.py diff --git a/iac_modules/pulumi/modules/identity/ram.py b/iac_modules/pulumi/modules/alicloud/identity/ram.py similarity index 100% rename from iac_modules/pulumi/modules/identity/ram.py rename to iac_modules/pulumi/modules/alicloud/identity/ram.py diff --git a/iac_modules/pulumi/modules/network/__init__.py b/iac_modules/pulumi/modules/alicloud/network/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/network/__init__.py rename to iac_modules/pulumi/modules/alicloud/network/__init__.py diff --git a/iac_modules/pulumi/modules/network/vpc.py b/iac_modules/pulumi/modules/alicloud/network/vpc.py similarity index 100% rename from iac_modules/pulumi/modules/network/vpc.py rename to iac_modules/pulumi/modules/alicloud/network/vpc.py diff --git a/iac_modules/pulumi/modules/security/__init__.py b/iac_modules/pulumi/modules/alicloud/security/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/security/__init__.py rename to iac_modules/pulumi/modules/alicloud/security/__init__.py diff --git a/iac_modules/pulumi/modules/security/security_groups.py b/iac_modules/pulumi/modules/alicloud/security/security_groups.py similarity index 100% rename from iac_modules/pulumi/modules/security/security_groups.py rename to iac_modules/pulumi/modules/alicloud/security/security_groups.py diff --git a/iac_modules/pulumi/modules/storage/__init__.py b/iac_modules/pulumi/modules/alicloud/storage/__init__.py similarity index 100% rename from iac_modules/pulumi/modules/storage/__init__.py rename to iac_modules/pulumi/modules/alicloud/storage/__init__.py diff --git a/iac_modules/pulumi/modules/storage/oss.py b/iac_modules/pulumi/modules/alicloud/storage/oss.py similarity index 100% rename from iac_modules/pulumi/modules/storage/oss.py rename to iac_modules/pulumi/modules/alicloud/storage/oss.py diff --git a/iac_modules/pulumi/modules/aws/__init__.py b/iac_modules/pulumi/modules/aws/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/iac_modules/pulumi/modules/aws/ec2/__init__.py b/iac_modules/pulumi/modules/aws/ec2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/iac_modules/pulumi/modules/aws/ec2/ec2_instance.py b/iac_modules/pulumi/modules/aws/ec2/ec2_instance.py new file mode 100644 index 00000000..5c9c988d --- /dev/null +++ b/iac_modules/pulumi/modules/aws/ec2/ec2_instance.py @@ -0,0 +1,97 @@ +import os +import pulumi +import pulumi_aws as aws +from .utils import resolve_ami + +def create_instances(instances_config, subnets_dict, sg_map: dict, key_name, depends_on=None): + outputs = {} + + for instance_cfg in instances_config: + name = instance_cfg["name"] + subnet_name = instance_cfg["subnet"] + subnet = subnets_dict[subnet_name] + subnet_id = subnet.id + + region = aws.config.region + ami = resolve_ami(instance_cfg["ami"], region) + instance_type = instance_cfg["type"] + disk_size = instance_cfg["disk_size_gb"] + + lifecycle = instance_cfg.get("lifecycle", "ondemand") + ttl = instance_cfg.get("ttl", "none") + env = instance_cfg.get("env", "dev") + owner = instance_cfg.get("owner", "unknown") + user_data_path = instance_cfg.get("user_data") + private_ip = instance_cfg.get("private_ip", None) + associate_public_ip = instance_cfg.get("associate_public_ip", True) + + # ✅ User data + user_data = None + if user_data_path: + expanded_path = os.path.expanduser(user_data_path) + if os.path.exists(expanded_path): + with open(expanded_path, "r") as f: + user_data = f.read() + else: + pulumi.log.warn(f"⚠️ user_data 文件不存在: {expanded_path}") + + tags = { + "Name": name, + "Lifecycle": lifecycle, + "TTL": ttl, + "Environment": env, + "Owner": owner, + } + + # ✅ Spot 实例配置 + 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" + ) + ) + + # ✅ 解析 security group ids(通过名字) + sg_names = instance_cfg.get("sg_names", []) + security_group_ids = [] + for sg_name in sg_names: + sg = sg_map.get(sg_name) + if sg: + security_group_ids.append(sg.id) + else: + pulumi.log.warn(f"⚠️ 实例 '{name}' 引用的 SG '{sg_name}' 未找到,已跳过") + + # ✅ 构建依赖项 + resource_dependencies = [subnet] + for sg in security_group_ids: + resource_dependencies.append(sg_map.get(sg_name)) + if depends_on: + resource_dependencies.extend(depends_on) + + # ✅ 创建实例 + ec2 = aws.ec2.Instance(name, + ami=ami, + instance_type=instance_type, + key_name=key_name, + subnet_id=subnet_id, + private_ip=private_ip, + associate_public_ip_address=associate_public_ip, + vpc_security_group_ids=security_group_ids, + user_data=user_data, + root_block_device={ + "volume_size": disk_size, + "volume_type": "gp2" + }, + instance_market_options=instance_market_options, + tags=tags, + opts=pulumi.ResourceOptions(depends_on=resource_dependencies) + ) + + outputs[name + "_id"] = ec2.id + outputs[name + "_public_ip"] = ec2.public_ip + outputs[name + "_private_ip"] = ec2.private_ip + + return outputs diff --git a/iac_modules/pulumi/modules/aws/ec2/utils.py b/iac_modules/pulumi/modules/aws/ec2/utils.py new file mode 100644 index 00000000..cfbbd266 --- /dev/null +++ b/iac_modules/pulumi/modules/aws/ec2/utils.py @@ -0,0 +1,43 @@ +import pulumi_aws as aws + +AMI_MAP = { + "ubuntu-22.04": ("099720109477", "*ubuntu*22.04*"), + "ubuntu-24.04": ("099720109477", "*ubuntu*24.04*"), + "rocky-8.10": ("792107900819", "Rocky-8-ec2-8.10*"), + "amazonlinux-2": ("137112412989", "amzn2-ami-hvm-*-gp2"), + "amazonlinux-2023": ("137112412989", "al2023-ami-*-x86_64"), + "debian-12": ("136693071363", "debian-12-*"), + "almalinux-9": ("151447241410", "AlmaLinux-9-*"), +} + +def query_latest_ami(owner: str, name_filter: str, architecture: str = "x86_64") -> str: + result = aws.ec2.get_ami( + most_recent=True, + owners=[owner], + filters=[ + {"name": "name", "values": [name_filter]}, + {"name": "architecture", "values": [architecture]}, + {"name": "virtualization-type", "values": ["hvm"]}, + ], + ) + return result.id + +def resolve_ami(ami_keyword: str, region: str, architecture: str = "x86_64") -> str: + if not aws.config.region: + raise ValueError("❌ AWS region is not set. Please set aws.config.region before calling resolve_ami") + + if ami_keyword.startswith("ami-"): + return ami_keyword + + keyword = ami_keyword.lower() + print(f"🔍 Resolving AMI for keyword='{keyword}' in region='{region}' with arch='{architecture}'") + + if keyword in AMI_MAP: + owner, name_filter = AMI_MAP[keyword] + try: + return query_latest_ami(owner, name_filter, architecture) + except Exception as e: + raise ValueError(f"❌ Failed to find AMI for '{keyword}' in region '{region}': {e}") + + raise ValueError(f"❌ Unsupported AMI keyword: {ami_keyword}. Supported keywords: {list(AMI_MAP.keys())}") + diff --git a/iac_modules/pulumi/modules/aws/security_group/__init__.py b/iac_modules/pulumi/modules/aws/security_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/iac_modules/pulumi/modules/aws/security_group/sg.py b/iac_modules/pulumi/modules/aws/security_group/sg.py new file mode 100644 index 00000000..1f7099b2 --- /dev/null +++ b/iac_modules/pulumi/modules/aws/security_group/sg.py @@ -0,0 +1,66 @@ +import pulumi_aws as aws +from pulumi_aws.ec2 import SecurityGroup, SecurityGroupIngressArgs, SecurityGroupEgressArgs + +def create_security_group(vpc_id: str, rule_config: dict) -> SecurityGroup: + """ + 创建 Security Group,支持 ingress/egress 配置,包括 TCP, UDP, ICMP + :param vpc_id: 目标 VPC ID + :param rule_config: 单个 firewall_rules 的字典配置 + :return: 创建的 SecurityGroup 资源对象 + """ + ingress_rules = [] + + source_ranges = rule_config.get("source_ranges", ["0.0.0.0/0"]) + egress_ranges = rule_config.get("egress_ranges", ["0.0.0.0/0"]) + + for allow_rule in rule_config.get("allow", []): + protocol = allow_rule.get("protocol", "tcp").lower() + ports = allow_rule.get("ports", []) + + # ICMP 无需端口处理 + if protocol == "icmp": + ingress_rules.append( + SecurityGroupIngressArgs( + protocol="icmp", + from_port=-1, + to_port=-1, + cidr_blocks=source_ranges + ) + ) + continue + + # 处理 TCP/UDP 等需要端口的协议 + for port in ports: + if isinstance(port, str) and port.lower() in ["*", "any", "all"]: + from_port, to_port = 0, 65535 + else: + port = int(port) + from_port = to_port = port + + ingress_rules.append( + SecurityGroupIngressArgs( + protocol=protocol, + from_port=from_port, + to_port=to_port, + cidr_blocks=source_ranges + ) + ) + + # 创建 Security Group + sg = aws.ec2.SecurityGroup( + rule_config.get("name", "default-sg"), + vpc_id=vpc_id, + description=f"Security Group: {rule_config.get('name', 'N/A')}", + ingress=ingress_rules, + egress=[ + SecurityGroupEgressArgs( + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=egress_ranges + ) + ], + tags={"Name": rule_config.get("name", "default-sg")} + ) + + return sg diff --git a/iac_modules/pulumi/modules/aws/vpc/__init__.py b/iac_modules/pulumi/modules/aws/vpc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/iac_modules/pulumi/modules/aws/vpc/vpc.py b/iac_modules/pulumi/modules/aws/vpc/vpc.py new file mode 100644 index 00000000..6b4ba324 --- /dev/null +++ b/iac_modules/pulumi/modules/aws/vpc/vpc.py @@ -0,0 +1,76 @@ +import pulumi_aws as aws +import pulumi + +def create_vpcs(vpc_list, region): + results = {} + for vpc_conf in vpc_list: + result = create_vpc(vpc_conf, region) + results[vpc_conf["name"]] = result + return results + +def create_vpc(vpc_conf, region): + vpc = aws.ec2.Vpc(vpc_conf['name'], + cidr_block=vpc_conf['cidr_block'], + enable_dns_support=True, + enable_dns_hostnames=True, + tags={"Name": vpc_conf['name']} + ) + + # 判断是否包含公有子网 + has_public = any(subnet["type"] == "public" for subnet in vpc_conf["subnets"]) + igw = aws.ec2.InternetGateway(f"{vpc_conf['name']}-igw", vpc_id=vpc.id) if has_public else None + + subnets = {} + for subnet_cfg in vpc_conf["subnets"]: + subnet = aws.ec2.Subnet(subnet_cfg["name"], + vpc_id=vpc.id, + cidr_block=subnet_cfg["cidr_block"], + map_public_ip_on_launch=subnet_cfg["type"] == "public", + availability_zone=subnet_cfg["availability_zone"], + tags={"Name": subnet_cfg["name"]} + ) + subnets[subnet_cfg["name"]] = subnet + + # 路由表创建,根据 subnet_type 分组 + route_tables = {} + + if "routes" in vpc_conf: + for route_cfg in vpc_conf["routes"]: + subnet_type = route_cfg["subnet_type"] + route_table_name = f"{vpc_conf['name']}-{subnet_type}-rt" + + # 如果还未创建该类型的路由表,则创建 + if subnet_type not in route_tables: + route_table = aws.ec2.RouteTable(route_table_name, + vpc_id=vpc.id, + routes=[], + tags={"Name": route_table_name} + ) + route_tables[subnet_type] = route_table + else: + route_table = route_tables[subnet_type] + + # 添加路由条目(追加) + aws.ec2.Route(f"{route_table_name}-{route_cfg['destination_cidr_block'].replace('/', '-')}", + route_table_id=route_table.id, + destination_cidr_block=route_cfg["destination_cidr_block"], + gateway_id=igw.id if route_cfg["gateway"] == "internet_gateway" else None + ) + + # 路由表关联到子网 + for subnet_cfg in vpc_conf["subnets"]: + subnet_type = subnet_cfg["type"] + if subnet_type in route_tables: + aws.ec2.RouteTableAssociation(f"{subnet_cfg['name']}-assoc", + subnet_id=subnets[subnet_cfg["name"]].id, + route_table_id=route_tables[subnet_type].id + ) + + # TODO: Peering 支持 + + return { + "vpc": vpc, + "subnets": subnets, + "igw": igw, + "route_tables": route_tables + }