This is how I do it.
Explore the docs »
Report Bug
.
Request Feature
Ben's Terraform AWS Account Defaults Module. Configures various defaults and network for your AWS account. Non-optionally sets basic account-wide billing alerts to help avoid run-away costs.
- EC2 Serial Console Access ENABLED by Default
- EBS Default KMS Key set to the AWS Managed KMS Key
- EBS Encryption ENABLED by Default
- 💸 Note potential costs from KMS @ $0.03 per 10,000 requests
- ECS EC2 Provisioned Trunking DISABLED by Default
- ECS Container Insights ENABLED by Default
- 💸 Note potential costs from CloudWatch custom Metrics @ $5.40/Service/Month
- IAM Allow Users to Change their own Password ENABLED
- IAM Hard Password Expiry DISABLED
- IAM Max Password Age 90 Days
- IAM Min Password Length 32
- IAM Password Reuse Prevention for the last 5 passwords
- IAM Password Complexity lowercase, uppercase, numbers, symbols all required
- VPC User-specified network
- 💸🚨Possible configuration costs from NAT Gateways @ $32.85/nat/month. See more in the usage and cost sections below.
- 💸 Note potential costs from IPv4 Addresses
The basic defaults are straightforward with variations on network configuration presenting various options. See the Costs section for the implications of network topology on cost.
module "context" {
source = "bendoerr-terraform-modules/context/null"
version = "xxx"
namespace = "btm"
role = "production"
region = "us-east-1"
project = "defaults"
}
module "defaults" {
source = "bendoerr-terraform-modules/defaults/aws"
version = "xxx"
context = module.context.shared
budget_monthly_limit = 10.00
budget_alert_emails = "alerts@example.com"
iam_alias_postfix = "core"
network = {
idr = "10.10.0.0/16"
enable_nat = false
one_nat = false
enable_private = false
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = ""
},
]
}
}
output "vpc_id" {
value = module.defaults.vpc_id
}
output "vpc_public_subnet_ids" {
value = module.defaults.vpc_public_subnet_ids
}For cost, this is my default network configuration. Each ENI assigned within this subnet will have an associated IPv4 address attached to it as well. At the moment this is the most cost-effective solution as there is no charge for active IPv4 address. 🚨 However, starting in February 2024 active IPv4 addresses will incur a $0.005/hour charge.
network = {
cidr = "10.10.0.0/16"
enable_nat = false
one_nat = false
enable_private = false
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = ""
},
{
az = "us-east-1b"
public = "10.10.2.0/24"
private = ""
},
{
az = "us-east-1c"
public = "10.10.3.0/24"
private = ""
},
{
az = "us-east-1d"
public = "10.10.4.0/24"
private = ""
},
{
az = "us-east-1e"
public = "10.10.5.0/24"
private = ""
},
{
az = "us-east-1f"
public = "10.10.6.0/24"
private = ""
},
]
}At a base level this configuration incurs no cost. However, take note of future IPv4 active addresses beginning to cost mention above.
Important Data Transfer Costs to Keep in Mind: While the VPC at rest does not cost, be sure to estimate Data Transfer OUT from AWS to the Internet which is charged at $0.09/GB beyond the first 100GB/per customer for the first 10TB/month. Also note that IPv4 data transferred between Availability Zones within the same region cost $0.01/GB in each direction. IPv6 only incurs this cost if transferred to a different VPC.
Project: With Public Subnets Only
Module path: examples/complete
Name Monthly Qty Unit Monthly Cost
OVERALL TOTAL $0.00
──────────────────────────────────
27 cloud resources were detected:
∙ 0 were estimated
∙ 25 were free, rerun with --show-skipped to see details
∙ 2 are not supported yet, rerun with --show-skipped to see details
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Project ┃ Monthly cost ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ With Public Subnets Only ┃ $0.00 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛
If for some reason you would like private subnets without access to the internet this configuration can achieve that.
network = {
cidr = "10.10.0.0/16"
enable_nat = false
one_nat = false
enable_private = true
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = "10.10.16.0/20"
},
{
az = "us-east-1b"
public = "10.10.2.0/24"
private = "10.10.32.0/20"
},
{
az = "us-east-1c"
public = "10.10.3.0/24"
private = "10.10.48.0/20"
},
{
az = "us-east-1d"
public = "10.10.4.0/24"
private = "10.10.64.0/20"
},
{
az = "us-east-1e"
public = "10.10.5.0/24"
private = "10.10.80.0/20"
},
{
az = "us-east-1f"
public = "10.10.6.0/24"
private = "10.10.96.0/20"
},
]
}At a base level this configuration incurs no cost. However, take note of future IPv4 active addresses beginning to cost mention above.
Important Data Transfer Costs to Keep in Mind: While the VPC at rest does not cost, be sure to estimate Data Transfer OUT from AWS to the Internet which is charged at $0.09/GB beyond the first 100GB/per customer for the first 10TB/month. Also note that IPv4 data transferred between Availability Zones within the same region cost $0.01/GB in each direction. IPv6 only incurs this cost if transferred to a different VPC.
Project: With Public & Private Subnets no/NAT
Module path: examples/complete
Name Monthly Qty Unit Monthly Cost
OVERALL TOTAL $0.00
──────────────────────────────────
40 cloud resources were detected:
∙ 0 were estimated
∙ 38 were free, rerun with --show-skipped to see details
∙ 2 are not supported yet, rerun with --show-skipped to see details
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Project ┃ Monthly cost ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ With Public & Private Subnets no/NAT ┃ $0.00 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛
This is a good starter Public/Private network topology which will use a single NAT in the first availability zone for all subnets. If you are spreading your workload across multiple availability zones for fault tolerance note that in this configuration the NAT Gateway becomes a single point of failure for outbound traffic.
network = {
cidr = "10.10.0.0/16"
enable_nat = true
one_nat = true
enable_private = true
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = "10.10.16.0/20"
},
{
az = "us-east-1b"
public = "10.10.2.0/24"
private = "10.10.32.0/20"
},
{
az = "us-east-1c"
public = "10.10.3.0/24"
private = "10.10.48.0/20"
},
{
az = "us-east-1d"
public = "10.10.4.0/24"
private = "10.10.64.0/20"
},
{
az = "us-east-1e"
public = "10.10.5.0/24"
private = "10.10.80.0/20"
},
{
az = "us-east-1f"
public = "10.10.6.0/24"
private = "10.10.96.0/20"
},
]
}🚨Using a NAT Gateway costs about $32.85/month to exist. Additionally, NAT Gateway's charge $0.045/1 GB data processed. There is no charge between the NAT Gateway and resources in the same availability zone, however data transfers between the NAT Gateway and resources in different availability zones do incur standard EC2 data transfer charges.
This cost example assumes 50GB of processed data at the NAT and that 4/5ths of that data is being distributed to other availability zones.
Project: With Public & Private Subnets with one NAT
Module path: examples/complete
Name Monthly Qty Unit Monthly Cost
aws_data_transfer.my_region
└─ Intra-region data transfer 80 GB $0.80
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[0]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
OVERALL TOTAL $35.90
──────────────────────────────────
44 cloud resources were detected:
∙ 2 were estimated, all of which include usage-based costs, see https://infracost.io/usage-file
∙ 40 were free, rerun with --show-skipped to see details
∙ 2 are not supported yet, rerun with --show-skipped to see details
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Project ┃ Monthly cost ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ With Public & Private Subnets with one NAT ┃ $36 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛
This is the more robust Public/Private topology that allows for failed regions to not take down NATing.
network = {
cidr = "10.10.0.0/16"
enable_nat = true
one_nat = false
enable_private = true
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = "10.10.16.0/20"
},
{
az = "us-east-1b"
public = "10.10.2.0/24"
private = "10.10.32.0/20"
},
{
az = "us-east-1c"
public = "10.10.3.0/24"
private = "10.10.48.0/20"
},
{
az = "us-east-1d"
public = "10.10.4.0/24"
private = "10.10.64.0/20"
},
{
az = "us-east-1e"
public = "10.10.5.0/24"
private = "10.10.80.0/20"
},
{
az = "us-east-1f"
public = "10.10.6.0/24"
private = "10.10.96.0/20"
},
]
}🚨Using a NAT Gateway costs about $32.85/month to exist. Additionally, NAT Gateway's charge $0.045/1 GB data processed. There is no charge between the NAT Gateway and resources in the same availability zone, however data transfers between the NAT Gateway and resources in different availability zones do incur standard EC2 data transfer charges.
This cost example assumes 50GB of processed data at the NAT without any need for inter-region data transfer.
Project: With Public & Private Subnets with NAT per AZ
Module path: examples/complete
Name Monthly Qty Unit Monthly Cost
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[0]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[1]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[2]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[3]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[4]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
module.aws_defaults.module.vpc_default.aws_nat_gateway.this[5]
├─ NAT gateway 730 hours $32.85
└─ Data processed 50 GB $2.25
OVERALL TOTAL $210.60
──────────────────────────────────
63 cloud resources were detected:
∙ 6 were estimated, all of which include usage-based costs, see https://infracost.io/usage-file
∙ 55 were free, rerun with --show-skipped to see details
∙ 2 are not supported yet, rerun with --show-skipped to see details
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Project ┃ Monthly cost ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ With Public & Private Subnets with NAT per AZ ┃ $211 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛
This module now supports IPv6 networking through the ip_mode variable in the
network configuration. Three modes are available:
network = {
cidr = "10.10.0.0/16"
enable_nat = false
one_nat = false
enable_private = false
ip_mode = "ipv4" # Default, can be omitted
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = ""
},
]
}This is the default behavior with no changes to existing configurations. IPv6 is not enabled and only IPv4 addresses are used.
network = {
cidr = "10.10.0.0/16"
enable_nat = false
one_nat = false
enable_private = true
ip_mode = "dual-stack"
subnets = [
{
az = "us-east-1a"
public = "10.10.1.0/24"
private = "10.10.11.0/24"
},
{
az = "us-east-1b"
public = "10.10.2.0/24"
private = "10.10.12.0/24"
},
]
}In dual-stack mode:
- AWS automatically assigns a
/56IPv6 CIDR block to the VPC - Subnets receive both IPv4 and IPv6 addresses
- Public subnets get
/64IPv6 CIDR blocks (prefixes 0, 1, 2, ...) - Private subnets get
/64IPv6 CIDR blocks (prefixes starting after public subnets) - Instances automatically receive IPv6 addresses on creation
- Egress-only Internet Gateway is created for private subnet IPv6 traffic
- NAT gateway behavior is unchanged for IPv4 traffic
network = {
cidr = "10.20.0.0/16"
enable_nat = false # Forced to false in IPv6-only mode regardless of user value
one_nat = false # Forced to false in IPv6-only mode regardless of user value
enable_private = true
ip_mode = "ipv6-only"
subnets = [
{
az = "us-east-1a"
},
{
az = "us-east-1b"
},
]
}In IPv6-only mode:
- Subnets are configured with
ipv6_native = true— EC2 instances receive IPv6 addresses via DHCPv6 and do not require private IPv4 addresses. The VPC itself still has an IPv4 CIDR block (AWS does not allow removing it), but subnets in this mode do not assign IPv4 addresses to instances on creation. - NAT gateways are automatically turned off (not needed for IPv6)
- Egress-only Internet Gateway handles outbound IPv6 traffic
- No IPv4 public address costs
- Note: IPv4 CIDRs (
publicandprivate) are optional (default tonull) and are only used inipv4ordual-stackmodes. Inipv6-onlymode they can be omitted entirely — the VPC module setscidr_block = nullfor IPv6-native subnets.
Warning: There is NO in-place migration path from IPv4-only subnets to IPv6-only subnets. AWS does not support converting an existing IPv4-only subnet to an IPv6-native subnet in place.
To migrate from
ip_mode = "ipv4"toip_mode = "ipv6-only"you must:
- First migrate to dual-stack (
ip_mode = "dual-stack")- Then switch to
ip_mode = "ipv6-only"-- this step will recreate your subnets and any resources inside themPlan for a maintenance window and workload migration before attempting this.
Migrating from IPv4-only to dual-stack:
- Add
ip_mode = "dual-stack"to your network configuration - Run
terraform planto review changes - Apply changes - this is a non-breaking change:
- Existing IPv4 functionality is preserved
- IPv6 CIDR blocks are added to VPC and subnets
- New resources will receive both IPv4 and IPv6 addresses
Migrating from dual-stack to IPv6-only:
- Update
ip_mode = "ipv6-only" - Set
enable_nat = false(NAT gateways not used with IPv6-only) - Understand that:
- Existing resources in subnets will need to be recreated
- Instances will no longer receive private IPv4 addresses on launch (subnet IPv4 CIDR blocks are retained; AWS does not allow removing them)
- Only IPv6 connectivity will be available
- Plan for workload migration or maintenance window
Rolling back:
To revert from dual-stack or IPv6-only back to IPv4-only:
- Change
ip_mode = "ipv4" - For IPv6-only → IPv4, expect subnet recreation
- For dual-stack → IPv4, IPv6 addresses are removed but subnets remain
When IPv6 is enabled (dual-stack or IPv6-only), additional outputs are available:
vpc_ipv6_cidr_block- The IPv6 CIDR block assigned to the VPCvpc_public_subnet_ipv6_cidr_blocks- List of IPv6 CIDR blocks for public subnetsvpc_private_subnet_ipv6_cidr_blocks- List of IPv6 CIDR blocks for private subnetsvpc_egress_only_internet_gateway_id- ID of the egress-only Internet Gateway
This module uses pessimistic version constraints (~>) for its providers to
ensure predictable behavior across deployments:
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0" # Allows 6.x, prevents 7.0
}
}Why pessimistic constraints?
- Prevents unexpected breaking changes from major provider updates
- Ensures consistent behavior across environments
- Makes upgrade impact predictable and controllable
When AWS provider v7.0 releases, this module will require an update to support it. That is intentional — we prefer explicit, tested upgrades over automatic major version bumps.
For consuming this module, you can use any AWS provider version that satisfies both your requirements and this module's constraints. Terraform's dependency resolver will find a compatible version automatically.
| Name | Version |
|---|---|
| terraform | >= 1.3.0 |
| aws | ~> 6.0 |
| Name | Version |
|---|---|
| aws | ~> 6.0 |
| Name | Source | Version |
|---|---|---|
| iam_account | terraform-aws-modules/iam/aws//modules/iam-account | 6.8.0 |
| label_account_alias | bendoerr-terraform-modules/label/null | 1.0.1 |
| label_monthly_total | bendoerr-terraform-modules/label/null | 1.0.1 |
| label_network | bendoerr-terraform-modules/label/null | 1.0.1 |
| vpc_default | terraform-aws-modules/vpc/aws | 6.6.1 |
| Name | Type |
|---|---|
| aws_budgets_budget.monthly_total | resource |
| aws_ebs_default_kms_key.default | resource |
| aws_ebs_encryption_by_default.default | resource |
| aws_ec2_serial_console_access.default | resource |
| aws_ecs_account_setting_default.awsvpc_trunking | resource |
| aws_ecs_account_setting_default.container_insights | resource |
| aws_kms_alias.ebs | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| budget_alert_emails | n/a | set(string) |
n/a | yes |
| budget_monthly_limit | n/a | string |
n/a | yes |
| context | Shared Context from Ben's terraform-null-context | object({ |
n/a | yes |
| iam_alias_postfix | n/a | string |
n/a | yes |
| network | Network configuration for VPC. ip_mode can be 'ipv4' (default), 'dual-stack', or 'ipv6-only'. NAT defaults are cost-optimized, not HA-optimized: enable_nat=false, one_nat=true. When a caller flips enable_nat=true and leaves one_nat unset, they get a single shared NAT gateway (no cross-AZ redundancy in the NAT path). Set one_nat=false to get one NAT gateway per AZ. |
object({ |
{ |
no |
| Name | Description |
|---|---|
| aws_budgets_budget_monthly_total_account | n/a |
| aws_budgets_budget_monthly_total_name | n/a |
| vpc_azs | n/a |
| vpc_egress_only_internet_gateway_id | The ID of the egress-only Internet Gateway |
| vpc_id | n/a |
| vpc_ipv6_cidr_block | The IPv6 CIDR block assigned to the VPC |
| vpc_private_subnet_ids | n/a |
| vpc_private_subnet_ipv6_cidr_blocks | List of IPv6 CIDR blocks for private subnets |
| vpc_public_subnet_ids | n/a |
| vpc_public_subnet_ipv6_cidr_blocks | List of IPv6 CIDR blocks for public subnets |
See the open issues for a list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
- If you have suggestions for adding or removing projects, feel free to open an issue to discuss it, or directly create a pull request after you edit the README.md file with necessary changes.
- Please make sure you check your spelling and grammar.
- Create individual PR for each suggestion.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Distributed under the MIT License. See LICENSE for more information.
- Benjamin R. Doerr - Terraformer - Benjamin R. Doerr - Built Ben's Terraform Modules
Only the latest tagged version is supported.
See SECURITY.md.