Tuesday, April 14, 2026

 This is a runbook for migrating GenAI workload comprising of AKS server and langfuse namespaces from one region to another:

1. Step 1: export rg with aztfexport:

#!/usr/bin/env bash

set -euo pipefail

# ---- CONFIG ----

SOURCE_SUBSCRIPTION_ID="<SOURCE_SUBSCRIPTION_ID>"

SOURCE_RG="<SOURCE_GENAI_RG>" # e.g., rg-genai-aks

SOURCE_LOCATION="<SOURCE_REGION>" # e.g., westus2

TARGET_SUBSCRIPTION_ID="<TARGET_SUBSCRIPTION_ID>"

TARGET_LOCATION="eastus2"

SUFFIX="eus2"

TARGET_RG="${SOURCE_RG}-${SUFFIX}"

EXPORT_DIR="./tfexport-${SOURCE_RG}-${SUFFIX}"

# ---- EXPORT FROM SOURCE ----

az account set --subscription "${SOURCE_SUBSCRIPTION_ID}"

mkdir -p "${EXPORT_DIR}"

echo "Exporting all resources from ${SOURCE_RG} using aztfexport..."

aztfexport group \

  --resource-group "${SOURCE_RG}" \

  --output-directory "${EXPORT_DIR}" \

  --append

echo "Export complete: ${EXPORT_DIR}"

# ---- CREATE TARGET RG ----

az account set --subscription "${TARGET_SUBSCRIPTION_ID}"

echo "Creating target RG ${TARGET_RG} in ${TARGET_LOCATION}..."

az group create \

  --name "${TARGET_RG}" \

  --location "${TARGET_LOCATION}" \

  --output none

# ---- REWRITE TF FOR DR ----

echo "Rewriting Terraform for ${TARGET_LOCATION} and -${SUFFIX} names..."

find "${EXPORT_DIR}" -type f -name "*.tf" | while read -r FILE; do

  # Change region

  sed -i "s/\"${SOURCE_LOCATION}\"/\"${TARGET_LOCATION}\"/g" "${FILE}"

  # Append suffix to resource names (simple heuristic; review before apply)

  sed -i -E "s/(name *= *\"[a-zA-Z0-9_-]+)\"/\1-${SUFFIX}\"/g" "${FILE}"

  # Retarget RG references

  sed -i "s/\"${SOURCE_RG}\"/\"${TARGET_RG}\"/g" "${FILE}"

done

echo "Rewrite done. Review ${EXPORT_DIR} and then:"

echo " cd ${EXPORT_DIR}"

echo " terraform init && terraform apply"

2. Step 2: migrate namespaces and workloads:

#!/usr/bin/env bash

set -euo pipefail

# ---- CONFIG ----

SOURCE_SUBSCRIPTION_ID="<SOURCE_SUBSCRIPTION_ID>"

TARGET_SUBSCRIPTION_ID="<TARGET_SUBSCRIPTION_ID>"

SRC_AKS_RG="<SRC_AKS_RG>"

SRC_AKS_NAME="<SRC_AKS_NAME>"

DST_AKS_RG="<DST_AKS_RG>"

DST_AKS_NAME="<DST_AKS_NAME>"

# Namespaces to exclude (system)

EXCLUDE_NS_REGEX="^(kube-system|kube-public|kube-node-lease|gatekeeper-system|azure-arc|default)$"

# ---- GET CONTEXTS ----

echo "Getting kubeconfig for source AKS..."

az account set --subscription "${SOURCE_SUBSCRIPTION_ID}"

az aks get-credentials -g "${SRC_AKS_RG}" -n "${SRC_AKS_NAME}" --overwrite-existing

SRC_CONTEXT=$(kubectl config current-context)

echo "Getting kubeconfig for destination AKS..."

az account set --subscription "${TARGET_SUBSCRIPTION_ID}"

az aks get-credentials -g "${DST_AKS_RG}" -n "${DST_AKS_NAME}" --overwrite-existing

DST_CONTEXT=$(kubectl config current-context)

echo "Source context: ${SRC_CONTEXT}"

echo "Destination context: ${DST_CONTEXT}"

echo ""

# ---- EXPORT NAMESPACES & WORKLOADS FROM SOURCE ----

EXPORT_DIR="./aks-migration-eus2"

mkdir -p "${EXPORT_DIR}"

kubectl config use-context "${SRC_CONTEXT}"

echo "Exporting namespaces and workloads from source cluster..."

NAMESPACES=$(kubectl get ns -o jsonpath='{.items[*].metadata.name}')

for NS in ${NAMESPACES}; do

  if [[ "${NS}" =~ ${EXCLUDE_NS_REGEX} ]]; then

    echo "Skipping system namespace: ${NS}"

    continue

  fi

  NS_DIR="${EXPORT_DIR}/${NS}"

  mkdir -p "${NS_DIR}"

  echo "Exporting namespace: ${NS}"

  # Namespace definition

  kubectl get ns "${NS}" -o yaml > "${NS_DIR}/namespace.yaml"

  # Core workload types (adjust as needed)

  for KIND in deployment statefulset daemonset service configmap secret ingress cronjob job; do

    kubectl get "${KIND}" -n "${NS}" -o yaml > "${NS_DIR}/${KIND}.yaml" || true

  done

done

echo "Export complete: ${EXPORT_DIR}"

# ---- APPLY TO DESTINATION CLUSTER ----

kubectl config use-context "${DST_CONTEXT}"

echo "Applying namespaces and workloads to destination cluster..."

for NS in ${NAMESPACES}; do

  if [[ "${NS}" =~ ${EXCLUDE_NS_REGEX} ]]; then

    continue

  fi

  NS_DIR="${EXPORT_DIR}/${NS}"

  if [[ ! -d "${NS_DIR}" ]]; then

    continue

  fi

  echo "Creating namespace: ${NS}"

  kubectl apply -f "${NS_DIR}/namespace.yaml" || true

  for KIND in deployment statefulset daemonset service configmap secret ingress cronjob job; do

    FILE="${NS_DIR}/${KIND}.yaml"

    if [[ -s "${FILE}" ]]; then

      echo "Applying ${KIND} in ${NS}"

      kubectl apply -n "${NS}" -f "${FILE}"

    fi

  done

done

echo "AKS namespace/workload migration complete."

3. Step 3: find storage accounts with aks subnet allows and migrate data

#!/usr/bin/env bash

set -euo pipefail

# ---- CONFIG ----

SOURCE_SUBSCRIPTION_ID="<SOURCE_SUBSCRIPTION_ID>"

TARGET_SUBSCRIPTION_ID="<TARGET_SUBSCRIPTION_ID>"

SRC_AKS_RG="<SRC_AKS_RG>"

SRC_AKS_NAME="<SRC_AKS_NAME>"

SUFFIX="eus2"

# ---- GET AKS VNET/SUBNETS ----

az account set --subscription "${SOURCE_SUBSCRIPTION_ID}"

AKS_INFO=$(az aks show -g "${SRC_AKS_RG}" -n "${SRC_AKS_NAME}")

# For Azure CNI with custom VNet:

VNET_SUBNET_IDS=$(echo "${AKS_INFO}" | jq -r '.agentPoolProfiles[].vnetSubnetId' | sort -u)

echo "AKS subnets:"

echo "${VNET_SUBNET_IDS}"

echo ""

# ---- FIND MATCHING STORAGE ACCOUNTS ----

echo "Finding storage accounts whose network rules allow these subnets..."

STORAGE_ACCOUNTS=$(az storage account list --query "[].id" -o tsv)

MATCHED_SA=()

for SA_ID in ${STORAGE_ACCOUNTS}; do

  SA_NAME=$(basename "${SA_ID}")

  SA_RG=$(echo "${SA_ID}" | awk -F/ '{print $5}')

  RULES=$(az storage account network-rule list \

    --account-name "${SA_NAME}" \

    --resource-group "${SA_RG}" 2>/dev/null || echo "")

  if [[ -z "${RULES}" ]]; then

    continue

  fi

  for SUBNET_ID in ${VNET_SUBNET_IDS}; do

    if echo "${RULES}" | jq -e --arg sn "${SUBNET_ID}" '.virtualNetworkRules[]?.virtualNetworkResourceId == $sn' >/dev/null 2>&1; then

      echo "Matched storage account: ${SA_NAME} (RG: ${SA_RG}) for subnet: ${SUBNET_ID}"

      MATCHED_SA+=("${SA_ID}")

      break

    fi

  done

done

MATCHED_SA_UNIQ=($(printf "%s\n" "${MATCHED_SA[@]}" | sort -u))

echo ""

echo "Matched storage accounts:"

printf "%s\n" "${MATCHED_SA_UNIQ[@]}"

echo ""

# ---- COPY DATA TO DR STORAGE ACCOUNTS ----

for SA_ID in "${MATCHED_SA_UNIQ[@]}"; do

  SA_NAME=$(basename "${SA_ID}")

  SA_RG=$(echo "${SA_ID}" | awk -F/ '{print $5}')

  TARGET_SA_NAME="${SA_NAME}${SUFFIX}"

  echo "Processing storage account:"

  echo " Source: ${SA_NAME} (RG: ${SA_RG})"

  echo " Target: ${TARGET_SA_NAME}"

  echo ""

  # Source key

  az account set --subscription "${SOURCE_SUBSCRIPTION_ID}"

  SRC_KEY=$(az storage account keys list \

    --account-name "${SA_NAME}" \

    --resource-group "${SA_RG}" \

    --query "[0].value" -o tsv)

  SRC_CONN="DefaultEndpointsProtocol=https;AccountName=${SA_NAME};AccountKey=${SRC_KEY};EndpointSuffix=core.windows.net"

  # Target key

  az account set --subscription "${TARGET_SUBSCRIPTION_ID}"

  # Adjust RG derivation if needed

  TARGET_SA_RG="<TARGET_RG_FOR_${TARGET_SA_NAME}>"

  TGT_KEY=$(az storage account keys list \

    --account-name "${TARGET_SA_NAME}" \

    --resource-group "${TARGET_SA_RG}" \

    --query "[0].value" -o tsv)

  TGT_CONN="DefaultEndpointsProtocol=https;AccountName=${TARGET_SA_NAME};AccountKey=${TGT_KEY};EndpointSuffix=core.windows.net"

  # List containers in source

  az account set --subscription "${SOURCE_SUBSCRIPTION_ID}"

  CONTAINERS=$(az storage container list \

    --connection-string "${SRC_CONN}" \

    --query "[].name" -o tsv)

  for CONT in ${CONTAINERS}; do

    echo "Copying container: ${CONT}"

    SRC_URL="https://${SA_NAME}.blob.core.windows.net/${CONT}"

    TGT_URL="https://${TARGET_SA_NAME}.blob.core.windows.net/${CONT}"

    azcopy copy "${SRC_URL}" "${TGT_URL}" --recursive=true

    echo "Completed copy for container ${CONT}"

  done

done

echo "Storage data copy to DR accounts complete."



No comments:

Post a Comment