diff --git a/dynamic_values/subnet.tf b/dynamic_values/subnet.tf index 8701de03..58b9d6b1 100644 --- a/dynamic_values/subnet.tf +++ b/dynamic_values/subnet.tf @@ -11,14 +11,15 @@ locals { [ for value in var.subnets[zone] : { - name = value.name # Subnet shortname - prefix_name = "${var.prefix}-${value.name}" # Creates a name of the prefix and subnet name - zone = index(keys(var.subnets), zone) + 1 # Zone 1, 2, or 3 - zone_name = "${var.region}-${index(keys(var.subnets), zone) + 1}" # Contains region and zone - cidr = value.cidr # CIDR Block - no_prefix = value.no_addr_prefix # If true will not create addr prefix for subnet under any circumstance - count = index(var.subnets[zone], value) + 1 # Count of the subnet within the zone - acl = value.acl_name + name = value.name + resource_name = (var.prefix != null ? "${var.prefix}-${value.name}" : value.name) + prefix_name = "${var.prefix}-${value.name}" # Creates a name of the prefix and subnet name + zone = index(keys(var.subnets), zone) + 1 # Zone 1, 2, or 3 + zone_name = "${var.region}-${index(keys(var.subnets), zone) + 1}" # Contains region and zone + cidr = value.cidr # CIDR Block + no_prefix = value.no_addr_prefix # If true will not create addr prefix for subnet under any circumstance + count = index(var.subnets[zone], value) + 1 # Count of the subnet within the zone + acl = value.acl_name # Public gateway ID public_gateway = ( lookup(value, "public_gateway", null) == true && lookup(var.use_public_gateways, zone, null) != null @@ -34,7 +35,13 @@ locals { # Convert list to map subnet_map = { for subnet in local.subnet_list : - "${var.prefix}-${subnet.name}" => subnet + "${subnet.zone}-${subnet.name}" => merge( + subnet, + { + subnet_id = "${subnet.zone}-${subnet.name}", + resource_name = var.prefix != null ? "${var.prefix}-${subnet.name}" : subnet.name + } + ) } } diff --git a/main.tf b/main.tf index 87111b5b..7c48fd7c 100644 --- a/main.tf +++ b/main.tf @@ -440,9 +440,12 @@ locals { } resource "ibm_is_vpn_gateway" "vpn_gateway" { - for_each = local.vpn_gateway_map - name = var.prefix != null ? "${var.prefix}-${each.key}" : each.key - subnet = local.subnets["${local.vpc_name}-${each.value.subnet_name}"].id + for_each = local.vpn_gateway_map + name = var.prefix != null ? "${var.prefix}-${each.key}" : each.key + subnet = one([ + for k, s in local.subnets : + s.id if endswith(k, "-${each.value.subnet_name}") + ]) mode = each.value.mode resource_group = each.value.resource_group == null ? var.resource_group_id : each.value.resource_group tags = var.tags diff --git a/subnet.tf b/subnet.tf index e64f59db..dc6cd594 100644 --- a/subnet.tf +++ b/subnet.tf @@ -18,8 +18,8 @@ locals { resource "ibm_is_vpc_address_prefix" "subnet_prefix" { # Address prefixes replace subnet prefixes # Only create prefix if creating subnets, flag not set to disable prefix creation, and no specific prefixes were supplied - for_each = { for k, v in local.subnet_object : k => v if(v.no_prefix == false && var.create_subnets == true && length(local.address_prefixes) == 0) } - name = each.value.prefix_name + for_each = { for k, v in local.subnet_object : v.subnet_id => v if(v.no_prefix == false && var.create_subnets == true && length(local.address_prefixes) == 0) } + name = each.value.resource_name zone = each.value.zone_name vpc = local.vpc_id cidr = each.value.cidr @@ -33,12 +33,12 @@ resource "ibm_is_vpc_address_prefix" "subnet_prefix" { ############################################################################## resource "ibm_is_subnet" "subnet" { - for_each = var.create_subnets ? local.subnet_object : {} + for_each = var.create_subnets ? { for k, v in local.subnet_object : v.subnet_id => v } : {} vpc = local.vpc_id - name = each.key + name = each.value.resource_name zone = each.value.zone_name resource_group = var.resource_group_id - ipv4_cidr_block = length(keys(local.address_prefixes)) == 0 && !each.value.no_prefix ? ibm_is_vpc_address_prefix.subnet_prefix[each.value.prefix_name].cidr : each.value.cidr + ipv4_cidr_block = length(keys(local.address_prefixes)) == 0 && !each.value.no_prefix ? ibm_is_vpc_address_prefix.subnet_prefix[each.value.subnet_id].cidr : each.value.cidr network_acl = ibm_is_network_acl.network_acl[each.value.acl].id public_gateway = each.value.public_gateway tags = var.tags diff --git a/update/schematics_update_vpc_v8_to_v9.sh b/update/schematics_update_vpc_v8_to_v9.sh new file mode 100644 index 00000000..3c0cff6e --- /dev/null +++ b/update/schematics_update_vpc_v8_to_v9.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "" +echo "==============================================================" +echo " SLZ VPC v8 → v9 Migration Script for IBM Schematics" +echo "==============================================================" + +WORKSPACE_ID="" +REGION="" +RESOURCE_GROUP="" + +while getopts "w:r:g:" opt; do + case "$opt" in + w) WORKSPACE_ID="$OPTARG" ;; + r) REGION="$OPTARG" ;; + g) RESOURCE_GROUP="$OPTARG" ;; + *) ;; + esac +done + +if [[ -z "$WORKSPACE_ID" || -z "$REGION" ]]; then + echo "❌ Usage: $0 -w WORKSPACE_ID -r REGION [-g RESOURCE_GROUP]" + exit 1 +fi + +echo "Using:" +echo " Workspace : $WORKSPACE_ID" +echo " Region : $REGION" +echo " Resource Group: ${RESOURCE_GROUP:-Default}" +echo "" + +echo "🔐 Logging in..." +ibmcloud target -r "$REGION" >/dev/null + +if [[ -n "$RESOURCE_GROUP" ]]; then + ibmcloud target -g "$RESOURCE_GROUP" >/dev/null +fi + +echo "🔍 Fetching remote Schematics state..." +STATE_JSON=$(ibmcloud schematics state pull -id "$WORKSPACE_ID") + +if [[ -z "$STATE_JSON" ]]; then + echo "❌ Could not retrieve workspace state" + exit 1 +fi + +echo "🔍 Extracting subnet keys..." +SUBNET_KEYS=$(echo "$STATE_JSON" | jq -r ' + .resources[]? + | select(.type=="ibm_is_subnet") + | select(.module|contains("module.slz_vpc")) + | .instances[].index_key +') + +if [[ -z "$SUBNET_KEYS" ]]; then + echo "❌ No SLZ VPC subnets found in workspace state." + exit 1 +fi + +echo "" +echo "Found subnets:" +echo "$SUBNET_KEYS" +echo "" + +echo "🛠 Generating migration commands..." +COMMANDS_FILE="schematics_migration.txt" +: > "$COMMANDS_FILE" + +for OLD_KEY in $SUBNET_KEYS; do + ZONE_SUFFIX=$(echo "$OLD_KEY" | awk -F '-' '{print $NF}') + NEW_KEY="1-subnet-$ZONE_SUFFIX" + + printf '%s\n' \ + "ibmcloud schematics state mv -id \"$WORKSPACE_ID\" \"module.slz_vpc.ibm_is_subnet.subnet[\\\"$OLD_KEY\\\"]\" \"module.slz_vpc.ibm_is_subnet.subnet[\\\"$NEW_KEY\\\"]\"" \ + >> "$COMMANDS_FILE" + + printf '%s\n' \ + "ibmcloud schematics state mv -id \"$WORKSPACE_ID\" \"module.slz_vpc.ibm_is_vpc_address_prefix.subnet_prefix[\\\"$OLD_KEY\\\"]\" \"module.slz_vpc.ibm_is_vpc_address_prefix.subnet_prefix[\\\"$NEW_KEY\\\"]\"" \ + >> "$COMMANDS_FILE" +done + +echo "" +echo "==============================================================" +echo "Generated Schematics Migration Commands" +echo "==============================================================" +cat "$COMMANDS_FILE" + +echo "" +read -r -p "Apply these migration commands now? (y/N): " CONFIRM + +if [[ "$CONFIRM" != "y" ]]; then + echo "❌ Migration cancelled." + exit 0 +fi + +echo "" +echo "🚀 Applying migration..." +while IFS= read -r CMD; do + echo "Running: $CMD" + eval "$CMD" +done < "$COMMANDS_FILE" + +echo "" +echo "✅ Migration completed successfully!" +echo "Run 'ibmcloud schematics plan -id $WORKSPACE_ID' to verify no subnets are recreated." diff --git a/update/update_version.md b/update/update_version.md new file mode 100644 index 00000000..29fc9205 --- /dev/null +++ b/update/update_version.md @@ -0,0 +1,206 @@ +# Updating from v8 to v9 + +Version 9 of the Landing Zone VPC module changes how **subnets and VPC address prefixes are identified in Terraform state**. +Subnets and address prefixes now use a **stable, prefix-independent `resource_name`** as the Terraform state key. + +In version 9, subnets and address prefixes use a **stable, prefix-independent `resource_name`** as the Terraform state key. + +:information_source: **Important:** +Without migrating existing Terraform state, updating the `prefix` value will result in **subnet and address prefix destruction and recreation**. + +Follow the steps below to safely upgrade to version 9 **without recreating networking resources**. + +## Before you begin + +Make sure you have recent versions of these command-line prerequisites. + +- [IBM Cloud CLI](https://cloud.ibm.com/docs/cli?topic=cli-getting-started) +- [IBM Cloud CLI plug-ins](https://cloud.ibm.com/docs/cli?topic=cli-plug-ins): + - `is` plug-in (vpc-infrastructure) + - For IBM Schematics deployments: `sch` plug-in (schematics) +- JSON processor `jq` (https://jqlang.github.io/jq/) +- [Curl](). To test whether curl is installed on your system, run the following command: + + ```sh + curl -V + ``` + + If you need to install curl, see https://everything.curl.dev/install/index.html. + +## Select a procedure + +Select the procedure that matches where you deployed the code. + +- [Deployed with Schematics](#deployed-with-schematics) +- [Local Terraform](#local-terraform) + +## Deployed with Schematics + +If you deployed your IBM Cloud infrastructure by using Schematics, the `schematics_update_v8_to_v9.sh` script creates a Schematics job. [View the script](schematics_update_v8_to_v9.sh). + +### Schematics process + +1. Set the environment variables: + + 1. Set the IBM Cloud API key that has access to your IBM Cloud project or Schematics workspace. Run the following command: + + ```sh + export IBMCLOUD_API_KEY="" #pragma: allowlist secret + ``` + + Replace `` with the value of your API key. + + 1. Find your Schematics workspace ID: + - If you are using IBM Cloud Projects: + 1. Go to [Projects](https://cloud.ibm.com/projects) + 1. Select the workspace associated with your VPC deployment + 1. Click the **Configurations** tab. + 1. Click the configuration name that is associated with your VPC deployment. + 1. Under **Workspace** copy the ID. + + - If you are not using IBM Cloud Projects: + 1. Go to [Schematics Workspaces](https://cloud.ibm.com/schematics/workspaces) + 1. Select the location that the workspace is in. + 1. Select the workspace associated with your VPC deployment. + 1. Click **Settings**. + 1. Copy the **Workspace ID**. + + 1. Run the following command to set the workspace ID as an environment variable: + + ```sh + export WORKSPACE_ID="" + ``` + +1. Download the script by running this Curl command: + + ```sh + curl https://raw.githubusercontent.com/terraform-ibm-modules/terraform-ibm-landing-zone-vpc/main/update/schematics_update_v8_to_v9.sh > schematics_update_v8_to_v9.sh + ``` + +1. Identify the region where the VPC is deployed. + +1. Run the script: + + ```sh + bash schematics_update_v8_to_v9.sh \ + -w "$WORKSPACE_ID" \ + -r "" \ + [-g ""] + ``` + + - -w : Schematics workspace ID + - -r : Region where the workspace resources are deployed + - -g : (Optional) Resource group to target + + + The script: + +- Pulls the remote Schematics Terraform state +- Detects SLZ VPC subnet and address prefix resources +- Migrates state keys using `ibmcloud schematics state mv` + + + +1. Monitor the status of the job by selecting the workspace from your [Schematics workspaces dashboard](https://cloud.ibm.com/schematics/workspaces). + - When the job completes successfully, go to the next step. + - If the job fails, see [Reverting changes](#reverting-changes). + +### Apply the changes in Schematics + +1. Update your configuration to consume version 9 of the Landing Zone VPC module. +1. In Schematics, click Generate plan and verify: + + - No subnets are destroyed or re-created + - No address prefixes are destroyed or re-created + - Only in-place name updates are shown + +1. Click Apply plan. + +1. If the job is successful, follow the steps in [Clean up](#clean-up). If the job fails, see [Reverting changes](#reverting-changes). + +## Local Terraform + +If you store both the Terraform code and state file locally, run the `update_v8_to_v9.sh` script locally. [View the script](schematics_update_v8_to_v9.sh). + +1. Set the IBM Cloud API key that has access to your VPCs as an environment variable by running the following command: + + ```sh + export IBMCLOUD_API_KEY="" #pragma: allowlist secret + ``` + + Replace `` with the value of your API key. + +1. Change to the directory containing your Terraform state file. + +1. Download the script to the directory with the state file by running this Curl command: + + ```sh + curl https://raw.githubusercontent.com/terraform-ibm-modules/terraform-ibm-landing-zone-vpc/main/update/update_v8_to_v9.sh > update_v8_to_v9.sh + ``` + +1. Run the script from the directory with the state file: + + ```sh + bash update_v8_to_v9.sh + ``` + + The script: + + - Reads the local Terraform state + + - Detects prefix-based subnet and address prefix keys + + - Generates terraform state mv commands + + - Prompts for confirmation before applying changes + + +1. Initialize, check the planned changes, and apply the changes: + + + 1. Run the `terraform init` command to pull the latest version. + 1. Run the `terraform plan` command to make sure that none of the VPC resources will be re-created. + + - You should see in-place updates to names. No resources should be set to be destroyed or re-created. + + 1. Run `terraform apply` to upgrade to the 9 version of the module and apply the update in place. + 1. If the commands are successful, follow the steps in [Clean up](#clean-up). + +### Expected Terraform plan after migration + +#### After migrating the state and updating the prefix value: + +- Subnets are updated in place + +- Address prefixes are updated in place + +- No CIDR changes occur + +- No subnet or address prefix resources are re-created + + +```sh +Example +~ name = "testvpc-vpc-subnet-a" -> "testvpc1-vpc-subnet-a" +``` + +### Clean up + +After a successful migration, remove temporary files created by the script: + +```sh +rm moved.json revert.json +``` + +## Reverting changes + +If the script fails, run the script again with the `-z` option to undo the changes. The script uses the `revert.json` file that was created when you ran the script without the `-z` option. + +```sh +bash schematics_update_v8_to_v9.sh -z +``` + +- If you ran the job in Schematics, a new workspace job reverts the state to what existed before you ran the script initially. +- If your code and state file are on your computer, the script reverts changes to the local Terraform state file. + +:exclamation: **Important:** After you revert the changes, don't run any other steps in this process. Create an IBM Cloud support case and include information about the script and errors. For more information, see [Creating support cases](https://cloud.ibm.com/docs/get-support?topic=get-support-open-case&interface=ui). diff --git a/update/update_vpc_v8_to_v9.sh b/update/update_vpc_v8_to_v9.sh new file mode 100755 index 00000000..68b14aa5 --- /dev/null +++ b/update/update_vpc_v8_to_v9.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "" +echo "==============================================================" +echo " SLZ VPC v3 → v4 Migration Script (Subnet Key Migration)" +echo "==============================================================" +echo "" + +STATE_JSON=$(terraform show -json 2>/dev/null || true) + +if [[ -z "$STATE_JSON" || "$STATE_JSON" == "null" ]]; then + echo "❌ ERROR: Terraform state could not be loaded." + echo "Run this from the directory containing terraform.tfstate." + exit 1 +fi + +extract_resources() { + echo "$STATE_JSON" | jq -r ' + .values.root_module.child_modules[] + | select(.address == "module.slz_vpc") + | .resources[] + ' +} + +echo "🔍 Scanning state for SLZ VPC subnets..." +SUBNETS=$(extract_resources | jq -r ' + select(.type == "ibm_is_subnet") + | .address +') + +if [[ -z "$SUBNETS" ]]; then + echo "❌ No SLZ subnets found in state. Nothing to migrate." + exit 1 +fi + +echo "" +echo "Found subnet resources:" +echo "$SUBNETS" +echo "" + +generate_new_key() { + local old="$1" + + local letter + letter=$(echo "$old" | sed -E 's/.*subnet-([a-z])/\1/') + + local zone + zone=$(echo "$STATE_JSON" | jq -r \ + --arg old "$old" ' + .values.root_module.child_modules[] + | select(.address == "module.slz_vpc") + | .resources[] + | select(.type == "ibm_is_subnet") + | select(.address | endswith($old + "\"]")) + | .values.zone + ') + + local zone_num + zone_num=$(echo "$zone" | sed -E 's/.*-([0-9]+)$/\1/') + + echo "${zone_num}-subnet-${letter}" +} + +MOVED_COMMANDS=() + +echo "🛠 Generating terraform state mv commands..." +echo "" + +for subnet in $SUBNETS; do + OLD_KEY=$(echo "$subnet" | sed -E 's/.*subnet\["([^"]+)"\].*/\1/') + NEW_KEY=$(generate_new_key "$OLD_KEY") + + echo "➡ Subnet: $OLD_KEY → $NEW_KEY" + + MOVED_COMMANDS+=( + "terraform state mv \"module.slz_vpc.ibm_is_subnet.subnet[\\\"$OLD_KEY\\\"]\" \"module.slz_vpc.ibm_is_subnet.subnet[\\\"$NEW_KEY\\\"]\"" + ) + + MOVED_COMMANDS+=( + "terraform state mv \"module.slz_vpc.ibm_is_vpc_address_prefix.subnet_prefix[\\\"$OLD_KEY\\\"]\" \"module.slz_vpc.ibm_is_vpc_address_prefix.subnet_prefix[\\\"$NEW_KEY\\\"]\"" + ) +done + +echo "" +echo "==============================================================" +echo "Generated Migration Commands" +echo "==============================================================" +printf "%s\n" "${MOVED_COMMANDS[@]}" +echo "" + +read -r -p "Apply these migration commands? (y/N): " confirm +if [[ "$confirm" != "y" ]]; then + echo "❌ Migration aborted by user." + exit 0 +fi + +echo "" +echo "🚀 Applying migration..." +for cmd in "${MOVED_COMMANDS[@]}"; do + echo "Running: $cmd" + eval "$cmd" +done + +echo "" +echo "✅ Migration completed successfully!" +echo "Run: terraform plan (should show NO subnet recreation)"