https://github.com/infrahouse/terraform-aws-website-pod
Terraform module for web application deployment with ALB, ACM SSL certificates, EC2 autoscaling, CloudWatch alarms, CAA records, and spot instance support.
https://github.com/infrahouse/terraform-aws-website-pod
alb autoscaling aws infrahouse terraform terraform-module website
Last synced: 5 months ago
JSON representation
Terraform module for web application deployment with ALB, ACM SSL certificates, EC2 autoscaling, CloudWatch alarms, CAA records, and spot instance support.
- Host: GitHub
- URL: https://github.com/infrahouse/terraform-aws-website-pod
- Owner: infrahouse
- License: apache-2.0
- Created: 2023-08-13T19:44:15.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2026-01-23T14:13:32.000Z (6 months ago)
- Last Synced: 2026-01-23T17:38:39.577Z (6 months ago)
- Topics: alb, autoscaling, aws, infrahouse, terraform, terraform-module, website
- Language: HCL
- Homepage: https://infrahouse.github.io/terraform-aws-website-pod
- Size: 1.04 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# terraform-aws-website-pod
[](https://infrahouse.com/contact)
[](https://infrahouse.github.io/terraform-aws-website-pod/)
[](https://registry.terraform.io/modules/infrahouse/website-pod/aws/latest)
[](https://github.com/infrahouse/terraform-aws-website-pod/releases/latest)
[](https://aws.amazon.com/elasticloadbalancing/)
[](https://github.com/infrahouse/terraform-aws-website-pod/actions/workflows/vuln-scanner-pr.yml)
[](LICENSE)
The module creates resources to run an HTTP service in an autoscaling group.
It creates a load balancer that terminates SSL on the TCP port 443.
It also issues the SSL certificate in ACM.
## Why This Module?
Setting up a production-ready web application on AWS requires configuring multiple interconnected services:
ALB, ASG, ACM certificates, Route53 DNS, security groups, CloudWatch alarms, and more.
This module handles all of that complexity in a single, well-tested package.
**Key benefits:**
- **Production-ready**: Battle-tested in production environments with comprehensive monitoring and alerting
- **Security-focused**: Automatic SSL/TLS certificates, CAA records, configurable access controls, and compliance support (SOC2, HIPAA, PCI-DSS)
- **Cost-optimized**: Support for spot instances to reduce costs by up to 90%
- **Fully automated**: Zero-downtime deployments with instance refresh and lifecycle hooks
## Features
- **Application Load Balancer (ALB)** with automatic HTTP to HTTPS redirect
- **Auto Scaling Group (ASG)** with target tracking scaling policies
- **ACM SSL Certificate** with automatic DNS validation
- **Route53 DNS Records** including CAA records for certificate security
- **CloudWatch Alarms** for CPU, latency, error rates, and unhealthy hosts
- **Security Groups** with configurable ingress rules for ALB and backend instances
- **Spot Instance Support** with configurable on-demand base capacity
- **ALB Access Logging** to S3 for security and compliance
- **Session Stickiness** for stateful applications
- **Lifecycle Hooks** for graceful instance launch and termination
## Documentation
For detailed documentation, visit the [GitHub Pages documentation site](https://infrahouse.github.io/terraform-aws-website-pod/).
- [Getting Started](https://infrahouse.github.io/terraform-aws-website-pod/getting-started/)
- [Architecture](https://infrahouse.github.io/terraform-aws-website-pod/architecture/)
- [Configuration Reference](https://infrahouse.github.io/terraform-aws-website-pod/configuration/)
- [Examples](https://infrahouse.github.io/terraform-aws-website-pod/examples/)
- [Troubleshooting](https://infrahouse.github.io/terraform-aws-website-pod/troubleshooting/)
## Quick Start
> **Note**: Starting from version 2.0 the module separates the main aws provider and a provider for
> Route53 resources. If you don't need to separate them, just pass the same provider for `aws` and `aws.dns`
> ```hcl
> providers = {
> aws = aws
> aws.dns = aws
> }
> ```
## Usage
```hcl
module "website" {
providers = {
aws = aws.aws-uw1
aws.dns = aws.aws-uw1
}
source = "infrahouse/website-pod/aws"
version = "5.17.0"
environment = var.environment
ami = data.aws_ami.ubuntu_22.image_id
backend_subnets = module.website-vpc.subnet_private_ids
zone_id = "Z07662251LH3YRF2ERM3G"
dns_a_records = ["", "www"]
internet_gateway_id = module.website-vpc.internet_gateway_id
key_pair_name = data.aws_key_pair.aleks.key_name
subnets = module.website-vpc.subnet_public_ids
userdata = module.webserver_userdata.userdata
stickiness_enabled = true
}
```
### Security groups
The module used default security groups up until version 2.5.0.
Starting from the version 2.6.0 the behavior changes, however in a backward-compatible manner.
The module creates two security groups. One for the load balancer, another - for the backend instances.
The load balancer security group allows traffic to TCP ports 443 and `var.alb_listener_port` (80 by default).
By default, traffic is allowed from any source (0.0.0.0/0), but this can be restricted using `var.alb_ingress_cidr_blocks`.
The backend security group allows user traffic and health checks coming from the load balancer.
Also, the security group allows SSH from the VPC where the backend instances reside and from `var.ssh_cidr_block`.
It is 0.0.0.0/0 by default, but the goal is allow user restrict access let's say to anyone but the management VPC.
Both security groups allow incoming ICMP traffic.
Additionally, the user can specify additional security groups via `var.extra_security_groups_backend`.
They will be added to the backend instance alongside with the created backend security group.
### Using spot instances
By default, the module launches on-demand instances only. However, if you specify `var.on_demand_base_capacity`,
the ASG will fulfill its capacity by as many on-demand instances as `var.on_demand_base_capacity` and the rest will
be spot instances.
### Certificate Authority Authorization (CAA) Records
The module automatically creates CAA records for each DNS A record to control which certificate authorities can issue certificates for your domain. By default, only Amazon (ACM) is allowed to issue certificates.
To allow additional certificate authorities, use the `certificate_issuers` variable:
```hcl
module "website" {
# ... other configuration ...
# Allow both Amazon and Let's Encrypt to issue certificates
certificate_issuers = ["amazon.com", "letsencrypt.org"]
}
```
The module automatically formats these domains into proper CAA records and adds a wildcard certificate blocking record (`0 issuewild ";"`) for security.
### Restricting ALB Access
By default, the load balancer accepts traffic from any source (0.0.0.0/0).
You can restrict access to specific CIDR ranges using the `alb_ingress_cidr_blocks` variable:
```hcl
module "website" {
# ... other configuration ...
# Allow access only from specific networks
alb_ingress_cidr_blocks = [
"10.0.0.0/8", # Internal corporate network
"203.0.113.0/24" # Specific external IP range
]
}
```
This creates separate security group rules for each CIDR block, allowing fine-grained control over
who can access your load balancer on both HTTP (port 80/`var.alb_listener_port`) and HTTPS (port 443).
### ALB Access Logging (Security Best Practice)
**Recommended:** Enable ALB access logging for security investigations, incident response, debugging, and compliance requirements.
```hcl
module "website" {
# ... other configuration ...
# Enable access logging (recommended for production)
alb_access_log_enabled = true
}
```
When enabled, the module creates an encrypted, versioned S3 bucket that stores detailed ALB access logs. These logs are essential for:
- **Security:** Track unauthorized access attempts and identify suspicious traffic patterns
- **Compliance:** Meet SOC2, HIPAA, PCI-DSS, and ISO 27001 requirements
- **Operations:** Debug production issues and analyze traffic patterns
- **AWS Best Practices:** Aligns with AWS Well-Architected Framework security pillar
**Cost Impact:** Minimal (~$4/year for moderate traffic). Storage costs are negligible compared to security and compliance benefits.
**Note:** Starting in v6.0.0, access logging will be enabled by default. See `variables.tf` for details.
## Deprecated Variables
The following variables contain typos and are deprecated. They will be removed in **v6.0.0**.
| Deprecated Variable (v5.x) | Correct Variable (Use This) | Status |
|-------------------------------------|--------------------------------------|---------------------------|
| `alb_healthcheck_uhealthy_threshold`| `alb_healthcheck_unhealthy_threshold`| ⚠️ Deprecated in v5.11.0 |
| `attach_tagret_group_to_asg` | `attach_target_group_to_asg` | ⚠️ Deprecated in v5.11.0 |
### Migration Instructions
If you're using the deprecated variables, update your code before upgrading to v6.0.0:
**Before:**
```hcl
module "website" {
source = "infrahouse/website-pod/aws"
version = "~> 5.0"
alb_healthcheck_uhealthy_threshold = 3 # Typo: "uhealthy"
attach_tagret_group_to_asg = true # Typo: "tagret"
}
```
**After:**
```hcl
module "website" {
source = "infrahouse/website-pod/aws"
version = "~> 5.11" # or "~> 6.0" when available
alb_healthcheck_unhealthy_threshold = 3 # Correct spelling
attach_target_group_to_asg = true # Correct spelling
}
```
For detailed migration guidance, see [UPGRADE-6.0.md](UPGRADE-6.0.md).
## Development and Testing
### Running Tests
The module includes a comprehensive test suite. Tests use pytest and deploy real infrastructure to AWS.
#### Run all tests:
```bash
make test
```
#### Run tests with AWS credentials:
```bash
make test-keep # Keeps infrastructure for debugging
make test-clean # Destroys infrastructure after test
```
### Troubleshooting Test Failures
If a test fails in CI, you can run the specific failed test locally:
#### Run a specific test:
```bash
# Run a specific test file and function
make test-keep TEST_PATH=tests/test_asg_name.py::test_lb TEST_FILTER=
# Run all tests in a file
make test-keep TEST_PATH=tests/test_asg_name.py TEST_FILTER=
```
#### Available test variables:
- `TEST_PATH` - Path to test file or specific test (default: `tests/test_create_lb.py`)
- `TEST_FILTER` - pytest `-k` filter expression (default: `"internet-facing and aws-6"`)
- `TEST_REGION` - AWS region for testing (default: `"us-west-2"`)
- `TEST_ROLE` - IAM role ARN for testing (default: module-specific test role)
#### Examples:
```bash
# Run with default filter
make test-keep
# Run specific test without filter
make test-keep TEST_PATH=tests/test_spot.py::test_lb TEST_FILTER=
# Run with custom filter
make test-keep TEST_FILTER="aws-5"
# Run specific test and keep resources for debugging
make test-keep TEST_PATH=tests/test_asg_name.py::test_lb TEST_FILTER=
# Run and clean up afterward
make test-clean TEST_PATH=tests/test_asg_name.py::test_lb TEST_FILTER=
```
### Other Development Commands
```bash
make bootstrap # Install dependencies and git hooks
make fmt # Format Terraform and Python code
make lint # Check code style
make validate # Validate Terraform configuration
```
## Requirements
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | ~> 1.5 |
| [aws](#requirement\_aws) | >= 5.11, < 7.0 |
| [random](#requirement\_random) | ~> 3.6 |
## Providers
| Name | Version |
|------|---------|
| [aws](#provider\_aws) | >= 5.11, < 7.0 |
| [aws.dns](#provider\_aws.dns) | >= 5.11, < 7.0 |
| [random](#provider\_random) | ~> 3.6 |
## Modules
| Name | Source | Version |
|------|--------|---------|
| [instance\_profile](#module\_instance\_profile) | registry.infrahouse.com/infrahouse/instance-profile/aws | 1.9.0 |
## Resources
| Name | Type |
|------|------|
| [aws_acm_certificate.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate) | resource |
| [aws_acm_certificate_validation.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation) | resource |
| [aws_alb.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/alb) | resource |
| [aws_alb_listener.redirect_to_ssl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/alb_listener) | resource |
| [aws_alb_listener_rule.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/alb_listener_rule) | resource |
| [aws_alb_target_group.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/alb_target_group) | resource |
| [aws_autoscaling_group.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group) | resource |
| [aws_autoscaling_lifecycle_hook.launching](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_lifecycle_hook) | resource |
| [aws_autoscaling_lifecycle_hook.terminating](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_lifecycle_hook) | resource |
| [aws_autoscaling_policy.cpu_load](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_policy) | resource |
| [aws_cloudwatch_metric_alarm.cpu_utilization](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource |
| [aws_cloudwatch_metric_alarm.low_success_rate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource |
| [aws_cloudwatch_metric_alarm.target_response_time](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource |
| [aws_cloudwatch_metric_alarm.unhealthy_host_count](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource |
| [aws_launch_template.website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template) | resource |
| [aws_lb_listener.ssl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource |
| [aws_route53_record.cert_validation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource |
| [aws_route53_record.extra](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource |
| [aws_route53_record.extra_caa_amazon](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource |
| [aws_s3_bucket.access_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket_policy.access_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource |
| [aws_s3_bucket_public_access_block.public_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
| [aws_s3_bucket_server_side_encryption_configuration.access_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_s3_bucket_versioning.access_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
| [aws_security_group.alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_security_group.backend](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_sns_topic.alarms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource |
| [aws_sns_topic_subscription.alarm_emails](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource |
| [aws_vpc_security_group_egress_rule.alb_outgoing](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_egress_rule.backend_outgoing](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.alb_icmp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.alb_listener_port](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.backend_healthcheck](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.backend_icmp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.backend_ssh_input](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.backend_ssh_local](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.backend_user_traffic](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.https](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [random_string.profile_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource |
| [aws_ami.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source |
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_default_tags.provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags) | data source |
| [aws_ec2_instance_type.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ec2_instance_type) | data source |
| [aws_iam_policy_document.access_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.default_permissions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
| [aws_route53_zone.webserver_zone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source |
| [aws_subnet.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source |
| [aws_vpc.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source |
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| [alarm\_cpu\_utilization\_threshold](#input\_alarm\_cpu\_utilization\_threshold) | CPU utilization percentage that triggers an alarm.
This alarm detects when autoscaling FAILS to keep up with demand, which may indicate:
- ASG reached max\_size (cannot scale further)
- New instances failing to provision
- New instances not becoming healthy
- Infrastructure capacity/quota issues
**Automatic calculation:**
If not specified, defaults to autoscaling\_target\_cpu\_load + 30%.
This provides a buffer for autoscaling to respond before alarming.
**Example automatic thresholds:**
- autoscaling\_target\_cpu\_load = 60%: alarm at 90%
- autoscaling\_target\_cpu\_load = 70%: alarm at 99% (capped)
**How it works:**
When CPU exceeds target (60% default), ASG launches new instances (~5-10 min).
If CPU stays high for 10 minutes (period × evaluation\_periods), autoscaling has failed - time to alert!
**Override for custom thresholds:**
alarm_cpu_utilization_threshold = 85 # Explicit threshold
**Note:** This is a Vanta compliance requirement (Server CPU monitored). | `number` | `null` | no |
| [alarm\_emails](#input\_alarm\_emails) | List of email addresses to receive CloudWatch alarm notifications for ALB monitoring.
⚠️ **IMPORTANT - EMAIL CONFIRMATION REQUIRED:**
After deployment, AWS SNS will send a confirmation email to each address.
**You MUST click the confirmation link** in each email to activate notifications.
Until confirmed:
- Subscription status: PendingConfirmation
- Alarms will fire but notifications will NOT be delivered
- No alerts will reach your team during incidents
**Action Required:** Check spam folders and confirm all subscription emails immediately after deployment.
**Vanta Compliance Requirements:**
When configured, creates CloudWatch alarms for:
- Load balancer unhealthy host count monitoring
- Load balancer latency monitoring
- Load balancer server errors (5xx) monitoring
- Server CPU utilization monitoring
**Example:**alarm_emails = ["ops-team@example.com", "on-call@example.com"]
⚠️ **FUTURE REQUIREMENT:** In v6.0.0, at least one email address will be required.
See UPGRADE-6.0.md for migration details. | `list(string)` | `[]` | no |
| [alarm\_evaluation\_periods](#input\_alarm\_evaluation\_periods) | Number of periods over which to compare the metric to the threshold.
With 1-minute periods, setting this to 2 means the alarm must breach
for 2 consecutive minutes before triggering. | `number` | `2` | no |
| [alarm\_success\_rate\_period](#input\_alarm\_success\_rate\_period) | Time period (in seconds) over which to calculate the success rate.
Longer periods provide more statistical stability, especially important
for low-traffic sites where individual errors can skew short-term rates.
**Default:** 300 seconds (5 minutes)
**Recommendations by traffic volume:**
- Very low traffic (< 1 req/min): 3600s (1 hour) for statistical significance
- Low traffic (1-10 req/min): 900s (15 min)
- Medium traffic (10-100 req/min): 300s (5 min) - default
- High traffic (> 100 req/min): 60s (1 min) for faster detection
**Detection time:** With evaluation\_periods=2:
- 3600s (1 hour) = 2 hour detection time
- 900s (15 min) = 30 minute detection time
- 300s (5 min) = 10 minute detection time
- 60s (1 min) = 2 minute detection time
**Example for low-traffic site:**alarm_success_rate_period = 3600 # 1 hour window
alarm_success_rate_threshold = 99.0
With 10 requests/hour, allows 1 error before alarming. | `number` | `300` | no |
| [alarm\_success\_rate\_threshold](#input\_alarm\_success\_rate\_threshold) | Minimum success rate (percentage) before triggering an alarm.
Success rate = (non-5xx responses) / (total responses) * 100
This is smarter than a raw error count because it scales with traffic volume.
A 1% error rate means the same thing whether you have 100 or 100,000 requests.
**Default:** 99.0 (alerts when error rate exceeds 1%)
**Examples:**
- 99.9 = Alert when more than 0.1% of requests fail (very strict SLO)
- 99.0 = Alert when more than 1% of requests fail (recommended)
- 95.0 = Alert when more than 5% of requests fail (lenient)
**Note:** Alarms won't trigger during periods with zero traffic. | `number` | `99` | no |
| [alarm\_target\_response\_time\_threshold](#input\_alarm\_target\_response\_time\_threshold) | Target response time threshold in seconds that triggers a latency alarm.
If not specified, defaults to 80% of alb\_idle\_timeout to alert before
connections start timing out.
Example: With default alb\_idle\_timeout=60s, this will default to 48s.
You can override this for more aggressive monitoring:
- API services: 0.5 - 1.0 seconds
- Web applications: 1.0 - 2.0 seconds
- Backend services: 2.0 - 5.0 seconds | `number` | `null` | no |
| [alarm\_topic\_arns](#input\_alarm\_topic\_arns) | List of existing SNS topic ARNs to send ALB alarms to.
Use this for advanced integrations like PagerDuty, Slack, OpsGenie, etc.
These topics will receive notifications in addition to any configured alarm\_emails.
**Example:**alarm_topic_arns = [
"arn:aws:sns:us-east-1:123456789012:pagerduty-critical",
"arn:aws:sns:us-east-1:123456789012:slack-alerts"
]
| `list(string)` | `[]` | no |
| [alarm\_unhealthy\_host\_threshold](#input\_alarm\_unhealthy\_host\_threshold) | Number of unhealthy hosts that triggers an alarm.
Uses GreaterThanThreshold comparison, so:
- 0 = Alert when ANY host becomes unhealthy (count > 0)
- 1 = Alert when 2+ hosts are unhealthy (count > 1) - default
- 2 = Alert when 3+ hosts are unhealthy (count > 2)
**Recommended:** Set to 0 for immediate alerting in production, or 1 to allow
for graceful deployments where one host may briefly be unhealthy during updates. | `number` | `1` | no |
| [alb\_access\_log\_enabled](#input\_alb\_access\_log\_enabled) | Whether to enable ALB access logging to S3.
**Security Best Practice:** Enabling access logs is recommended for:
- Security investigations and incident response
- Debugging production issues
- Compliance requirements (SOC2, HIPAA, PCI-DSS)
- AWS Well-Architected Framework best practices
When enabled, creates an encrypted, versioned S3 bucket for access logs.
Storage costs are minimal compared to security and operational benefits.
**Note:** In v6.0.0, this will default to `true` (enabled by default).
See UPGRADE-6.0.md for details. | `bool` | `false` | no |
| [alb\_access\_log\_force\_destroy](#input\_alb\_access\_log\_force\_destroy) | Destroy S3 bucket with access logs even if non-empty | `bool` | `false` | no |
| [alb\_healthcheck\_enabled](#input\_alb\_healthcheck\_enabled) | Whether health checks are enabled. | `bool` | `true` | no |
| [alb\_healthcheck\_healthy\_threshold](#input\_alb\_healthcheck\_healthy\_threshold) | Number of times the host have to pass the test to be considered healthy | `number` | `2` | no |
| [alb\_healthcheck\_interval](#input\_alb\_healthcheck\_interval) | Number of seconds between checks | `number` | `5` | no |
| [alb\_healthcheck\_path](#input\_alb\_healthcheck\_path) | Path on the webserver that the elb will check to determine whether the instance is healthy or not | `string` | `"/index.html"` | no |
| [alb\_healthcheck\_port](#input\_alb\_healthcheck\_port) | Port of the webserver that the elb will check to determine whether the instance is healthy or not | `any` | `80` | no |
| [alb\_healthcheck\_protocol](#input\_alb\_healthcheck\_protocol) | Protocol to use with the webserver that the elb will check to determine whether the instance is healthy or not | `string` | `"HTTP"` | no |
| [alb\_healthcheck\_response\_code\_matcher](#input\_alb\_healthcheck\_response\_code\_matcher) | Range of http return codes that can match | `string` | `"200-299"` | no |
| [alb\_healthcheck\_timeout](#input\_alb\_healthcheck\_timeout) | Number of seconds to timeout a check | `number` | `4` | no |
| [alb\_healthcheck\_uhealthy\_threshold](#input\_alb\_healthcheck\_uhealthy\_threshold) | ⚠️ DEPRECATED - Contains typo, use 'alb\_healthcheck\_unhealthy\_threshold' instead.
This variable will be removed in v6.0.0. See deprecations.tf for details.
Number of times the host must fail the test to be considered unhealthy. | `number` | `null` | no |
| [alb\_healthcheck\_unhealthy\_threshold](#input\_alb\_healthcheck\_unhealthy\_threshold) | Number of consecutive health check failures required before considering the target unhealthy | `number` | `2` | no |
| [alb\_idle\_timeout](#input\_alb\_idle\_timeout) | The time in seconds that the connection is allowed to be idle. | `number` | `60` | no |
| [alb\_ingress\_cidr\_blocks](#input\_alb\_ingress\_cidr\_blocks) | List of CIDR blocks allowed to access the ALB. Defaults to allow all (0.0.0.0/0). | `list(string)` | [
"0.0.0.0/0"
]
| no |
| [alb\_listener\_port](#input\_alb\_listener\_port) | TCP port that a load balancer listens to to serve client HTTP requests. The load balancer redirects this port to 443 and HTTPS. | `number` | `80` | no |
| [alb\_name\_prefix](#input\_alb\_name\_prefix) | Name prefix for the load balancer | `string` | `"web"` | no |
| [allow\_wildcard\_certificates](#input\_allow\_wildcard\_certificates) | If true, CAA records will allow wildcard certificates from the configured certificate\_issuers.
If false, wildcard certificates are blocked. | `bool` | `false` | no |
| [ami](#input\_ami) | Image for EC2 instances | `string` | n/a | yes |
| [asg\_default\_cooldown](#input\_asg\_default\_cooldown) | Amount of time, in seconds, after a scaling activity completes before another
scaling activity can start. This prevents rapid scale-in/scale-out cycles. | `number` | `300` | no |
| [asg\_enabled\_metrics](#input\_asg\_enabled\_metrics) | List of ASG metrics to enable for CloudWatch monitoring.
Set to empty list to disable metrics collection.
Available metrics:
- GroupDesiredCapacity
- GroupInServiceInstances
- GroupPendingInstances
- GroupTerminatingInstances
- GroupTotalInstances
- GroupMinSize
- GroupMaxSize
- GroupInServiceCapacity
- GroupPendingCapacity
- GroupStandbyCapacity
- GroupStandbyInstances
- GroupTerminatingCapacity
- GroupTotalCapacity
- WarmPoolDesiredCapacity
- WarmPoolWarmedCapacity
- WarmPoolPendingCapacity
- WarmPoolTerminatingCapacity
- WarmPoolTotalCapacity
- WarmPoolMinSize
- GroupAndWarmPoolDesiredCapacity
- GroupAndWarmPoolTotalCapacity | `list(string)` | [
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupTerminatingInstances",
"GroupTotalInstances"
]
| no |
| [asg\_lifecycle\_hook\_heartbeat\_timeout](#input\_asg\_lifecycle\_hook\_heartbeat\_timeout) | How much time in seconds to wait until the hook is completed before proceeding with the default action. | `number` | `3600` | no |
| [asg\_lifecycle\_hook\_initial](#input\_asg\_lifecycle\_hook\_initial) | Name for an initial LAUNCHING lifecycle hook configured via the initial\_lifecycle\_hook
block in the ASG. This hook is evaluated during ASG creation.
Only one initial hook is allowed per ASG.
Use this for simple lifecycle hooks that don't require additional configuration. | `string` | `null` | no |
| [asg\_lifecycle\_hook\_launching](#input\_asg\_lifecycle\_hook\_launching) | Name for a LAUNCHING lifecycle hook configured via a separate
aws\_autoscaling\_lifecycle\_hook resource. This allows for more complex configurations
and can be created after the ASG exists.
Use this if you need to attach SNS notifications or additional settings to the lifecycle hook. | `string` | `null` | no |
| [asg\_lifecycle\_hook\_launching\_default\_result](#input\_asg\_lifecycle\_hook\_launching\_default\_result) | Default result for launching lifecycle hook. | `string` | `"ABANDON"` | no |
| [asg\_lifecycle\_hook\_terminating](#input\_asg\_lifecycle\_hook\_terminating) | Create a TERMINATING lifecycle hook with this name. | `string` | `null` | no |
| [asg\_lifecycle\_hook\_terminating\_default\_result](#input\_asg\_lifecycle\_hook\_terminating\_default\_result) | Default result for terminating lifecycle hook. | `string` | `"ABANDON"` | no |
| [asg\_max\_healthy\_percentage](#input\_asg\_max\_healthy\_percentage) | Specifies the upper limit on the number of instances that are in the InService or Pending state with a healthy status during an instance replacement activity. | `number` | `200` | no |
| [asg\_max\_size](#input\_asg\_max\_size) | Maximum number of instances in ASG | `number` | `10` | no |
| [asg\_min\_elb\_capacity](#input\_asg\_min\_elb\_capacity) | Terraform will wait until this many EC2 instances in the autoscaling group become healthy. By default, it's equal to var.asg\_min\_size. | `number` | `null` | no |
| [asg\_min\_healthy\_percentage](#input\_asg\_min\_healthy\_percentage) | Specifies the lower limit on the number of instances that must be in the InService state with a healthy status during an instance replacement activity. | `number` | `100` | no |
| [asg\_min\_size](#input\_asg\_min\_size) | Minimum number of instances in ASG | `number` | `2` | no |
| [asg\_name](#input\_asg\_name) | Autoscaling group name, if provided. | `string` | `null` | no |
| [asg\_scale\_in\_protected\_instances](#input\_asg\_scale\_in\_protected\_instances) | Behavior when encountering instances protected from scale in are found. Available behaviors are Refresh, Ignore, and Wait. | `string` | `"Ignore"` | no |
| [assume\_dns](#input\_assume\_dns) | If True, create DNS records provided by var.dns\_a\_records. | `bool` | `true` | no |
| [attach\_tagret\_group\_to\_asg](#input\_attach\_tagret\_group\_to\_asg) | ⚠️ DEPRECATED - Contains typo, use 'attach\_target\_group\_to\_asg' instead.
This variable will be removed in v6.0.0. See deprecations.tf for details.
Whether to register ASG instances in the target group. Disable if using ECS which registers targets itself. | `bool` | `null` | no |
| [attach\_target\_group\_to\_asg](#input\_attach\_target\_group\_to\_asg) | Whether to register ASG instances in the target group. Disable if using ECS which registers targets itself. | `bool` | `true` | no |
| [autoscaling\_target\_cpu\_load](#input\_autoscaling\_target\_cpu\_load) | Target CPU load for autoscaling | `number` | `60` | no |
| [backend\_subnets](#input\_backend\_subnets) | Subnet ids where EC2 instances should be present | `list(string)` | n/a | yes |
| [certificate\_issuers](#input\_certificate\_issuers) | List of certificate authority domains allowed to issue certificates for this domain (e.g., ["amazon.com", "letsencrypt.org"]). The module will format these as CAA records. | `list(string)` | [
"amazon.com"
]
| no |
| [dns\_a\_records](#input\_dns\_a\_records) | List of A records in the zone\_id that will resolve to the ALB dns name. | `list(string)` | [
""
]
| no |
| [dns\_routing\_policy](#input\_dns\_routing\_policy) | DNS routing policy for Route53 A records.
**Available policies:**
- `simple` (default): Standard DNS routing. Each A record resolves directly to the ALB.
Best for: Single deployments, standard configurations.
- `weighted`: Enables Route53 weighted routing policy for zero-downtime migrations.
Requires: dns\_set\_identifier must be set.
Best for: Blue/green deployments, gradual traffic migration, A/B testing.
**Migration workflow example:**
1. Deploy new service with `dns_routing_policy = "weighted"`, `dns_weight = 0`
2. Convert existing service to weighted with `dns_weight = 100`
3. Gradually shift: 90/10 → 50/50 → 10/90 → 0/100
4. Remove old service
**Note:** When using weighted routing, you can have multiple modules create
records for the same DNS name, each with a unique dns\_set\_identifier.
**Note:** This routing policy applies to ALL DNS records created via dns\_a\_records.
If you need different routing policies per record, deploy separate module instances. | `string` | `"simple"` | no |
| [dns\_set\_identifier](#input\_dns\_set\_identifier) | Unique identifier for weighted routing records.
Required when dns\_routing\_policy is not "simple".
This identifier distinguishes between multiple weighted records with the same name.
Must be unique across all weighted records for the same DNS name.
**Recommended naming conventions:**
- Environment-based: "production-blue", "production-green"
- Version-based: "v1", "v2", "v3"
- Region-based: "us-west-2-primary", "us-east-1-secondary"
- Module-based: "website-pod-main", "ecs-service-new"
**Example:**hcl
# Old service (being deprecated)
dns_routing_policy = "weighted"
dns_set_identifier = "legacy-service"
dns_weight = 10
# New service (receiving traffic)
dns_routing_policy = "weighted"
dns_set_identifier = "new-service"
dns_weight = 90
| `string` | `null` | no |
| [dns\_weight](#input\_dns\_weight) | Weight for Route53 weighted routing policy (0-255).
Only used when dns\_routing\_policy = "weighted".
**Weight behavior:**
- 0: No traffic routed to this endpoint (useful during initial deployment)
- 255: Maximum weight priority
- Traffic distribution = (this\_weight / sum\_of\_all\_weights) * 100%
**Examples:**
- Two endpoints with weights 100 and 100: 50% each
- Two endpoints with weights 100 and 0: 100% to first, 0% to second
- Three endpoints with weights 70, 20, 10: 70%, 20%, 10%
**Migration tip:** Start new deployments with weight=0, then gradually increase. | `number` | `100` | no |
| [enable\_deletion\_protection](#input\_enable\_deletion\_protection) | Prevent load balancer from destroying | `bool` | `false` | no |
| [environment](#input\_environment) | Name of environment | `string` | `"development"` | no |
| [extra\_security\_groups\_backend](#input\_extra\_security\_groups\_backend) | A list of security group ids to assign to backend instances | `list(string)` | `[]` | no |
| [health\_check\_grace\_period](#input\_health\_check\_grace\_period) | ASG will wait up to this number of seconds for instance to become healthy | `number` | `600` | no |
| [health\_check\_type](#input\_health\_check\_type) | Type of healthcheck the ASG uses. Can be EC2 or ELB. | `string` | `"ELB"` | no |
| [instance\_profile\_permissions](#input\_instance\_profile\_permissions) | A JSON policy document to attach to the instance profile.
This should be the output of an aws\_iam\_policy\_document data source.
Example:
instance\_profile\_permissions = data.aws\_iam\_policy\_document.my\_policy.json
If not specified, defaults to a minimal policy allowing sts:GetCallerIdentity. | `string` | `null` | no |
| [instance\_role\_name](#input\_instance\_role\_name) | If specified, the instance profile role will have this name. Otherwise, the role name will be generated. | `string` | `null` | no |
| [instance\_type](#input\_instance\_type) | EC2 instances type | `string` | `"t3.micro"` | no |
| [internet\_gateway\_id](#input\_internet\_gateway\_id) | Not used, but AWS Internet Gateway must be present. Ensure by passing its id. | `string` | `null` | no |
| [key\_pair\_name](#input\_key\_pair\_name) | SSH keypair name to be deployed in EC2 instances | `string` | n/a | yes |
| [load\_balancing\_algorithm\_type](#input\_load\_balancing\_algorithm\_type) | Load balancing algorithm for the target group.
**Available algorithms:**
- `round_robin` (default): Distributes requests evenly across healthy targets.
Best for: General-purpose workloads with similar request processing times.
- `least_outstanding_requests`: Routes to the target with fewest in-flight requests.
Best for: Workloads with varying request processing times, long-running requests,
or when backend instances have different capacities.
**Note:** When stickiness is enabled, the algorithm applies only to initial
session assignment. Subsequent requests from the same client go to the same target. | `string` | `"round_robin"` | no |
| [max\_instance\_lifetime\_days](#input\_max\_instance\_lifetime\_days) | The maximum amount of time, in \_days\_, that an instance can be in service, values must be either equal to 0 or between 7 and 365 days. | `number` | `30` | no |
| [min\_healthy\_percentage](#input\_min\_healthy\_percentage) | Amount of capacity in the Auto Scaling group that must remain healthy during an instance refresh to allow the operation to continue, as a percentage of the desired capacity of the Auto Scaling group. | `number` | `100` | no |
| [on\_demand\_base\_capacity](#input\_on\_demand\_base\_capacity) | If specified, the ASG will request spot instances and this will be the minimal number of on-demand instances. | `number` | `null` | no |
| [protect\_from\_scale\_in](#input\_protect\_from\_scale\_in) | Whether newly launched instances are automatically protected from termination by Amazon EC2 Auto Scaling when scaling in. | `bool` | `false` | no |
| [root\_volume\_size](#input\_root\_volume\_size) | Root volume size in EC2 instance in Gigabytes | `number` | `30` | no |
| [service\_name](#input\_service\_name) | Descriptive name of a service that will use this VPC | `string` | `"website"` | no |
| [sns\_topic\_alarm\_arn](#input\_sns\_topic\_alarm\_arn) | ARN of SNS topic for Cloudwatch alarms on base EC2 instance. | `string` | `null` | no |
| [ssh\_cidr\_block](#input\_ssh\_cidr\_block) | CIDR range that is allowed to SSH into the backend instances. Format is a.b.c.d/. | `string` | `null` | no |
| [stickiness\_enabled](#input\_stickiness\_enabled) | If true, enable stickiness on the target group ensuring a clients is forwarded to the same target. | `bool` | `true` | no |
| [subnets](#input\_subnets) | Subnet ids where load balancer should be present | `list(string)` | n/a | yes |
| [tags](#input\_tags) | Tags to apply to resources creatded by the module. | `map(string)` | `{}` | no |
| [target\_group\_deregistration\_delay](#input\_target\_group\_deregistration\_delay) | Time in seconds for ALB to wait before deregistering a target.
During this time, the target continues to receive existing connections
but no new connections. This allows in-flight requests to complete.
Common use cases:
- Reduce for faster deployments (e.g., 30s for stateless apps)
- Increase for long-running requests (e.g., 600s for file uploads)
Valid range: 0-3600 seconds. AWS default is 300 seconds. | `number` | `300` | no |
| [target\_group\_port](#input\_target\_group\_port) | TCP port that a target listens to to serve requests from the load balancer. | `number` | `80` | no |
| [target\_group\_protocol](#input\_target\_group\_protocol) | Protocol for the target group.
Use HTTP for standard backend communication (ALB terminates SSL).
Use HTTPS for end-to-end encryption to backend instances. | `string` | `"HTTP"` | no |
| [target\_group\_type](#input\_target\_group\_type) | Target group type: instance, ip, alb. Default is instance. | `string` | `"instance"` | no |
| [upstream\_module](#input\_upstream\_module) | Module that called this module. | `string` | `null` | no |
| [userdata](#input\_userdata) | userdata for cloud-init to provision EC2 instances | `string` | n/a | yes |
| [vanta\_contains\_ephi](#input\_vanta\_contains\_ephi) | This tag allows administrators to define whether or not a resource contains electronically Protected Health Information (ePHI). It can be set to either (true) or if they do not have ephi data (false). | `bool` | `false` | no |
| [vanta\_contains\_user\_data](#input\_vanta\_contains\_user\_data) | his tag allows administrators to define whether or not a resource contains user data (true) or if they do not contain user data (false). | `bool` | `false` | no |
| [vanta\_description](#input\_vanta\_description) | This tag allows administrators to set a description, for instance, or add any other descriptive information. | `string` | `null` | no |
| [vanta\_no\_alert](#input\_vanta\_no\_alert) | Administrators can add this tag to mark a resource as out of scope for their audit. If this tag is added, the administrator will need to set a reason for why it's not relevant to their audit. | `string` | `null` | no |
| [vanta\_owner](#input\_vanta\_owner) | The email address of the instance's owner, and it should be set to the email address of a user in Vanta. An owner will not be assigned if there is no user in Vanta with the email specified. | `string` | `null` | no |
| [vanta\_production\_environments](#input\_vanta\_production\_environments) | Environment names to consider production grade in Vanta. | `list(string)` | [
"production",
"prod"
]
| no |
| [vanta\_user\_data\_stored](#input\_vanta\_user\_data\_stored) | This tag allows administrators to describe the type of user data the instance contains. | `string` | `null` | no |
| [wait\_for\_capacity\_timeout](#input\_wait\_for\_capacity\_timeout) | How much time to wait until all instances are healthy | `string` | `"20m"` | no |
| [zone\_id](#input\_zone\_id) | Domain name zone ID where the website will be available | `string` | n/a | yes |
## Outputs
| Name | Description |
|------|-------------|
| [acm\_certificate\_arn](#output\_acm\_certificate\_arn) | ARN of the ACM certificate used by the load balancer |
| [alarm\_sns\_topic\_arn](#output\_alarm\_sns\_topic\_arn) | ARN of the SNS topic for ALB CloudWatch alarms (if created). IMPORTANT: Email subscribers must confirm their subscription via the AWS confirmation email to receive notifications. |
| [alarm\_sns\_topic\_name](#output\_alarm\_sns\_topic\_name) | Name of the SNS topic for ALB CloudWatch alarms (if created) |
| [alb\_security\_group\_id](#output\_alb\_security\_group\_id) | ID of the ALB security group |
| [asg\_arn](#output\_asg\_arn) | ARN of the created autoscaling group |
| [asg\_name](#output\_asg\_name) | Name of the created autoscaling group |
| [backend\_security\_group](#output\_backend\_security\_group) | Map with security group id and rules |
| [backend\_security\_group\_id](#output\_backend\_security\_group\_id) | ID of the backend instances security group |
| [cloudwatch\_alarm\_arns](#output\_cloudwatch\_alarm\_arns) | ARNs of CloudWatch alarms created for ALB and ASG monitoring |
| [dns\_name](#output\_dns\_name) | DNS name of the load balancer. |
| [instance\_profile\_name](#output\_instance\_profile\_name) | EC2 instance profile name. |
| [instance\_role\_arn](#output\_instance\_role\_arn) | ARN of the instance role. |
| [instance\_role\_name](#output\_instance\_role\_name) | Name of the instance role. |
| [instance\_role\_policy\_arn](#output\_instance\_role\_policy\_arn) | Policy ARN attached to EC2 instance profile. |
| [instance\_role\_policy\_attachment](#output\_instance\_role\_policy\_attachment) | Policy attachment id. |
| [instance\_role\_policy\_name](#output\_instance\_role\_policy\_name) | Policy name attached to EC2 instance profile. |
| [load\_balancer\_arn](#output\_load\_balancer\_arn) | Load Balancer ARN |
| [load\_balancer\_arn\_suffix](#output\_load\_balancer\_arn\_suffix) | Load Balancer ARN suffix for use in CloudWatch metrics dimensions. |
| [load\_balancer\_dns\_name](#output\_load\_balancer\_dns\_name) | Load balancer DNS name. |
| [load\_balancer\_security\_groups](#output\_load\_balancer\_security\_groups) | Security groups associated with the load balancer |
| [load\_balancing\_algorithm\_type](#output\_load\_balancing\_algorithm\_type) | Load balancing algorithm used by the target group (round\_robin or least\_outstanding\_requests). |
| [ssl\_listener\_arn](#output\_ssl\_listener\_arn) | SSL listener ARN |
| [target\_group\_arn](#output\_target\_group\_arn) | Target group ARN that listens to the service port. |
| [target\_group\_arn\_suffix](#output\_target\_group\_arn\_suffix) | Target group ARN suffix for use in CloudWatch metrics dimensions. |
| [zone\_id](#output\_zone\_id) | Zone id where A records are created for the service. |
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.