feat(iac): modularize EC2 env + add dynamic AMI lookup

- new envs/dev-ec2 environment
- add ami_lookup module (Ubuntu/Rocky/AmazonLinux auto-resolve)
- add keypair, sg, ec2 modules
- remove VPC remote_state dependency
- fix SG duplicate rules
- unify module variables/outputs
This commit is contained in:
Haitao Pan 2025-11-17 13:05:36 +08:00
parent a75754a2ee
commit 7c57c839ef
21 changed files with 459 additions and 0 deletions

View File

@ -0,0 +1,23 @@
name_prefix: "dev-ec2"
vpc_id: "vpc-0d0d8d822fa215104"
subnet_id: "subnet-0c370f7ff7311388e"
instance:
type: "t3.micro"
ami: "ubuntu-2204"
keypair:
name: "dev-key"
public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEsuS135lzjVvlH2iNrKz23lDFr7b686xs4d2HINP2glFPmgkgx1D6Dqwisb1UbhWHZmUUzRxXeNlE8fiaO0TXN/C0dsdUxgopnQRyakcA+gfJqqb38Syx8eqdC7mQy9ygOf763dWm6d/SYZ8WgNWLldk4QF9DiZOW9K22DMtY4/1Cqe/YE/WGpOMVr9T9BwvmOjarjWp2OPbx6RVlSOd735Mze5X+cJ9QqdLaisCiSoJ3j9S6dulcxm+7ghPfATvxlJyZWSrRrVqnmV45lPbeuUHlIEyuK1PK2MS6NtUP03ZhdRYJQKZLECpR5xAO/BliOtDdRornvHV1gutYD8/n3IS8sRVzYPvN9DuOhzBnBQUgciu2++R8zMfdVoH7mSbsE8u++vMcBk3UJ1Op0Ct+trl2bsnue96cAnoiII08JKwAaczD5uZIGhdkGV8zKnChNCjzCxP0i4PV/MYW04eWmH+E8G81zq4ZsvrvPYmilBbRrkwHvvbPba3SSb2F2As= shenlan@shenlandeMacBook-Air-2.local"
security_group:
name: "dev-ec2-sg"
ssh_cidr: "0.0.0.0/0"
additional_ingress:
- port: 80
protocol: tcp
cidr: "0.0.0.0/0"
- port: 443
protocol: tcp
cidr: "0.0.0.0/0"

View File

@ -0,0 +1,25 @@
# Local terraform files
.terraform/
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup
# Auto tfvars generated by CI/CD or sensitive data
*.tfvars
*.auto.tfvars
*.tfvars.json
# IDE / editor files
.idea/
.vscode/
*.swp
# AWS credentials — never commit
.aws/
credentials
config
# OS-specific
.DS_Store
Thumbs.db

View File

@ -0,0 +1,17 @@
# envs/dev-ec2/Makefile
init:
terraform init --upgrade
plan: init
terraform plan
apply: init
terraform apply -auto-approve
output:
terraform output
destroy: init
terraform destroy -auto-approve

View File

@ -0,0 +1,9 @@
terraform {
backend "s3" {
bucket = "svc-plus-iac-state"
key = "account/dev/iam/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "svc-plus-iac-state-dynamodb-lock"
}
}

View File

@ -0,0 +1,41 @@
locals {
account = yamldecode(file("${path.root}/../../config/accounts/dev.yaml"))
ec2_conf = yamldecode(file("${path.root}/../../config/resources/ec2/dev.yaml"))
}
module "ami_lookup" {
source = "../../modules/ami_lookup"
name = local.ec2_conf.instance.ami
region = local.account.region
}
module "keypair" {
source = "../../modules/keypair"
name = local.ec2_conf.keypair.name
public_key = local.ec2_conf.keypair.public_key
tags = local.account.tags
}
module "sg" {
source = "../../modules/sg"
name = local.ec2_conf.security_group.name
vpc_id = local.ec2_conf.vpc_id # <<<<<< YAML
ssh_cidr = local.ec2_conf.security_group.ssh_cidr
additional_ingress = local.ec2_conf.security_group.additional_ingress
tags = local.account.tags
}
module "ec2" {
source = "../../modules/ec2"
name_prefix = local.ec2_conf.name_prefix
instance = {
type = local.ec2_conf.instance.type
ami = module.ami_lookup.id # <<<<<< AMI
}
subnet_id = local.ec2_conf.subnet_id # <<<<<< YAML
sg_id = module.sg.sg_id
keypair_name = module.keypair.keypair_name
tags = local.account.tags
}

View File

@ -0,0 +1,24 @@
output "instance_id" {
description = "EC2 instance ID"
value = module.ec2.instance_id
}
output "public_ip" {
description = "Public IP address of the EC2 instance"
value = module.ec2.public_ip
}
output "private_ip" {
description = "Private IP address of the EC2 instance"
value = module.ec2.private_ip
}
output "keypair_name" {
description = "KeyPair name used for the instance"
value = module.keypair.keypair_name
}
output "security_group_id" {
description = "Security Group ID attached to the EC2 instance"
value = module.sg.sg_id
}

View File

@ -0,0 +1,20 @@
terraform {
required_version = ">= 1.2"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.92.0"
}
}
}
provider "aws" {
region = local.account.region
assume_role {
role_arn = "arn:aws:iam::730335654753:role/TerraformDeployRole-Dev"
session_name = "TerraformDevSession"
}
}

View File

@ -0,0 +1,7 @@
创建:
✔ VPC
✔ 2 Public Subnets
✔ 2 Private Subnets
✔ Internet Gateway
✔ NAT Gateway
✔ Public Route Table + Private Route Table

View File

@ -0,0 +1,60 @@
locals {
name = var.name
# OS type flags
is_ubuntu_2204 = local.name == "ubuntu_2204"
is_ubuntu_2404 = local.name == "ubuntu_2404"
is_rocky_8 = local.name == "rocky_8"
is_rocky_9 = local.name == "rocky_9"
is_rocky_10 = local.name == "rocky_10"
is_amzn2 = local.name == "amazonlinux_2"
# Filters OS pattern
ami_filters = (
local.is_ubuntu_2204 ? [
"ubuntu/images/hvm-ssd-gp3/ubuntu-jammy-22.04-amd64-server-*"
] :
local.is_ubuntu_2404 ? [
"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"
] :
local.is_rocky_8 ? [
"Rocky-8-*-x86_64-*"
] :
local.is_rocky_9 ? [
"Rocky-9-*-x86_64-*"
] :
local.is_rocky_10 ? [
"Rocky-10-*-x86_64-*"
] :
local.is_amzn2 ? [
"amzn2-ami-hvm-*-x86_64-gp2"
] :
["*"]
)
# AMI Owner IDs
ami_owners = (
(local.is_rocky_8 || local.is_rocky_9 || local.is_rocky_10) ? ["679593333241"] :
(local.is_ubuntu_2204 || local.is_ubuntu_2404) ? ["099720109477"] :
local.is_amzn2 ? ["137112412989"] :
["amazon"]
)
}
data "aws_ami" "selected" {
most_recent = true
owners = local.ami_owners
dynamic "filter" {
for_each = local.ami_filters
content {
name = "name"
values = [filter.value]
}
}
}
output "ami_id" {
value = data.aws_ami.selected.id
description = "Resolved AMI ID"
}

View File

@ -0,0 +1,9 @@
output "id" {
description = "Resolved AMI ID"
value = data.aws_ami.selected.id
}
output "name" {
description = "Resolved AMI name"
value = data.aws_ami.selected.name
}

View File

@ -0,0 +1,10 @@
variable "name" {
description = "Short AMI name, e.g. ubuntu-2204 | ubuntu-2404 | rocky-8 | amazonlinux-2"
type = string
}
variable "region" {
description = "AWS region"
type = string
}

View File

@ -0,0 +1,15 @@
resource "aws_instance" "this" {
ami = var.instance.ami
instance_type = var.instance.type
# env
subnet_id = var.subnet_id
vpc_security_group_ids = [var.sg_id]
key_name = var.keypair_name
tags = merge(var.tags, {
Name = "${var.name_prefix}-instance"
})
}

View File

@ -0,0 +1,24 @@
output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.this.id
}
output "instance_arn" {
description = "EC2 instance ARN"
value = aws_instance.this.arn
}
output "public_ip" {
description = "Public IPv4 address"
value = aws_instance.this.public_ip
}
output "private_ip" {
description = "Private IPv4 address"
value = aws_instance.this.private_ip
}
output "subnet_id" {
description = "Instance subnet ID"
value = aws_instance.this.subnet_id
}

View File

@ -0,0 +1,32 @@
variable "name_prefix" {
type = string
description = "Prefix for EC2 Name tag"
}
variable "instance" {
type = object({
type = string
ami = string
})
description = "Instance config"
}
variable "subnet_id" {
type = string
description = "Subnet ID where EC2 instance will be launched"
}
variable "sg_id" {
type = string
description = "Security Group ID"
}
variable "keypair_name" {
type = string
description = "KeyPair name"
}
variable "tags" {
type = map(string)
description = "Common tags"
}

View File

@ -0,0 +1,9 @@
resource "aws_key_pair" "this" {
key_name = var.name
public_key = var.public_key
tags = merge(var.tags, {
Name = var.name
})
}

View File

@ -0,0 +1,10 @@
output "keypair_name" {
description = "The name of the AWS KeyPair"
value = aws_key_pair.this.key_name
}
output "fingerprint" {
description = "KeyPair fingerprint"
value = aws_key_pair.this.fingerprint
}

View File

@ -0,0 +1,14 @@
variable "name" {
description = "Name of the KeyPair"
type = string
}
variable "public_key" {
description = "Public key material for AWS KeyPair"
type = string
}
variable "tags" {
description = "Common tags"
type = map(string)
}

View File

@ -0,0 +1,25 @@
# Local terraform files
.terraform/
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup
# Auto tfvars generated by CI/CD or sensitive data
*.tfvars
*.auto.tfvars
*.tfvars.json
# IDE / editor files
.idea/
.vscode/
*.swp
# AWS credentials — never commit
.aws/
credentials
config
# OS-specific
.DS_Store
Thumbs.db

View File

@ -0,0 +1,42 @@
resource "aws_security_group" "this" {
name = var.name
vpc_id = var.vpc_id
tags = merge(var.tags, {
Name = var.name
})
}
# Merge SSH rule + additional_ingress
locals {
ingress_rules = concat(
length(var.ssh_cidr) > 0 ? [
{
port = 22
protocol = "tcp"
cidr = var.ssh_cidr
}
] : [],
var.additional_ingress
)
}
resource "aws_security_group_rule" "ingress" {
for_each = { for idx, rule in local.ingress_rules : idx => rule }
type = "ingress"
from_port = each.value.port
to_port = each.value.port
protocol = each.value.protocol
cidr_blocks = [each.value.cidr]
security_group_id = aws_security_group.this.id
}
resource "aws_security_group_rule" "egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.this.id
}

View File

@ -0,0 +1,14 @@
output "sg_id" {
description = "Security Group ID"
value = aws_security_group.this.id
}
output "sg_name" {
description = "Security Group Name"
value = aws_security_group.this.name
}
output "sg_arn" {
description = "ARN of the Security Group"
value = aws_security_group.this.arn
}

View File

@ -0,0 +1,29 @@
variable "name" {
description = "Security Group name"
type = string
}
variable "vpc_id" {
description = "VPC ID for the Security Group"
type = string
}
variable "ssh_cidr" {
description = "CIDR allowed to SSH"
type = string
}
variable "additional_ingress" {
description = "Additional ingress rules"
type = list(object({
port = number
protocol = string
cidr = string
}))
}
variable "tags" {
description = "Common tags"
type = map(string)
}