aws-ia / terraform-aws-control_tower_account_factory

AWS Control Tower Account Factory
Apache License 2.0
604 stars 386 forks source link

Enable AFT to use IPAM to create VPCs in new accounts #401

Open stumins opened 8 months ago

stumins commented 8 months ago

Describe the outcome you'd like

I would like to use Amazon's IP Address Manager IPAM to manage VPC's in my Control Tower managed accounts.

Is your feature request related to a problem you are currently experiencing? If so, please describe.

AFT's current behavior is to create VPC's with the same default CIDR ranges.


Originally requested by @mbuotidem in https://github.com/aws-ia/terraform-aws-control_tower_account_factory/issues/152

dennisneuman commented 8 months ago

I would also appreciate this very much.

hanafya commented 8 months ago

@dennisneuman i added your request to the existing feature request!

mikeplem commented 4 months ago

I have been able to get this working with an account customization.

I have a vpc module with this as the contents of main.tf

  filter {
    name   = "locale"
    values = [var.region]
  }

  filter {
    name   = "description"
    values = [var.ipam_description]
  }

  filter {
    name   = "address-family"
    values = ["ipv4"]
  }

  filter {
    name   = "ipam-scope-type"
    values = ["private"]
  }
}

resource "aws_vpc" "this" {

  ipv4_ipam_pool_id   = data.aws_vpc_ipam_pool.this.id
  ipv4_netmask_length = var.ipv4_netmask_length

  assign_generated_ipv6_cidr_block     = var.enable_ipv6 ? true : null
  ipv6_cidr_block                      = var.ipv6_cidr
  ipv6_ipam_pool_id                    = var.ipv6_ipam_pool_id
  ipv6_netmask_length                  = var.ipv6_netmask_length
  ipv6_cidr_block_network_border_group = var.ipv6_cidr_block_network_border_group

  instance_tenancy                     = var.instance_tenancy
  enable_dns_hostnames                 = var.enable_dns_hostnames
  enable_dns_support                   = var.enable_dns_support
  enable_network_address_usage_metrics = var.enable_network_address_usage_metrics

  tags = merge(
    var.tags,
    var.vpc_tags,
    { "Name" = var.name }
  )

  lifecycle {
    ignore_changes = [
      tags,
    ]
  }
}

This how I call the module.

module "vpc" {
  source = "./modules/vpc"

  name                = var.vpc_name
  ipam_description    = var.vpc_ipam_type
  region              = var.vpc_region
  ipv4_netmask_length = var.vpc_netmask_length

  vpc_tags = merge(
    local.vpc_tags,
    {
      "Name" = var.vpc_name,
    }
  )

  tags = local.vpc_tags

  providers = {
    aws = aws.custom
  }
}
shahbhavik01 commented 3 months ago

@mikeplem Which customizations repository did you put the VPC module and the call to it? Our IPAM is shared with the AFT account. Can you please expand on the solution?

I'm putting the VPC code in aft-account-provisioning-customizations repository but it's not able to get the IPAM pool id because the customizations are run from the new account. I also need to pass tags and other variables to the VPC module but not sure how to configure these properly. We're using Terraform Cloud on the backend.

mikeplem commented 3 months ago

@shahbhavik01 I put the VPC module in the aft-account-customizations directory.

This is what my vpc module looks like.

data "aws_vpc_ipam_pool" "this" {
  filter {
    name   = "locale"
    values = [var.region]
  }

  filter {
    name   = "description"
    values = [var.ipam_description]
  }

  filter {
    name   = "address-family"
    values = ["ipv4"]
  }

  filter {
    name   = "ipam-scope-type"
    values = ["private"]
  }
}

resource "aws_vpc" "this" {

  ipv4_ipam_pool_id   = data.aws_vpc_ipam_pool.this.id
  ipv4_netmask_length = var.ipv4_netmask_length

  assign_generated_ipv6_cidr_block     = var.enable_ipv6 ? true : null
  ipv6_cidr_block                      = var.ipv6_cidr
  ipv6_ipam_pool_id                    = var.ipv6_ipam_pool_id
  ipv6_netmask_length                  = var.ipv6_netmask_length
  ipv6_cidr_block_network_border_group = var.ipv6_cidr_block_network_border_group

  instance_tenancy                     = var.instance_tenancy
  enable_dns_hostnames                 = var.enable_dns_hostnames
  enable_dns_support                   = var.enable_dns_support
  enable_network_address_usage_metrics = var.enable_network_address_usage_metrics

  tags = var.tags

  lifecycle {
    ignore_changes = [
      tags,
    ]
  }
}

The vars.tf in the vpc module is as follows:

variable "region" {
  description = "aws region"
  type = string
  default = ""
}

variable "ipam_description" {
  description = "ipam description field value"
  type = string
  default = ""
}

variable "ipv4_netmask_length" {
  description = "netmask_length - defaults to a /24"
  type = number
  default = 24
}

# - taken from AWS VPC module
# https://github.com/terraform-aws-modules/terraform-aws-vpc/blob/master/variables.tf

variable "name" {
  description = "Name to be used on all the resources as identifier"
  type        = string
  default     = ""
}

variable "enable_ipv6" {
  description = "Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block"
  type        = bool
  default     = false
}

variable "ipv6_cidr" {
  description = "(Optional) IPv6 CIDR block to request from an IPAM Pool. Can be set explicitly or derived from IPAM using `ipv6_netmask_length`"
  type        = string
  default     = null
}

variable "ipv6_ipam_pool_id" {
  description = "(Optional) IPAM Pool ID for a IPv6 pool. Conflicts with `assign_generated_ipv6_cidr_block`"
  type        = string
  default     = null
}

variable "ipv6_netmask_length" {
  description = "(Optional) Netmask length to request from IPAM Pool. Conflicts with `ipv6_cidr_block`. This can be omitted if IPAM pool as a `allocation_default_netmask_length` set. Valid values: `56`"
  type        = number
  default     = null
}

variable "ipv6_cidr_block_network_border_group" {
  description = "By default when an IPv6 CIDR is assigned to a VPC a default ipv6_cidr_block_network_border_group will be set to the region of the VPC. This can be changed to restrict advertisement of public addresses to specific Network Border Groups such as LocalZones"
  type        = string
  default     = null
}

variable "instance_tenancy" {
  description = "A tenancy option for instances launched into the VPC"
  type        = string
  default     = "default"
}

variable "enable_dns_hostnames" {
  description = "Should be true to enable DNS hostnames in the VPC"
  type        = bool
  default     = true
}

variable "enable_dns_support" {
  description = "Should be true to enable DNS support in the VPC"
  type        = bool
  default     = true
}

variable "enable_network_address_usage_metrics" {
  description = "Determines whether network address usage metrics are enabled for the VPC"
  type        = bool
  default     = null
}

variable "tags" {
  description = "A map of tags to add to all resources"
  type        = map(string)
  default     = {}
}

The outputs.tf in the vpc module is

################################################################################
# VPC
################################################################################

output "vpc_id" {
  description = "The ID of the VPC"
  value       = try(aws_vpc.this.id, null)
}

output "vpc_arn" {
  description = "The ARN of the VPC"
  value       = try(aws_vpc.this.arn, null)
}

output "vpc_cidr_block" {
  description = "The CIDR block of the VPC"
  value       = try(aws_vpc.this.cidr_block, null)
}

output "default_security_group_id" {
  description = "The ID of the security group created by default on VPC creation"
  value       = try(aws_vpc.this.default_security_group_id, null)
}

output "default_network_acl_id" {
  description = "The ID of the default network ACL"
  value       = try(aws_vpc.this.default_network_acl_id, null)
}

output "default_route_table_id" {
  description = "The ID of the default route table"
  value       = try(aws_vpc.this.default_route_table_id, null)
}

output "vpc_instance_tenancy" {
  description = "Tenancy of instances spin up within VPC"
  value       = try(aws_vpc.this.instance_tenancy, null)
}

output "vpc_enable_dns_support" {
  description = "Whether or not the VPC has DNS support"
  value       = try(aws_vpc.this.enable_dns_support, null)
}

output "vpc_enable_dns_hostnames" {
  description = "Whether or not the VPC has DNS hostname support"
  value       = try(aws_vpc.this.enable_dns_hostnames, null)
}

output "vpc_main_route_table_id" {
  description = "The ID of the main route table associated with this VPC"
  value       = try(aws_vpc.this.main_route_table_id, null)
}

output "vpc_ipv6_association_id" {
  description = "The association ID for the IPv6 CIDR block"
  value       = try(aws_vpc.this.ipv6_association_id, null)
}

output "vpc_ipv6_cidr_block" {
  description = "The IPv6 CIDR block"
  value       = try(aws_vpc.this.ipv6_cidr_block, null)
}

output "vpc_owner_id" {
  description = "The ID of the AWS account that owns the VPC"
  value       = try(aws_vpc.this.owner_id, null)
}

Referring to my previous comment where I call aws.custom as the provider, I have the following in the pre-api-helpers.sh script. The value of VPC_REGION is queried by pulling it from the account's SSM Parameter Store.

VPC_REGION=$(aws ssm get-parameter --query 'Parameter.Value' --output text --name "/aft/account-request/custom-fields/vpc_region)

The DEFAULT_PATH and CUSTOMIZATION are already in the environment variables so I do not have to provide them.

cat << EOF > "${DEFAULT_PATH}/${CUSTOMIZATION}/terraform/custom-provider.tf"
provider "aws" {
  region = "${VPC_REGION}"
  alias  = "custom"
  assume_role {
    role_arn = "arn:aws:iam::${ACCOUNT_ID}:role/AWSAFTExecution"
  }
  default_tags {
    tags = {
      managed_by = "AFT"
      ManagedBy = "Terraform"
      GithubRepo = "https://github.com/bubblegroup/aft-account-customizations"
    }
  }
}
EOF

The value for VPC_REGION comes from the custom_fields block in the aft-account-request terraform.

  custom_fields = {
    vpc_name         = "d999999"
    vpc_ipam_type    = "dedicated"
    vpc_region       = "us-west-1"
    vpc_az_count     = 2
    use_tgw          = "yes"
  }

Another piece I also do is create a terraform.tfvars file with the necessary variables that my Terraform needs. Since Terraform looks for terraform.tfvars when it runs it will pick these up and use them automatically.

cat << EOF >> "${DEFAULT_PATH}/${CUSTOMIZATION}/terraform/terraform.tfvars"
vpc_name = "${VPC_NAME}"
vpc_ipam_type = "${IPAM_TYPE}"
vpc_region = "${VPC_REGION}"
vpc_az_count = ${VPC_AZ_COUNT}
EOF
shahbhavik01 commented 3 months ago

@mikeplem Highly appreciated. Thank you so much. I was going around in circles.