Book a call
All talks and takeaways

Template

The migration readiness map

A one-page map of every workload and data store before a move to an EU region, plus a Terraform policy that denies non-EU regions.

Moving out of us-east-1 is rarely hard technically. I'm starting to see the same failure in a lot of moves: a dependency nobody drew. A hard-coded region, a forgotten Lambda, a partner webhook pointed at an IP address. Every surprise in a migration is a line missing from the map.

The map has one row per workload and data store. Any cell that says 'not sure' is the next thing to find out. The region-deny policy at the end turns the target into a rule.

From the talk: Leaving us-east-1: moving a startup into the EU without downtime

The map

The first row is an example. The blank rows are for your own estate.

Workload or data storeRegion todayOwnerDepends onCalled from outsidePersonal dataSizeTolerable read-only windowWave
orders-db (RDS PostgreSQL)us-east-1Platformapi, billing-workerNoYes180 GB5 minutes3
Blank row to fill in
Blank row to fill in

How I fill it: from the APIs, not from memory

  1. I list what runs where. AWS Resource Explorer with an aggregator index, or AWS Config. For example: aws resource-explorer-2 search --query-string "region:us-east-1".
  2. I map who calls whom. VPC flow logs, load balancer logs, and CloudTrail show the real traffic, including the forgotten calls.
  3. I find what calls in from outside. Webhooks, partner IP allow-lists, and mobile app builds with hard-coded endpoints.
  4. I search the code for the old region. Hard-coded region names and ARNs are the classic missing line.
  5. I order the waves. Low-risk, stateless workloads go first. Data stores with personal data and short read-only windows go last, after a rehearsal.

Choosing the EU region

I pick where customers, latency, and service availability point, not where the console opens.

CloudRegions in the EU
AWSIreland (eu-west-1), Frankfurt (eu-central-1), Paris (eu-west-3), Stockholm (eu-north-1), Milan (eu-south-1), Spain (eu-south-2). London and Zurich are in Europe, but outside the EU.
AzureWest Europe sits in the Netherlands.
Google Cloudeurope-west4 sits in Eemshaven, the Netherlands.

The region-deny policy

A service control policy that denies every region except the chosen EU ones. I attach it to a test OU first and to the workloads OU after.

eu-only.tf
# eu-only.tf: deny every AWS region except the EU ones you choose.
# Global services (IAM, Organizations, CloudFront, Route 53, and others) run in
# us-east-1, so they are exempt. Compare not_actions with AWS's current
# region-deny example before you attach it, and try it on a test OU first.

variable "allowed_regions" {
  type    = list(string)
  default = ["eu-central-1", "eu-west-1"]
}

variable "target_id" {
  description = "The OU (or account) the policy applies to. Never the root on day one."
  type        = string
}

data "aws_iam_policy_document" "eu_only" {
  statement {
    sid       = "DenyOutsideAllowedRegions"
    effect    = "Deny"
    resources = ["*"]
    not_actions = [
      "a4b:*", "acm:*", "aws-marketplace-management:*", "aws-marketplace:*",
      "aws-portal:*", "budgets:*", "ce:*", "chime:*", "cloudfront:*", "config:*",
      "cur:*", "directconnect:*", "ec2:DescribeRegions",
      "ec2:DescribeTransitGateways", "ec2:DescribeVpnGateways", "fms:*",
      "globalaccelerator:*", "health:*", "iam:*", "importexport:*", "kms:*",
      "mobileanalytics:*", "networkmanager:*", "organizations:*", "pricing:*",
      "route53:*", "route53domains:*", "route53-recovery-cluster:*",
      "route53-recovery-control-config:*", "route53-recovery-readiness:*",
      "s3:GetAccountPublic*", "s3:ListAllMyBuckets",
      "s3:ListMultiRegionAccessPoints", "s3:PutAccountPublic*", "shield:*",
      "sts:*", "support:*", "trustedadvisor:*", "waf-regional:*", "waf:*",
      "wafv2:*", "wellarchitected:*",
    ]

    condition {
      test     = "StringNotEquals"
      variable = "aws:RequestedRegion"
      values   = var.allowed_regions
    }

    # Break-glass: the role Organizations creates in each account keeps access.
    condition {
      test     = "ArnNotLike"
      variable = "aws:PrincipalARN"
      values   = ["arn:aws:iam::*:role/OrganizationAccountAccessRole"]
    }
  }
}

resource "aws_organizations_policy" "eu_only" {
  name        = "eu-only-regions"
  description = "Deny requests outside the approved EU regions"
  type        = "SERVICE_CONTROL_POLICY"
  content     = data.aws_iam_policy_document.eu_only.minified_json
}

resource "aws_organizations_policy_attachment" "eu_only" {
  policy_id = aws_organizations_policy.eu_only.id
  target_id = var.target_id
}

What I leave out on purpose

I leave out re-architecting on the way, a multi-region active-active design, and a move to Kubernetes at the same time. Each one adds risk to the move without improving the residency answer. One change at a time keeps the rollback simple.

Next step

I highly recommend filling in the map for your three biggest data stores first. If any row says 'not sure', a thirty-minute call finds the missing lines before a customer does. Which data store worries your team most?

Book a call

These takeaways are an engineer's reading of the rules and the tools. I wrote them to help teams decide what to build. They aren't legal advice. I recommend checking the current text with your own counsel before planning around a date or a threshold.

Let's talk.

A 30-minute call, no pitch. We look at your current setup, your stage, and the thing that's bothering you about it. I give a straight opinion on what to change, what to leave alone, and whether the problem is the one it looks like. What's the one thing in your stack you'd fix tomorrow?