DEV Community

Cover image for Scanning Terraform for Lock-In Before a Cloud Migration
Denys Zaruin
Denys Zaruin

Posted on Originally published at moveinfra.eu

Scanning Terraform for Lock-In Before a Cloud Migration

Originally published on moveinfra.eu.

To scan your Terraform setup for cloud lock-in before a migration, extract a full list of resource types from your .tf files or state file and group them into three categories: directly portable standards (like virtual machines, PostgreSQL databases, and S3-compatible buckets), managed services that need configuration changes (like message queues and serverless functions), and proprietary services that require application code rewrites (like DynamoDB, Firestore, or BigQuery). Pair this infrastructure audit with a scan of your application dependencies for provider-specific SDKs so you know exactly which engineering teams need to change code before you move to a European cloud provider.

Key takeaways

  • Terraform does not make your infrastructure portable by default. It standardizes how you write provisioning code, not the underlying cloud APIs. Moving from AWS to an EU cloud still requires rewriting provider blocks and resource definitions.
  • Most cloud lock-in hides in data stores and IAM, not compute. Virtual machines, Kubernetes clusters, and standard relational databases move with minimal friction. Proprietary NoSQL databases and event buses require application changes.
  • Scan both .tf files and your Terraform state. Static code scans are fast and require no cloud credentials, while state file inspections catch resources generated dynamically by loops or external modules.
  • Check your application repositories alongside your infrastructure. A resource in Terraform is only truly locked in if your application code calls its proprietary SDK directly instead of using a standard protocol like SQL, S3, or AMQP.
  • Automate the check in CI/CD. Once you baseline your current lock-in, run a scanner in your pull requests to avoid adding new proprietary services while you plan your migration.

Why Terraform does not prevent cloud lock-in

A common misconception among management and non-infrastructure engineers is that using Infrastructure as Code (IaC) like Terraform or OpenTofu means you can switch cloud providers by changing a single setting. In practice, Terraform uses provider-specific plugins. An aws_instance on Amazon Web Services, a google_compute_instance on Google Cloud, and an upcloud_server on UpCloud all represent a Linux virtual machine, but they use completely different resource names, attributes, and networking models.

However, rewriting Terraform HCL (HashiCorp Configuration Language) is rarely the hardest part of a cloud migration. Rewriting an aws_db_instance running PostgreSQL into a managed database resource on Scaleway or OVHcloud takes an experienced engineer a few hours.

Real lock-in happens when the cloud resource you provision in Terraform forces your application code to adopt a proprietary API or mental model. If your Terraform code provisions an aws_dynamodb_table or a google_bigquery_dataset, you cannot simply map that resource to an equivalent on Hetzner or UpCloud. To leave the hyperscaler, your developers will have to rewrite data access layers, change query languages, and migrate data schemas.

Scanning your Terraform codebase before a migration gives you an honest bill of materials. It separates the infrastructure that can be rebuilt on a European cloud in days from the architectural dependencies that require weeks of engineering work.

The three tiers of Terraform resource portability

When auditing your Terraform repositories, you do not need to evaluate every resource from scratch. Almost every cloud resource falls into one of three portability tiers.

Portability Tier What It Means Common Terraform Examples (AWS / GCP / Azure) EU Cloud Migration Effort
Tier 1: Directly Portable (Green) Uses open standards or protocols (Linux, SQL, Kubernetes API, S3 API, Redis protocol). Application code only needs a new connection string or endpoint URL. aws_instance, aws_db_instance (Postgres/MySQL), aws_s3_bucket, aws_eks_cluster, google_sql_database_instance, azurerm_kubernetes_cluster, aws_elasticache_cluster (Redis) Low. Rebuild the Terraform resource for the target EU provider; sync data via logical replication or S3 tools.
Tier 2: Adapter Required (Yellow) Standard concept, but configured via proprietary APIs or deeply tied to provider networking and identity. Requires plumbing or CI/CD changes, and sometimes minor SDK swaps. aws_sqs_queue, aws_lambda_function, aws_alb, google_cloudfunctions_function, azurerm_service_bus_namespace, aws_iam_role, aws_kms_key Medium. Replace with open equivalents (e.g., RabbitMQ, Kafka, Knative, or standard container deployments) and update app configuration.
Tier 3: High Lock-In (Red) Proprietary database engines, global serverless platforms, or analytics warehouses with custom query dialects and SDKs. No drop-in EU cloud equivalent exists. aws_dynamodb_table, google_firestore_database, azurerm_cosmosdb_account, aws_redshift_cluster, google_bigquery_dataset, aws_step_functions_state_machine High. Requires refactoring application code, redesigning schemas (e.g., moving from DynamoDB to PostgreSQL), and writing custom data migration scripts.

You can check how specific hyperscaler services correspond to European alternatives using our free EU cloud service map.

How to scan your Terraform codebase step by step

You can audit your infrastructure in less than an hour without granting third-party tools access to your live cloud environment. Follow these four steps to build your portability inventory.

Step 1: Extract a raw list of resource types from your .tf files

Start by listing every resource type declared in your repository. If you use standard UNIX command-line tools (or ripgrep), you can extract and count every resource block across your codebase:

grep -rhE '^resource\s+"[^"]+"' . | awk -F'"' '{print $2}' | sort | uniq -c | sort -nr
Enter fullscreen mode Exit fullscreen mode

This command outputs a ranked list of every resource type in your repository, such as:

  42 aws_security_group_rule
  18 aws_iam_role_policy_attachment
  12 aws_s3_bucket
   6 aws_db_instance
   4 aws_sqs_queue
   2 aws_dynamodb_table
Enter fullscreen mode Exit fullscreen mode

Right away, you can ignore the noise. Having forty aws_security_group_rule blocks is not a migration blocker; every European cloud has firewall rules, and translating IP ranges and ports is straightforward. Your eyes should go straight to the data stores and messaging layers at the bottom of the list: aws_db_instance, aws_sqs_queue, and aws_dynamodb_table.

Step 2: Inspect the engines and attributes inside data resources

Knowing the resource type is sometimes not enough. A single Terraform resource type can represent either a portable open-source service or a proprietary one, depending on its attributes.

For example, if you see aws_rds_cluster or aws_db_instance, inspect the engine attribute:

  • engine = "postgres" or engine = "mysql" puts the resource firmly in Tier 1. You can migrate this to managed PostgreSQL or MySQL on UpCloud, Scaleway, or OVHcloud using standard logical replication.
  • engine = "aurora-postgresql" is still mostly Tier 1, unless your application relies on Aurora-specific serverless scaling APIs or custom AWS extensions (like aws_s3 inside PostgreSQL to import data directly from buckets).
  • engine = "sqlserver" or engine = "oracle-ee" moves the resource into Tier 2 or 3, because most European cloud providers do not offer managed Microsoft SQL Server or Oracle databases, requiring you to either self-host on virtual machines or migrate engines.

Similarly, check aws_s3_bucket resources. Basic object storage and lifecycle expiration rules translate cleanly to European S3-compatible object storage. However, if you see aws_s3_bucket_notification triggering an aws_lambda_function on every file upload, you have an event-driven dependency that you will need to replicate in your application code or via an alternative webhook mechanism.

Step 3: Check what third-party modules are hiding

If your engineering team uses public modules from the Terraform Registry (such as terraform-aws-modules/vpc/aws or terraform-aws-modules/eks/aws), a simple grep on your .tf files will only show module blocks, hiding the underlying resources.

To see every resource that Terraform actually manages—including those nested inside modules—run terraform show against your state file (or a saved plan file) in JSON format and parse it with jq:

terraform show -json | jq -r '
  .. | objects | select(has("type") and has("mode")) 
  | select(.mode == "managed") 
  | .type
' | sort | uniq -c | sort -nr
Enter fullscreen mode Exit fullscreen mode

This command queries the resolved state tree and prints the exact count of every managed resource currently running in your account. It will also reveal if a seemingly simple module is quietly provisioning proprietary KMS keys, CloudWatch log groups, or Systems Manager parameters behind the scenes.

Step 4: Automate the audit with an open-source lock-in scanner

Doing this manually works well for a single repository, but most companies split their Terraform across multiple environments and mix infrastructure code with application code.

To speed this up, you can use our free, open-source cloud lock-in scanner (available on our free tools page or directly on GitHub at moveinfra/cloud-lock-in-scanner). It runs locally via CLI or as a GitHub Action and scans two things at once:

  1. Your Terraform files: It identifies AWS, Google Cloud, and Azure resources and categorizes them by how difficult they are to move to standard European infrastructure.
  2. Your application code: It checks your package.json, requirements.txt, go.mod, or pom.xml files, as well as source code imports, for cloud-specific SDKs.

Why scan both? Suppose your Terraform scan flags an aws_s3_bucket. If your Node.js application accesses that bucket using the standard @aws-sdk/client-s3 library with a configurable endpoint URL, switching to Scaleway or Hetzner Object Storage is a one-line configuration change. By contrast, if the code scanner finds @aws-sdk/client-dynamodb or @google-cloud/firestore imported across 35 different application files, you have an accurate measure of the refactoring work ahead.

A practical checklist for your Terraform lock-in audit

When you review your scan results with your engineering team, walk through these six areas to turn the raw output into a migration plan:

  • [ ] Databases and state: List every database resource. Confirm which ones run standard open-source engines (PostgreSQL, MySQL, Redis) and flag any proprietary NoSQL or data warehouse services (DynamoDB, Firestore, Cosmos DB, BigQuery, Redshift).
  • [ ] Object storage features: Verify whether your buckets only use standard GET/PUT/DELETE operations and presigned URLs, or if they rely on proprietary features like AWS S3 Object Lambda, Glacier deep archive APIs, or bucket-triggered serverless functions.
  • [ ] Identity and access management (IAM): Count how many applications rely on cloud-native workload identity (such as AWS IAM Roles for Service Accounts in Kubernetes, or GCP Workload Identity) to authenticate to databases or queues instead of standard username/password or TLS certificate authentication.
  • [ ] Secrets and configuration: Check for aws_ssm_parameter, aws_secretsmanager_secret, or google_secret_manager_secret. Determine whether your applications fetch these secrets at runtime via cloud SDKs, or if they are injected as standard environment variables during deployment.
  • [ ] Queues and background jobs: Locate all aws_sqs_queue, aws_sns_topic, or google_pubsub_topic resources. Decide whether to replace them with a managed queue service on an EU provider, a Redis-backed job runner (like BullMQ or Sidekiq), or a self-hosted RabbitMQ/Kafka instance.
  • [ ] Untracked manual resources ("ClickOps"): Compare your Terraform state against your monthly cloud invoice. Companies frequently create experimental databases, DNS zones, or storage buckets manually in the web console and forget to import them into Terraform. Uploading your billing export to our free cloud bill analyzer is a reliable way to catch active services that do not appear in your git repositories.

How to reduce lock-in before you migrate

If your scan uncovers several Tier 2 and Tier 3 resources, you do not have to wait until migration day to fix them. In fact, decoupling your architecture while you are still on AWS, Google Cloud, or Azure makes the eventual cutover much safer.

  1. Swap proprietary SDKs for standard protocols where possible. If your app uses AWS SES via the proprietary AWS SDK to send transactional emails, consider switching the code to use standard SMTP credentials, or isolate the email-sending logic behind a single internal interface.
  2. Inject secrets at deploy time, not runtime. Instead of having your Python or Go application call AWS Secrets Manager on startup, have your CI/CD pipeline or Kubernetes External Secrets Operator fetch the secrets and mount them as standard environment variables. That way, the application code remains completely unaware of which cloud it runs on.
  3. Consolidate small NoSQL tables into PostgreSQL. Many teams use DynamoDB or Firestore simply to store session state, feature flags, or basic JSON documents because it was easy to set up. Modern PostgreSQL with jsonb columns handles these workloads easily at the scale of most 10-to-300-person companies, and every major EU cloud offers managed PostgreSQL.
  4. Add a lock-in check to pull requests. When planning a migration for the next quarter, the last thing you want is a product team introducing a new dependency on AWS Step Functions or Google Cloud Tasks next Tuesday. Running a lock-in scanner in CI alerts tech leads when a pull request adds a new proprietary Terraform resource or cloud SDK.

Finally, if your audit confirms that your infrastructure is mostly portable and you are ready to plan your timeline, remember that EU law is on your side regarding exit costs. Under Article 29 of the EU Data Act (Regulation (EU) 2023/2854), cloud providers may charge no more than their direct cost for switching—including data egress fees—until 12 January 2027, and switching charges are banned entirely from 12 January 2027 onward. Many hyperscalers also have specific exit waiver programs today, though you should always check the provider's official documentation and our cloud exit fees guide before initiating a large data transfer.

Frequently asked questions

Does switching from Terraform to OpenTofu reduce cloud lock-in?

No. Switching from Terraform to OpenTofu removes licensing lock-in with HashiCorp (IBM), because OpenTofu is open-source and community-governed. However, OpenTofu uses the exact same cloud provider plugins (aws, google, azurerm). Your infrastructure code is still bound to the underlying cloud provider's APIs until you rewrite the resources for an alternative provider.

Can an automated tool convert my AWS Terraform files into Hetzner or UpCloud Terraform files?

Large language models and conversion scripts can translate individual resource blocks—such as turning an aws_instance into an upcloud_server—reasonably well. However, a 1-to-1 file translation rarely works for a whole environment because networking models differ between clouds. AWS requires explicit route tables, internet gateways, and NAT gateways per availability zone, whereas most European clouds use simpler VPC and firewall primitives. It is cleaner to generate fresh, idiomatic Terraform modules designed for the target EU cloud that match your existing sizing and topology.

What if part of our infrastructure was created manually in the cloud console and is not in Terraform?

A static Terraform scan will only show what is tracked in code. To catch "ClickOps" resources, inspect your cloud bill line by line or use read-only API discovery against your cloud account before planning a migration. Any production resource created by hand should be inventoried and written into your new EU cloud Terraform codebase so you eliminate configuration drift during the move.

Should we use a multi-cloud abstraction layer like Crossplane or Pulumi to avoid future lock-in?

For most companies between 10 and 300 people, building a custom abstraction layer that tries to make AWS and an EU cloud look identical in code adds more maintenance overhead than it saves. You do not migrate clouds every month. A more practical approach is to rely on standard, portable building blocks—Linux VMs, Kubernetes, PostgreSQL, Redis, and S3-compatible object storage—and keep your Terraform clean and simple for whichever provider you currently use.

Sources


Drafted with AI assistance and fact-checked by the MoveInfra team. The original, kept up to date, is at moveinfra.eu. Free tools for pricing a move to an EU cloud: moveinfra.eu/tools.

Top comments (0)