Skip to main content

NTC Core Network Migration (v1 → v2)

This guide covers the migration of terraform-aws-ntc-core-network from v1.x to v2.x.

Overview of Changes

v2.0.0 introduces major architectural improvements to simplify multi-region network configurations:

Key Changes

Featurev1.xv2.x
Provider ArchitectureAWS Provider v5 with multiple aliasesAWS Provider v6 with region attribute
Regional ConfigurationSeparate module per region + provider aliasSingle module with transit_gateways array
Multi-Region SupportMultiple module calls + provider aliasesSingle module with regional configuration arrays
Peering ManagementSeparate peering submodule per connectionIntegrated peering_requests in transit gateway config
Custom RoutesSeparate custom-routes submodule per regionSingle custom-routes submodule with region attribute per route
Module ComplexityMultiple module calls per regionTwo module calls for all regions
Resource KeysSimple keys (e.g., [0], ["tgw-core-rtb-hub"])Composite keys (e.g., ["tgw-core-frankfurt"], ["tgw-core-frankfurt/tgw-core-rtb-hub"])

Before & After Comparison

# Frankfurt region module
module "ntc_core_network_frankfurt" {
source = "github.com/...terraform-aws-ntc-core-network?ref=1.2.1"

customer_managed_prefix_lists = local.customer_managed_prefix_lists

transit_gateway = {
name = "tgw-core-frankfurt"
amazon_side_asn = 64512
route_table_names = ["tgw-core-rtb-hub", "tgw-core-rtb-spoke-prod"]
ram_share_principals = [local.workloads_ou_id]
}

transit_gateway_flow_log_destinations = [
{
destination_type = "s3"
destination_arn = local.log_bucket_arn
}
]

providers = {
aws = aws.euc1
}
}

# Zurich region module
module "ntc_core_network_zurich" {
source = "github.com/...terraform-aws-ntc-core-network?ref=1.2.1"

customer_managed_prefix_lists = local.customer_managed_prefix_lists

transit_gateway = {
name = "tgw-core-zurich"
amazon_side_asn = 64513
route_table_names = ["tgw-core-rtb-hub", "tgw-core-rtb-spoke-prod"]
ram_share_principals = [local.workloads_ou_id]
}

transit_gateway_flow_log_destinations = [
{
destination_type = "s3"
destination_arn = local.log_bucket_arn
}
]

providers = {
aws = aws.euc2
}
}

# Peering module (creator side)
module "ntc_core_network_zurich_peering" {
source = "github.com/...terraform-aws-ntc-core-network//modules/peering?ref=1.2.1"

create_transit_gateway_peering_association = true
transit_gateway_peering_association_with_route_table_id =
module.ntc_core_network_zurich.transit_gateway_route_table_ids["tgw-core-rtb-hub"]

transit_gateway_create_peerings = {
requester_transit_gateway_name = module.ntc_core_network_zurich.transit_gateway_name
requester_transit_gateway_id = module.ntc_core_network_zurich.transit_gateway_id
accepter_transit_gateways = [
module.ntc_core_network_frankfurt.transit_gateway_peering_info_for_creator
]
}

providers = {
aws = aws.euc2
}
}

# Peering module (accepter side)
module "ntc_core_network_frankfurt_peering" {
source = "github.com/...terraform-aws-ntc-core-network//modules/peering?ref=1.2.1"

create_transit_gateway_peering_association = true
transit_gateway_peering_association_with_route_table_id =
module.ntc_core_network_frankfurt.transit_gateway_route_table_ids["tgw-core-rtb-hub"]

transit_gateway_accept_peerings = [
module.ntc_core_network_zurich_peering.transit_gateway_peering_info_for_accepter["tgw-core-frankfurt"]
]

providers = {
aws = aws.euc1
}
}

# Custom routes module
module "ntc_core_network_frankfurt_custom_routes" {
source = "github.com/...terraform-aws-ntc-core-network//modules/custom-routes?ref=1.2.1"

transit_gateway_custom_routes = [
{
route_identifier = "spoke_prod_default_route_inspection_vpc"
route_table_id = module.ntc_core_network_frankfurt.transit_gateway_route_table_ids["tgw-core-rtb-spoke-prod"]
attachment_id = module.ntc_vpc_inspection.transit_gateway_vpc_attachement_id
destination_type = "cidr_block"
destination = "0.0.0.0/0"
}
]

providers = {
aws = aws.euc1
}
}

Breaking Changes

Breaking Changes

The following changes require a migration to avoid resource recreation:

  1. Module Structure: Regional modules consolidated into single module with arrays
  2. Resource Paths: All resources now use composite keys with region/TGW name prefixes
  3. Peering Management: Separate peering modules removed, integrated into transit gateway config
  4. Provider Architecture: Multi-provider aliases replaced with single provider and region attributes
  5. Prefix Lists: Now require regions array instead of being region-specific

Migration Steps

Step 1: Update Provider Configuration

Update terraform.required_providers to use AWS Provider v6:

terraform {
required_version = "~> 1.5"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

provider "aws" {
region = "eu-central-1"
alias = "euc1"
}

provider "aws" {
region = "eu-central-2"
alias = "euc2"
}

Step 2: Consolidate Module Configuration

Extract configurations from regional modules and consolidate into single module:

# Separate module per region
module "ntc_core_network_frankfurt" {
source = "github.com/...?ref=1.2.1"

customer_managed_prefix_lists = local.customer_managed_prefix_lists

transit_gateway = {
name = "tgw-core-frankfurt"
amazon_side_asn = 64512
route_table_names = ["tgw-core-rtb-hub", "tgw-core-rtb-spoke-prod"]
}

providers = { aws = aws.euc1 }
}

module "ntc_core_network_zurich" {
source = "github.com/...?ref=1.2.1"

customer_managed_prefix_lists = local.customer_managed_prefix_lists

transit_gateway = {
name = "tgw-core-zurich"
amazon_side_asn = 64513
route_table_names = ["tgw-core-rtb-hub", "tgw-core-rtb-spoke-prod"]
}

providers = { aws = aws.euc2 }
}

# Peering modules
module "ntc_core_network_zurich_peering" { ... }
module "ntc_core_network_frankfurt_peering" { ... }

# Custom routes modules (one per region)
module "ntc_core_network_frankfurt_custom_routes" { ... }
module "ntc_core_network_zurich_custom_routes" { ... }

Step 3: Add State Migration Blocks

Create a state_migrations.tf file to migrate resources without recreating them.

Multi-Region State Migrations

You'll need moved blocks for each region and each transit gateway. The examples below show Frankfurt and Zurich. Adjust the module names, region identifiers, and resource names to match your configuration.

state_migrations.tf
# =====================================================================================================================
# STATE MIGRATIONS - v1.x to v2.x
# =====================================================================================================================
# These moved blocks ensure Terraform state is updated without recreating resources
# Run: terraform plan - should show only "moved" operations (no creates/destroys)
# After successful migration, this file can be deleted

# -------------------------------------------------------------------------------------------------------------------
# CORE NETWORK - FRANKFURT (eu-central-1)
# -------------------------------------------------------------------------------------------------------------------
# Transit Gateway
moved {
from = module.ntc_core_network_frankfurt.aws_ec2_transit_gateway.ntc_core[0]
to = module.ntc_core_network.aws_ec2_transit_gateway.ntc_core["tgw-core-frankfurt"]
}

# Transit Gateway Route Tables
moved {
from = module.ntc_core_network_frankfurt.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-rtb-hub"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-frankfurt/tgw-core-rtb-hub"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-rtb-spoke-prod"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-frankfurt/tgw-core-rtb-spoke-prod"]
}

# Add moved blocks for all other route tables: spoke-dev, spoke-int, onprem, etc.

# RAM Resource Sharing
moved {
from = module.ntc_core_network_frankfurt.aws_ram_resource_share.ntc_tgw_share[0]
to = module.ntc_core_network.aws_ram_resource_share.ntc_tgw_share["tgw-core-frankfurt"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_ram_resource_association.ntc_tgw_share[0]
to = module.ntc_core_network.aws_ram_resource_association.ntc_tgw_share["tgw-core-frankfurt"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_ram_principal_association.ntc_tgw_share["ou-xxxxx-yyyyyyyy"]
to = module.ntc_core_network.aws_ram_principal_association.ntc_tgw_share["tgw-core-frankfurt/ou-xxxxx-yyyyyyyy"]
}

# Flow Logs
moved {
from = module.ntc_core_network_frankfurt.module.flow_logs["s3"].aws_flow_log.ntc_flow_logs
to = module.ntc_core_network.module.flow_logs["tgw-core-frankfurt/s3"].aws_flow_log.ntc_flow_logs
}

# Direct Connect (if configured)
moved {
from = module.ntc_core_network_frankfurt.aws_dx_gateway.ntc_dx["dx-gateway"]
to = module.ntc_core_network.aws_dx_gateway.ntc_dx["dx-gateway"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_dx_lag.ntc_dx["dx-con-frankfurt"]
to = module.ntc_core_network.aws_dx_lag.ntc_dx["eu-central-1/dx-con-frankfurt"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_dx_connection.ntc_dx["dx-con-frankfurt/1"]
to = module.ntc_core_network.aws_dx_connection.ntc_dx["eu-central-1/dx-con-frankfurt/1"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_dx_gateway_association.ntc_dx["tgw-core-frankfurt/dx-gateway"]
to = module.ntc_core_network.aws_dx_gateway_association.ntc_dx["eu-central-1/tgw-core-frankfurt/dx-gateway"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_ec2_transit_gateway_route_table_association.ntc_dx["tgw-core-frankfurt/dx-gateway"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table_association.ntc_dx["eu-central-1/tgw-core-frankfurt/dx-gateway"]
}

# VPN (if configured)
moved {
from = module.ntc_core_network_frankfurt.aws_customer_gateway.ntc_vpn["i7_frankfurt"]
to = module.ntc_core_network.aws_customer_gateway.ntc_vpn["eu-central-1/i7_frankfurt"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_vpn_connection.ntc_vpn["i7_frankfurt_vpn1"]
to = module.ntc_core_network.aws_vpn_connection.ntc_vpn["eu-central-1/i7_frankfurt_vpn1"]
}

moved {
from = module.ntc_core_network_frankfurt.aws_ec2_transit_gateway_route_table_propagation.ntc_vpn["i7_frankfurt_vpn1/tgw-core-rtb-hub"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table_propagation.ntc_vpn["eu-central-1/i7_frankfurt_vpn1/tgw-core-rtb-hub"]
}

# Add moved blocks for all VPN route table propagations

# -------------------------------------------------------------------------------------------------------------------
# CORE NETWORK - ZURICH (eu-central-2)
# -------------------------------------------------------------------------------------------------------------------
# Transit Gateway
moved {
from = module.ntc_core_network_zurich.aws_ec2_transit_gateway.ntc_core[0]
to = module.ntc_core_network.aws_ec2_transit_gateway.ntc_core["tgw-core-zurich"]
}

# Transit Gateway Route Tables
moved {
from = module.ntc_core_network_zurich.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-rtb-hub"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-zurich/tgw-core-rtb-hub"]
}

moved {
from = module.ntc_core_network_zurich.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-rtb-spoke-prod"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table.ntc_core["tgw-core-zurich/tgw-core-rtb-spoke-prod"]
}

# Add moved blocks for all other route tables

# RAM Resource Sharing
moved {
from = module.ntc_core_network_zurich.aws_ram_resource_share.ntc_tgw_share[0]
to = module.ntc_core_network.aws_ram_resource_share.ntc_tgw_share["tgw-core-zurich"]
}

moved {
from = module.ntc_core_network_zurich.aws_ram_resource_association.ntc_tgw_share[0]
to = module.ntc_core_network.aws_ram_resource_association.ntc_tgw_share["tgw-core-zurich"]
}

moved {
from = module.ntc_core_network_zurich.aws_ram_principal_association.ntc_tgw_share["ou-xxxxx-yyyyyyyy"]
to = module.ntc_core_network.aws_ram_principal_association.ntc_tgw_share["tgw-core-zurich/ou-xxxxx-yyyyyyyy"]
}

# Flow Logs
moved {
from = module.ntc_core_network_zurich.module.flow_logs["s3"].aws_flow_log.ntc_flow_logs
to = module.ntc_core_network.module.flow_logs["tgw-core-zurich/s3"].aws_flow_log.ntc_flow_logs
}

# -------------------------------------------------------------------------------------------------------------------
# CORE NETWORK - PEERING (FRANKFURT <-> ZURICH)
# -------------------------------------------------------------------------------------------------------------------
# Peering Attachment (creator side)
moved {
from = module.ntc_core_network_zurich_peering.aws_ec2_transit_gateway_peering_attachment.ntc_peering["tgw-core-frankfurt"]
to = module.ntc_core_network.aws_ec2_transit_gateway_peering_attachment.ntc_peering["tgw-core-frankfurt:tgw-core-zurich"]
}

moved {
from = module.ntc_core_network_zurich_peering.aws_ec2_transit_gateway_route_table_association.ntc_peering_creator["tgw-core-frankfurt"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table_association.ntc_peering_creator["tgw-core-frankfurt:tgw-core-zurich"]
}

# Peering Attachment (accepter side)
moved {
from = module.ntc_core_network_frankfurt_peering.aws_ec2_transit_gateway_peering_attachment_accepter.ntc_peering["tgw-core-zurich"]
to = module.ntc_core_network.aws_ec2_transit_gateway_peering_attachment_accepter.ntc_peering["tgw-core-frankfurt:tgw-core-zurich"]
}

moved {
from = module.ntc_core_network_frankfurt_peering.aws_ec2_transit_gateway_route_table_association.ntc_peering_acceptor["tgw-core-zurich"]
to = module.ntc_core_network.aws_ec2_transit_gateway_route_table_association.ntc_peering_acceptor["tgw-core-frankfurt:tgw-core-zurich"]
}

Step 4: Remove Old Module Definitions

After consolidating the configuration, remove the old module calls:

# DELETE THESE:
# module "ntc_core_network_frankfurt" { ... }
# module "ntc_core_network_zurich" { ... }
# module "ntc_core_network_frankfurt_peering" { ... }
# module "ntc_core_network_zurich_peering" { ... }

# CONSOLIDATE CUSTOM ROUTES (if used):
# Merge multiple regional custom routes modules into a single module call
# module "ntc_core_network_frankfurt_custom_routes" { ... } <- DELETE
# module "ntc_core_network_zurich_custom_routes" { ... } <- DELETE
# Create single consolidated module "ntc_core_network_custom_routes" with:
# - Add 'region' attribute to each route
# - Update output references to use new composite keys
# - Example: module.ntc_core_network.transit_gateway_route_table_ids["tgw-core-frankfurt"]["rtb-name"]

Step 5: Update Output References

Update any references to old module outputs:

# Old references
transit_gateway_id = module.ntc_core_network_frankfurt.transit_gateway_id
route_table_id = module.ntc_core_network_frankfurt.transit_gateway_route_table_ids["tgw-core-rtb-hub"]
dx_gateway_id = module.ntc_core_network_frankfurt.dx_gateway_ids_by_name["dx-gateway"]

Step 6: Initialize and Plan

# Upgrade to AWS Provider v6
terraform init -upgrade

# Review the plan
terraform plan

Expected plan output:

  • ✅ Many moved operations (state migrations)
  • ✅ Some in-place updates (configuration changes)
  • ✅ Possible additions for prefix lists in new regions
  • ✅ In-place updates for custom routes module output references (if used)
  • NO deletions or recreations of TGWs, route tables, flow logs, DX, VPN, RAM shares, or custom routes

If you see unexpected deletions or recreations, do NOT apply! Review your configuration and state migration blocks carefully.

Step 7: Apply Migration

terraform apply

Monitor the output. The migration should complete with only moved operations and minor updates.

Step 8: Clean Up

After successful migration:

# Remove the state migration file (optional, but recommended after verification)
rm state_migrations.tf

# Remove old provider aliases if no longer needed
# (Only remove if other modules don't use them)

# Delete old .tf.old backup files
rm ntc_core_network_frankfurt.tf.old ntc_core_network_zurich.tf.old

# Commit your changes
git add .
git commit -m "Migrate ntc-core-network from v1.x to v2.x"

Configuration Changes Summary

Parameter Structure Changes

v1.x Parameterv2.x ParameterNotes
transit_gateway = { }transit_gateways = [ ]Now an array of objects with region parameter
transit_gateway_flow_log_destinationsflow_log_destinations (in each TGW)Now nested within each transit gateway config
(separate peering modules)peering_requests = [ ] (in each TGW)Peering integrated into TGW config
transit_gateway_custom_routes (per region module)transit_gateway_custom_routes (with region per route)Consolidated with region attribute, updated output references
customer_managed_prefix_lists (per module)customer_managed_prefix_lists (with regions array)Now specify target regions explicitly
direct_connect = { } (per module)direct_connect = { } (with region in each item)Consolidated with region parameters
virtual_private_network = { } (per module)virtual_private_network = { } (with region in each item)Consolidated with region parameters

New Features in v2.x

  • Multi-Region in Single Module: Define all regions in one module call
  • Integrated Peering: Peering requests configured directly in transit gateway definitions
  • Explicit Region Parameters: All resources specify their target region
  • Composite Resource Keys: Better organization with hierarchical keys
  • Cleaner Output Structure: Nested maps for regional resources

Troubleshooting

Issue: Resources Being Recreated

Symptom: terraform plan shows deletions and recreations of transit gateways, route tables, or other core resources

Solution:

  1. Check your moved blocks match your v1.x module names exactly
  2. Use terraform state list | grep ntc_core_network to see current resource addresses
  3. Verify composite keys format: ["tgw-name/resource-name"] for route tables, ["region/resource-name"] for DX/VPN
  4. Ensure you've created moved blocks for all regions and all resources

Issue: Provider Configuration Errors

Symptom: "provider configuration not present" errors or "No configuration found for provider"

Solution:

  1. Ensure AWS Provider v6 is installed (terraform init -upgrade)
  2. Verify your required_providers block has version = "~> 6.0"
  3. Remove old provider aliases from v1.x (unless other modules still need them)
  4. Confirm the module no longer has providers = { aws = aws.alias } blocks

Issue: Peering Connection Shows as To Be Destroyed

Symptom: Peering attachments show as destroy/create instead of moved

Solution:

  1. Check the peering key format: ["requester-tgw:accepter-tgw"] (e.g., ["tgw-core-frankfurt:tgw-core-zurich"])
  2. Verify which module was the "creator" (requester) vs "accepter" in v1.x
  3. The creator module's peering attachment should map to aws_ec2_transit_gateway_peering_attachment
  4. The accepter module's peering attachment should map to aws_ec2_transit_gateway_peering_attachment_accepter

Issue: Custom Routes Module Configuration

Symptom: Custom routes module fails with "Invalid index" or similar errors, or multiple regional custom routes modules exist

Solution: The custom-routes submodule is still available in v2, but has two key changes:

  1. Add region attribute: Each route now requires a region parameter (e.g., region = "eu-central-1")
  2. Update module output references: Change from regional to consolidated module outputs
    • Old: module.ntc_core_network_frankfurt.transit_gateway_route_table_ids["tgw-core-rtb-spoke-prod"]
    • New: module.ntc_core_network.transit_gateway_route_table_ids["tgw-core-frankfurt"]["tgw-core-rtb-spoke-prod"]
  3. Consolidate regional modules: Merge multiple regional custom routes modules (e.g., ntc_core_network_frankfurt_custom_routes, ntc_core_network_zurich_custom_routes) into a single module call
  4. No state migration needed for custom routes - only configuration updates

Issue: Direct Connect/VPN Resources Not Found

Symptom: Error about DX or VPN resources not being found after migration

Solution:

  1. Verify the region prefix in resource keys: ["eu-central-1/resource-name"]
  2. Check that connection names match between v1.x and v2.x
  3. For DX gateway associations, ensure the key format is: ["region/tgw-name/dx-gateway-name"]
  4. Review the new configuration structure with explicit region parameters

Issue: Prefix Lists Recreated Across Regions

Symptom: Prefix lists being destroyed in some regions and created in others

Solution:

  1. Prefix lists now use a regions array - ensure all target regions are specified
  2. The key format changed from region-specific to name-based with regional deployment
  3. If you had different prefix lists per region in v1.x, you may need multiple prefix list definitions in v2.x with different regions arrays

Issue: Flow Logs Resource Path Changed

Symptom: Flow logs show as moved but to incorrect path

Solution: The flow logs path includes the TGW name and destination type now:

  • v1.x: module.ntc_core_network_frankfurt.module.flow_logs["s3"]
  • v2.x: module.ntc_core_network.module.flow_logs["tgw-core-frankfurt/s3"]

Ensure your moved blocks use the correct composite key format.

Need Help?