Skip to content

Latest commit

Β 

History

History
759 lines (628 loc) Β· 34 KB

File metadata and controls

759 lines (628 loc) Β· 34 KB

Logo

Ben's Terraform AWS Account Defaults Module

This is how I do it.

Explore the docs Β»

Report Bug . Request Feature

GitHub contributors GitHub issues GitHub pull requests GitHub workflow: Terratest GitHub workflow: Linting GitHub tag (with filter) OSSF-Scorecard Score GitHub License

About The Project

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.

Defaults

  • EC2 Serial Console Access ENABLED by Default
  • EBS Default KMS Key set to the AWS Managed KMS Key
  • EBS Encryption ENABLED by Default
  • ECS EC2 Provisioned Trunking DISABLED by Default
  • ECS Container Insights ENABLED by Default
  • 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

Usage

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
}

Example Network with Public Subnets Only

infracost

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        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛

Example Network with Public & Private Subnets without NAT

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

infracost

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        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛

Example Network with Public & Private Subnets and a Single NAT

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

infracost

🚨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          ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛

Example Network with Public & Private Subnets and a NAT per AZ

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

infracost

🚨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         ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛

IPv6 Support

This module now supports IPv6 networking through the ip_mode variable in the network configuration. Three modes are available:

IPv4 Only (Default)

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.

Dual-Stack (IPv4 + IPv6)

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 /56 IPv6 CIDR block to the VPC
  • Subnets receive both IPv4 and IPv6 addresses
  • Public subnets get /64 IPv6 CIDR blocks (prefixes 0, 1, 2, ...)
  • Private subnets get /64 IPv6 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

IPv6-Only

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 (public and private) are optional (default to null) and are only used in ipv4 or dual-stack modes. In ipv6-only mode they can be omitted entirely β€” the VPC module sets cidr_block = null for IPv6-native subnets.

Migration Guide

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" to ip_mode = "ipv6-only" you must:

  1. First migrate to dual-stack (ip_mode = "dual-stack")
  2. Then switch to ip_mode = "ipv6-only" -- this step will recreate your subnets and any resources inside them

Plan for a maintenance window and workload migration before attempting this.

Migrating from IPv4-only to dual-stack:

  1. Add ip_mode = "dual-stack" to your network configuration
  2. Run terraform plan to review changes
  3. 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:

⚠️ This is a breaking change that will recreate subnets:

  1. Update ip_mode = "ipv6-only"
  2. Set enable_nat = false (NAT gateways not used with IPv6-only)
  3. 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
  4. 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

IPv6 Outputs

When IPv6 is enabled (dual-stack or IPv6-only), additional outputs are available:

  • vpc_ipv6_cidr_block - The IPv6 CIDR block assigned to the VPC
  • vpc_public_subnet_ipv6_cidr_blocks - List of IPv6 CIDR blocks for public subnets
  • vpc_private_subnet_ipv6_cidr_blocks - List of IPv6 CIDR blocks for private subnets
  • vpc_egress_only_internet_gateway_id - ID of the egress-only Internet Gateway

Version Constraints

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.

Requirements

Name Version
terraform >= 1.3.0
aws ~> 6.0

Providers

Name Version
aws ~> 6.0

Modules

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

Resources

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

Inputs

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({
attributes = list(string)
dns_namespace = string
environment = string
instance = string
instance_short = string
namespace = string
region = string
region_short = string
role = string
role_short = string
project = string
tags = map(string)
})
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({
cidr = string
enable_nat = optional(bool, false)
one_nat = optional(bool, true)
enable_private = optional(bool, false)
ip_mode = optional(string, "ipv4")
subnets = list(object({
az = string
public = optional(string)
private = optional(string)
}))
})
{
"cidr": "0.0.0.0/0",
"subnets": [
{
"az": "us-east-1a",
"public": "0.0.0.0/0"
}
]
}
no

Outputs

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

Roadmap

GitHub issues

See the open issues for a list of proposed features (and known issues).

Contributing

GitHub pull requests

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.

Creating A Pull Request

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

GitHub License

Distributed under the MIT License. See LICENSE for more information.

Authors

GitHub contributors

  • Benjamin R. Doerr - Terraformer - Benjamin R. Doerr - Built Ben's Terraform Modules

Supported Versions

Only the latest tagged version is supported.

Reporting a Vulnerability

See SECURITY.md.

Acknowledgements