Skip to content

About

AWS IaaC Code for Infrastructure Resources

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

terraform-aws-ha

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.


Table of Contents


Architecture

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;
Loading

Data flow

  1. Clients reach the ALB on port 80/443 through the ALB security group.
  2. The ALB lives in the public subnets and forwards port 80 to the target group.
  3. 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.
  4. The EC2 security group only allows HTTP from the ALB security group, so web traffic can only reach the instances through the load balancer.
  5. 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).
  6. The hardened S3 bucket (versioning + SSE + lifecycle + public-access block) and DynamoDB lock table are pre-provisioned for the future remote state backend.

Repository Layout

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.


What Gets Deployed

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

Prerequisites

  • 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: aws CLI for inspecting/destroying resources.

Required IAM Permissions

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 AdministratorAccess managed policy on the executing role. For a locked-down setup, attach only the scopes above (all scoped to the account/region, with elasticloadbalancing and s3:* being the widest).


Getting Started

1. Clone & Configure

cd terraform-aws-ha
cp terraform.tfvars.example terraform.tfvars

Edit terraform.tfvars:

  • Set a globally unique bucket_name (S3 bucket names are global).
  • Restrict ssh_cidr_blocks to your office/VPN CIDR, or set ssh_cidr_blocks = [] to disable SSH entirely.
  • Optionally set key_name if you want SSH key-based access.

2. Initialize

terraform init

This downloads the AWS provider and installs the local modules. The default backend is local state (terraform.tfstate), so nothing else is required to run.

3. Plan

terraform plan

Review 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).

4. Apply

terraform apply -auto-approve

Or use the interactive flow:

terraform apply

When 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"

5. Verify

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)

6. Destroy

terraform destroy

The S3 bucket has force_destroy = false by default. Set force_destroy_bucket = true in terraform.tfvars only 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.


Accessing the Instances (SSH + Session Manager)

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-1

Or 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.


Migrating to the S3 Remote Backend

The s3_backend module already created a versioned, encrypted, locked-down bucket plus a DynamoDB lock table. To move state there:

  1. Apply once with local state (as above) so the bucket and table exist.

  2. Edit backend.tf and uncomment the terraform.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
      }
    }
  3. 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/apply runs are safe for a team.

Keep the backend block commented out in the repo so the project still runs locally out of the box.


How the Modules Interact

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_each everywhere 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), never 0.0.0.0/0.
  • Consistent tagging — the root provider sets default_tags from local.tags; modules merge their own Name tags 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).

Module Reference

vpc

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

subnet

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

internet_gateway

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

route_table

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

security_group

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

iam

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

ec2

Superseded: the root module now uses the autoscaling module. This module is kept as a reusable building block for launching a single, static aws_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

autoscaling

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

alb

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

s3

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)

Customization

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"
}

Known Limitations & Next Steps

  • No HTTPS listener yet. Adding one requires an ACM certificate; a second aws_lb_listener for 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-ready route_table module are already in place.
  • ssh_cidr_blocks default 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_name fallback is deterministic but may already exist in another account; always set bucket_name explicitly.
  • 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.

About

AWS IaaC Code for Infrastructure Resources

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages