A production-grade, modular Terraform project that deploys a highly available web application stack on AWS using Terraform v1.5+ and the AWS Provider v5.x.
Everything is built from small, reusable child modules under modules/ and orchestrated by a thin root module. The stack provisions a custom VPC with public/private subnets across two availability zones, two EC2 web servers (SSH + AWS Systems Manager Session Manager management), an internet-facing Application Load Balancer, and a hardened S3 bucket + DynamoDB table ready to be adopted as the remote state backend.
Built to demonstrate modular Terraform, AWS networking, ALB, IAM, S3, Systems Manager, remote-state preparation, reusable modules, and Infrastructure as Code best practices.
- Architecture
- Repository Layout
- What Gets Deployed
- Prerequisites
- Required IAM Permissions
- Getting Started
- Managing the Instances with Session Manager
- Migrating to the S3 Remote Backend
- How the Modules Interact
- Module Reference
- Customization
- Known Limitations & Next Steps
flowchart TB
subgraph Internet["Internet"]
USER["Clients / Users"]
end
subgraph VPC["VPC 10.0.0.0/16"]
subgraph AZA["Availability Zone us-east-1a"]
PUB_A["Public Subnet<br/>10.0.3.0/24"]
PRI_A["Private Subnet<br/>10.0.1.0/24 (unused)"]
end
subgraph AZB["Availability Zone us-east-1b"]
PUB_B["Public Subnet<br/>10.0.4.0/24"]
PRI_B["Private Subnet<br/>10.0.2.0/24 (unused)"]
end
subgraph ASG["Auto Scaling Group tuxtechlab-dev-web<br/>min 2 / desired 3 / max 5"]
EC2_A["EC2 Web Server<br/>(nginx + SSH + SSM agent)"]
EC2_B["EC2 Web Server<br/>(nginx + SSH + SSM agent)"]
EC2_C["EC2 Web Server<br/>(nginx + SSH + SSM agent)"]
end
IGW["Internet Gateway"]
PUB_RT["Public Route Table<br/>0.0.0.0/0 -> IGW"]
ALB["Application Load Balancer"]
ALB_SG["ALB Security Group<br/>HTTP/HTTPS 0.0.0.0/0"]
EC2_SG["EC2 Security Group<br/>HTTP from ALB SG only"]
SSH_SG["SSH/Management SG<br/>Port 22 from approved CIDRs"]
TG["Target Group :80<br/>Health check GET /"]
ALB --- PUB_A
ALB --- PUB_B
IGW --- PUB_RT
PUB_A --- PUB_RT
PUB_B --- PUB_RT
EC2_A --- PUB_A
EC2_B --- PUB_B
ALB_SG --> ALB
EC2_SG --> ASG
SSH_SG --> ASG
ALB --> TG
TG --> ASG
ASG -. SSM Session Manager .-> IAM["IAM Role<br/>AmazonSSMManagedInstanceCore"]
end
USER -->|"HTTP / HTTPS"| ALB_SG
SSH_USER["Operator"] -->|"SSH :22"| SSH_SG
IGW -->|"internet route"| ALB
IAM --> AWS_SSM["AWS Systems Manager<br/>Session Manager"]
S3["S3 Bucket<br/>versioned + encrypted"] -. "future Terraform backend" .-> AWS_S3
DDB["DynamoDB<br/>state lock table"] -. "future backend locking" .-> AWS_S3
classDef public fill:#bae6fd,stroke:#0284c7,color:#082f49;
classDef private fill:#dcfce7,stroke:#16a34a,color:#052e16;
classDef service fill:#fef3c7,stroke:#d97706,color:#451a03;
class PUB_A,PUB_B,ALB,IGW,PUB_RT,ASG public;
class PRI_A,PRI_B,EC2_SG,SSH_SG private;
class S3,DDB,IAM,AWS_SSM service;
Data flow
- Clients reach the ALB on port 80/443 through the ALB security group.
- The ALB lives in the public subnets and forwards port 80 to the target group.
- The target group health-checks and distributes requests to the Auto Scaling Group (desired 3, min 2 / max 5), whose instances live in the public subnets. Instances that fail the ALB health check are terminated and replaced automatically.
- The EC2 security group only allows HTTP from the ALB security group, so web traffic can only reach the instances through the load balancer.
- Operators can SSH into the instances (public IP + key pair + SSH SG on port 22) and manage them via Session Manager (SSM agent + IAM instance role).
- The hardened S3 bucket (versioning + SSE + lifecycle + public-access block) and DynamoDB lock table are pre-provisioned for the future remote state backend.
terraform-aws-ha/
├── backend.tf # Commented-out S3 + DynamoDB remote backend config
├── provider.tf # AWS provider, region + default_tags
├── versions.tf # Terraform v1.5+ and AWS provider ~>5.0 constraints
├── variables.tf # All root inputs with defaults + validations
├── terraform.tfvars.example# Copy me to terraform.tfvars
├── locals.tf # Common tag set + derived locals
├── main.tf # Root orchestration wiring all modules together
├── outputs.tf # Root outputs (ALB DNS name, IDs, ARNs, ...)
├── userdata/
│ └── web.sh # nginx bootstrap script (IMDSv2 metadata page)
├── modules/ # Reusable child modules (each: main/variables/outputs)
│ ├── vpc/ # aws_vpc
│ ├── subnet/ # aws_subnet (for_each over a subnet map)
│ ├── internet_gateway/ # aws_internet_gateway
│ ├── route_table/ # aws_route_table + routes + associations
│ ├── security_group/ # aws_security_group + rules
│ ├── iam/ # role + policy attachments + instance profile
│ ├── autoscaling/ # launch template + ASG (ELB health checks)
│ ├── alb/ # ALB + target group + listener
│ └── s3/ # hardened bucket + optional DynamoDB lock table
└── README.md
Every module contains main.tf, variables.tf, and outputs.tf with descriptive comments and validation.
| Resource | Details |
|---|---|
| VPC | 10.0.0.0/16, DNS hostnames + DNS support enabled |
| Public subnets | 10.0.3.0/24 (us-east-1a), 10.0.4.0/24 (us-east-1b) |
| Private subnets | 10.0.1.0/24 (us-east-1a), 10.0.2.0/24 (us-east-1b) |
| Internet Gateway | Attached to the VPC |
| Public route table | 0.0.0.0/0 -> IGW, associated with only the public subnets |
| Private subnets | Isolated (no internet route, no NAT — placeholder for future NAT) |
| Security groups | ALB (80/443 from anywhere), EC2 (80 from ALB SG only), SSH/management (22 from configurable CIDRs, attached to the instances) |
| Auto Scaling Group | min 2 / desired 3 / max 5 t3.micro instances across the public subnets (one per AZ). Launch template with public IP, SSH key pair, IMDSv2, and encrypted gp3 root volume. ELB health checks (GET /) drive automatic replacement of unhealthy instances. Default AMI is Amazon Linux 2023 (via SSM lookup); this repo ships pinned as Ubuntu 26.04 (ami-0b6d9d3d33ba97d99) |
| IAM | Role + instance profile with AmazonSSMManagedInstanceCore |
| Load balancer | Internet-facing ALB, target group :80, HTTP listener. Instances are registered by the Auto Scaling Group (no static attachments) |
| S3 bucket | Versioning + SSE (KMS) + public access block + lifecycle rules |
| DynamoDB | Optional LockID table for Terraform state locking |
- Terraform CLI >= 1.5.0 (install)
- AWS credentials configured via the standard chain: environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY), a profile (~/.aws/credentials), or an identity provider / role (~/.aws/config). Example:export AWS_PROFILE=my-profile - The IAM principal must have the permissions listed below.
- Optional:
awsCLI for inspecting/destroying resources.
The operator principal needs enough permission to create every resource. The minimum scopes (for the deployed account/region) are:
ec2:CreateVpc, CreateSubnet, CreateInternetGateway, AttachInternetGateway,
ec2:CreateRouteTable, CreateRoute, AssociateRouteTable, CreateSecurityGroup,
ec2:AuthorizeSecurityGroupIngress, AuthorizeSecurityGroupEgress, RunInstances,
ec2:Describe* , ec2:CreateTags, ec2:Delete*, ec2:ReleaseAddress
elasticloadbalancing:CreateLoadBalancer, CreateTargetGroup, CreateListener,
elasticloadbalancing:RegisterTargets, elasticloadbalancing:Describe*, Delete*
iam:CreateRole, CreateInstanceProfile, AddRoleToInstanceProfile,
iam:AttachRolePolicy, iam:GetRole, iam:GetPolicy, iam:PassRole, iam:Delete*
s3:CreateBucket, PutBucketVersioning, PutBucketEncryption,
s3:PutBucketPublicAccessBlock, PutBucketLifecycleConfiguration, s3:Get*, s3:Delete*
dynamodb:CreateTable, DescribeTable, DeleteTable
ssm:GetParameter # resolves the latest Amazon Linux 2023 AMI
The quickest path for a demo is the
AdministratorAccessmanaged policy on the executing role. For a locked-down setup, attach only the scopes above (all scoped to the account/region, withelasticloadbalancingands3:*being the widest).
cd terraform-aws-ha
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars:
- Set a globally unique
bucket_name(S3 bucket names are global). - Restrict
ssh_cidr_blocksto your office/VPN CIDR, or setssh_cidr_blocks = []to disable SSH entirely. - Optionally set
key_nameif you want SSH key-based access.
terraform initThis downloads the AWS provider and installs the local modules. The default backend is local state (terraform.tfstate), so nothing else is required to run.
terraform planReview the proposed changes (expect ~36 resources to add). The AMI comes from ami_id in terraform.tfvars (pinned Ubuntu 26.04); if it is left commented, the latest Amazon Linux 2023 AMI is resolved at plan time from the SSM parameter /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64. The bootstrap script userdata/web.sh auto-detects the OS family so nginx installs correctly on Ubuntu (apt) or Amazon Linux (dnf/yum).
terraform apply -auto-approveOr use the interactive flow:
terraform applyWhen the apply finishes, Terraform prints the outputs, including:
alb_dns_name = "tuxtechlab-dev-alb-1234567890.us-east-1.elb.amazonaws.com"
instance_ids = { a = "i-...", b = "i-..." }
s3_bucket_name = "tuxtechlab-dev-terraform-state"
dynamodb_lock_table_name = "tuxtechlab-dev-terraform-state-lock"Open the ALB DNS name in a browser — each request shows a status page reporting the instance ID, availability zone, and IPs served by the responding web server. Refresh a few times to see traffic alternate between instance a and b.
You can also query the API:
curl http://$(terraform output -raw alb_dns_name)terraform destroyThe S3 bucket has
force_destroy = falseby default. Setforce_destroy_bucket = trueinterraform.tfvarsonly if you want Terraform to delete the bucket (and any objects) on destroy. This is deliberately opt-in so a real state bucket can never be silently wiped.
Two ways to reach the web servers:
1. SSH over the internet (the instances are in public subnets with public IPs):
# Get the public IPs from the terraform output (a list — instances churn as the ASG replaces them)
terraform output -json instance_public_ips | jq -r '.[0]'
ssh -i ~/.ssh/ec2-login2.pem ubuntu@$(terraform output -json instance_public_ips | jq -r '.[0]')The SSH/management security group allows port 22 from ssh_cidr_blocks (restrict this from 0.0.0.0/0!). The login user for Ubuntu AMIs is ubuntu; for Amazon Linux it is ec2-user.
2. Session Manager (works even without a key pair — the instances carry the AmazonSSMManagedInstanceCore policy via their instance profile):
# List SSM-managed instances
aws ssm describe-instance-information --region us-east-1
# Start a session with one of the instances (from the terraform output)
aws ssm start-session --target i-0xxxx --region us-east-1Or click Session Manager in the EC2 console. This works because the SSM agent on the instance reaches the SSM control plane over HTTPS, and the instance profile authorizes it.
The s3_backend module already created a versioned, encrypted, locked-down bucket plus a DynamoDB lock table. To move state there:
-
Apply once with local state (as above) so the bucket and table exist.
-
Edit
backend.tfand uncomment theterraform.backend "s3"block, replacing the placeholders with the values from your outputs:terraform { backend "s3" { bucket = "tuxtechlab-dev-terraform-state" # s3_bucket_name key = "envs/dev/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "tuxtechlab-dev-terraform-state-lock" # dynamodb_lock_table_name } }
-
Reconfigure and let Terraform copy the existing state into S3:
terraform init -reconfigure terraform apply
Terraform detects the existing local state and prompts to copy it into the bucket. From now on all state is shared and locked by DynamoDB, so concurrent
plan/applyruns are safe for a team.
Keep the backend block commented out in the repo so the project still runs locally out of the box.
The root module (main.tf) is the only place that instantiates modules; modules never call each other. Dependencies are expressed purely through references to module outputs, so Terraform builds the correct graph with no manual depends_on:
module.vpc
├─ module.public_subnets (vpc_id)
│ └─ module.public_route_table (subnet_ids)
│ └─ module.internet_gateway (vpc_id) # route targets igw.id
├─ module.private_subnets (vpc_id)
├─ module.alb_security_group (vpc_id)
│ └─ module.ec2_security_group (source_sg = alb_sg.id) # SG-to-SG reference
├─ module.ssh_security_group (vpc_id) # count-based (optional)
├─ module.iam (independent)
│ └─ module.autoscaling (public subnet ids + sg ids + profile name + target group arn)
│ └─ module.alb (public subnet ids + alb sg id) # ASG registers instances to the TG
└─ module.s3_backend (independent)
Key patterns used throughout:
for_eacheverywhere it helps — subnets, route table associations, SG rules, policy attachments, target group attachments, lifecycle rules.- No hardcoded AMI IDs — resolved from an SSM parameter data source (overridable via
ami_id). - SG references over CIDR — the EC2 group allows HTTP only from the ALB group (
source_security_group_id), never0.0.0.0/0. - Consistent tagging — the root provider sets
default_tagsfromlocal.tags; modules merge their ownNametags on top. - Everything configurable — region, project, environment, CIDRs, AZs, instance type, key name, bucket name, SSH CIDRs, user data path, and extra tags are all variables (see
terraform.tfvars.example).
Creates the VPC. DNS support and hostnames are enabled.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Name tag |
cidr_block |
string |
— | VPC CIDR |
instance_tenancy |
string |
"default" |
Tenancy |
enable_dns_support |
bool |
true |
Enable DNS resolution |
enable_dns_hostnames |
bool |
true |
Enable DNS hostnames |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
id |
VPC ID |
arn |
VPC ARN |
cidr_block |
VPC CIDR |
main_route_table_id |
AWS default route table |
default_security_group_id |
AWS default SG |
enable_dns_support / enable_dns_hostnames |
Effective DNS flags |
Creates N subnets via for_each over a subnet map.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
vpc_id |
string |
— | Target VPC |
name_prefix |
string |
— | Prefix for Name tags |
subnets |
map(object{cidr_block, availability_zone, map_public_ip_on_launch}) |
— | Keyed by short name |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
ids |
map name → subnet ID |
ids_list |
List of subnet IDs |
cidr_blocks |
map name → CIDR |
availability_zones |
map name → AZ |
map_public_ip_on_launch |
map name → auto-assign public IP flag |
Attaches an IGW to the VPC.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Name tag |
vpc_id |
string |
— | Target VPC |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
id |
IGW ID |
arn |
IGW ARN |
owner_id |
AWS account owner |
Creates a route table, optional routes, and subnet associations.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Name tag |
vpc_id |
string |
— | Target VPC |
subnet_ids |
list(string) |
[] |
Subnets to associate |
routes |
list(object{...}) |
[] |
Routes with optional targets (gateway_id, nat_gateway_id, transit_gateway_id, vpc_peering_connection_id, network_interface_id) |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
id |
Route table ID |
arn |
Route table ARN |
association_ids |
Subnet association IDs |
route_ids |
Route IDs |
Creates a SG plus arbitrary ingress/egress rules. Rules support cidr_blocks, ipv6_cidr_blocks, source_security_group_id, and self.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | SG name |
description |
string |
— | Console description |
vpc_id |
string |
— | Target VPC |
ingress_rules |
list(object{...}) |
[] |
Ingress rules |
egress_rules |
list(object{...}) |
allow-all outbound | Egress rules |
tags |
map(string) |
{} |
Common tags |
Rule object shape:
{
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = null # or ["0.0.0.0/0"]
source_security_group_id = "sg-xxx" # or null
self = false
}Outputs
| Output | Description |
|---|---|
id |
SG ID |
arn |
SG ARN |
name |
SG name |
owner_id |
AWS account owner |
Creates an IAM role (EC2 trust policy by default), attaches policies, and bundles a matching instance profile.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Role + instance profile name |
description |
string |
"Managed by Terraform" |
Role description |
policy_arns |
list(string) |
[] |
Managed/custom policy ARNs to attach |
assume_role_policy |
string |
EC2 service principal | Custom trust policy |
max_session_duration |
number |
3600 |
Max session (3600–43200s) |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
role_arn / role_name / role_id |
Role attributes |
instance_profile_arn / instance_profile_name / instance_profile_id |
Instance profile attributes |
Superseded: the root module now uses the
autoscalingmodule. This module is kept as a reusable building block for launching a single, staticaws_instance.
Launches one EC2 instance. Resolves the latest AMI from an SSM parameter unless ami_id is set (the repo pins Ubuntu 26.04 via ami_id).
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Name tag |
instance_type |
string |
— | Instance type |
subnet_id |
string |
— | Subnet to launch into |
vpc_security_group_ids |
list(string) |
— | Attached SGs |
iam_instance_profile |
string |
null |
Instance profile |
key_name |
string |
null |
Key pair |
user_data |
string |
null |
Bootstrap script (use file()) |
ami_id |
string |
null |
Pinned AMI (skips SSM lookup) |
ami_ssm_parameter |
string |
AL2023 path | SSM parameter for AMI lookup |
root_volume_size |
number |
20 |
Root EBS size (GiB) |
root_volume_type |
string |
"gp3" |
Root EBS type |
root_volume_encrypted |
bool |
true |
Encrypt root volume |
monitoring |
bool |
false |
Detailed CloudWatch monitoring |
associate_public_ip_address |
bool |
false |
Assign a public IP (required for internet SSH; instance must be in a public subnet) |
disable_api_termination |
bool |
false |
Termination protection |
tags |
map(string) |
{} |
Common tags |
The instance also enforces IMDSv2 only (http_tokens = "required"), which pairs with the user data script's token-based metadata fetch.
Outputs
| Output | Description |
|---|---|
id / arn |
Instance attributes |
private_ip / private_dns |
Private addressing |
public_ip / public_dns |
Public addressing (null when no public IP assigned) |
availability_zone / subnet_id |
Placement |
ami / instance_type |
Launch details |
security_group_ids |
Attached SGs |
Creates a launch template + Auto Scaling Group for the web servers. Instances are spread across all provided subnets (here: the public subnets, one per AZ) and registered with the ALB target group via target_group_arns. With health_check_type = "ELB" the ASG terminates and replaces any instance that fails the ALB health check, and capacity can scale between min_size and max_size.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Prefix for the launch template, ASG, and propagated instance Name tags |
ami_id |
string |
null |
Pinned AMI (skips SSM lookup) |
ami_ssm_parameter |
string |
AL2023 path | SSM parameter for AMI lookup |
instance_type |
string |
"t3.micro" |
Instance type |
key_name |
string |
null |
Key pair for SSH |
vpc_security_group_ids |
list(string) |
— | SGs attached to every instance |
iam_instance_profile |
string |
null |
Instance profile |
user_data |
string |
null |
Bootstrap script (use file()); base64-encoded internally |
associate_public_ip_address |
bool |
true |
Public IP (requires public subnets) |
subnet_ids |
list(string) |
— | Subnets to distribute instances across |
target_group_arns |
list(string) |
[] |
ALB target groups to register with |
min_size |
number |
2 |
Minimum capacity |
desired_capacity |
number |
3 |
Steady-state capacity |
max_size |
number |
5 |
Maximum capacity |
health_check_type |
string |
"ELB" |
ELB (app-aware) or EC2 |
health_check_grace_period |
number |
300 |
Seconds before health checks start |
default_cooldown |
number |
300 |
Cooldown between scaling activities |
wait_for_capacity_timeout |
string |
"10m" |
Apply waits for desired capacity |
root_device_name |
string |
"/dev/sda1" |
Ubuntu AMI root device (AL2023 uses /dev/xvda) |
root_volume_size / type / encrypted |
number/string/bool |
20 / "gp3" / true |
Root EBS volume |
tags |
map(string) |
{} |
Common tags (propagated to instances) |
Instances also enforce IMDSv2 only (http_tokens = "required") and propagate tags via tag_specifications/propagate_at_launch.
Outputs
| Output | Description |
|---|---|
asg_id / asg_arn / asg_name |
ASG attributes |
min_size / desired_capacity / max_size |
Capacity bounds |
launch_template_id / launch_template_latest_version |
Launch template attributes |
launch_template_ami |
AMI in use |
Deploys an ALB, target group, and HTTP listener. Instances are registered with the target group by the Auto Scaling Group (via target_group_arns), so the instance_ids input is empty in the root module.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string |
— | ALB name |
vpc_id |
string |
— | VPC for the target group |
subnet_ids |
list(string) |
— | ALB subnets (public) |
security_group_ids |
list(string) |
— | ALB SGs |
internal |
bool |
false |
Internet-facing vs internal |
load_balancer_type |
string |
"application" |
LB type |
idle_timeout |
number |
60 |
Idle timeout (s) |
enable_deletion_protection |
bool |
false |
Deletion protection |
drop_invalid_header_fields |
bool |
true |
Drop invalid HTTP/2 headers |
target_group_name |
string |
— | Target group name |
target_group_port |
number |
80 |
Target port |
target_group_protocol |
string |
"HTTP" |
Target protocol |
target_type |
string |
"instance" |
instance or ip |
health_check |
object |
sane defaults | Health check config |
listener_port |
number |
80 |
Listener port |
listener_protocol |
string |
"HTTP" |
Listener protocol |
instance_ids |
map(string) |
{} |
name → instance ID |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
alb_id / alb_arn / alb_arn_suffix |
ALB attributes |
alb_dns_name |
DNS name (use this to reach the app) |
alb_zone_id |
Hosted zone for Route 53 aliases |
target_group_arn / target_group_name |
Target group attributes |
listener_arn |
HTTP listener ARN |
target_group_attachment_ids |
Attachment IDs |
Creates a hardened bucket (versioning + SSE + public access block + lifecycle) and optionally the DynamoDB lock table.
Inputs
| Variable | Type | Default | Description |
|---|---|---|---|
bucket_name |
string |
— | Globally unique bucket name |
enable_versioning |
bool |
true |
Object versioning |
force_destroy |
bool |
false |
Delete bucket with contents |
sse_algorithm |
string |
"aws:kms" |
aws:kms or AES256 |
kms_master_key_id |
string |
null |
Custom KMS key |
block_public_acls / block_public_policy / ignore_public_acls / restrict_public_buckets |
bool |
true |
Public access block |
lifecycle_rules |
list(object{...}) |
1 sensible rule | Expiration / versioning rules |
create_dynamodb_lock_table |
bool |
true |
Create lock table |
dynamodb_table_name |
string |
derived | Lock table name |
dynamodb_billing_mode |
string |
"PAY_PER_REQUEST" |
Billing mode |
tags |
map(string) |
{} |
Common tags |
Outputs
| Output | Description |
|---|---|
bucket_id / bucket_arn |
Bucket attributes |
bucket_domain_name / bucket_regional_domain_name |
Domain names |
versioning_enabled |
Versioning status |
sse_algorithm |
Active encryption |
dynamodb_lock_table_name / dynamodb_lock_table_arn |
Lock table (null when disabled) |
Everything that matters is a variable. Common overrides via terraform.tfvars:
# Scale to a bigger instance
instance_type = "t3.medium"
# Autoscaling capacity bounds (must satisfy min <= desired <= max)
autoscaling_min_size = 3
autoscaling_desired_capacity = 4
autoscaling_max_size = 8
# Run in another region / namespacing
region = "us-east-1"
project_name = "myapp"
environment = "prod"
# Reachable SSH CIDRs (keep this locked down!)
ssh_cidr_blocks = ["203.0.113.10/32", "10.0.0.0/16"]
# Use your own key pair
key_name = "prod-key"
# Pin the AMI instead of resolving latest via SSM
ami_id = "ami-0abcdef1234567890"
# Custom user data
userdata_script = "./userdata/web.sh"You can also add extra tags:
tags = {
Owner = "platform-team"
CostCenter = "platform-01"
Repo = "terraform-aws-ha"
}- No HTTPS listener yet. Adding one requires an ACM certificate; a second
aws_lb_listenerfor port 443 forwarding to the same target group is the standard extension. - Instances are in public subnets to support internet SSH (a design choice for this demo). For a hardened setup, move them back to the private subnets, remove
associate_public_ip_address, and rely on SSM (or a bastion) — the private subnets and NAT-readyroute_tablemodule are already in place. ssh_cidr_blocksdefault is wide open (0.0.0.0/0). Restrict it to your office/VPN CIDR before real use.- Single target group. Fine for this demo; multi-service apps would add per-service target groups.
state_bucket_namefallback is deterministic but may already exist in another account; always setbucket_nameexplicitly.- No scaling policy / CloudWatch alarms yet — the ASG currently runs at
desired_capacity. Adding a target-tracking policy (CPU-based) or scheduled scaling is the natural next step. - No Route 53 / ACM / HTTPS / CloudWatch alarms — deliberate scope. Each would be its own module following the same conventions.