
See how TV4 migrated 1,000 workspaces and 50,000 resources and Ably eliminated run queue bottlenecks by switching away from TFC.
Terraform infrastructure automation has shifted dramatically in recent years. With the December 2025 announcement that HashiCorp's legacy free tier will reach end-of-life on March 31, 2026, along with ongoing feature restrictions and pricing changes, many teams are re-evaluating their infrastructure automation strategies. This comprehensive guide walks you through the complete migration process, from understanding your motivations to post-migration validation.
Whether you're forced to migrate due to HCP Terraform pricing constraints, seeking greater control over your infrastructure automation, or exploring more cost-effective alternatives, this guide provides everything you need to plan and execute a successful migration.
Context: With HashiCorp discontinuing the HCP Terraform free tier on March 31, 2026, many teams are evaluating migration paths now.
On December 15, 2025, HashiCorp announced the end-of-life for the legacy HCP Terraform Free plan, effective March 31, 2026. This was the third major change in a pattern of restrictions that accelerated after IBM's acquisition of HashiCorp closed in late 2024:
Key Timeline:
The Resources Under Management (RUM) pricing model charges per resource per hour, creating significant and often unpredictable operational expenses:
| Managed Resources | Monthly Cost (Standard) | Annual Cost |
|---|---|---|
| 1,000 | ~$350 | ~$4,200 |
| 5,000 | ~$2,450 | ~$29,400 |
| 10,000 | ~$10,200 | ~$122,400 |
Consider that every security group rule, IAM policy, and S3 lifecycle configuration counts as a resource. Real-world resource counts are typically 30-50% higher than initially expected.
Before embarking on migration, conduct a thorough audit of your current TFC/TFE setup:
This is the most critical preparatory step. State files are your source of truth.
Backup Methods:
terraform state pull > workspace_name.tfstateAdditional Considerations:
terraform plan to check drift)Choose and set up your target backend:
AWS S3:
Azure Blob Storage:
Google Cloud Storage (GCS):
Modify your Terraform configuration to point to the new backend.
If migrating from TFC using cloud block:
# Remove:
# terraform {
# cloud {
# organization = "your-tfc-org"
# workspaces {
# name = "your-workspace-name"
# }
# }
# }
# Add:
terraform {
backend "s3" {
bucket = "your-new-s3-bucket-name"
key = "path/to/your/terraform.tfstate"
region = "your-aws-region"
dynamodb_table = "your-dynamodb-lock-table-name"
encrypt = true
}
}For Azure Blob Storage:
terraform {
backend "azurerm" {
resource_group_name = "your-resource-group"
storage_account_name = "yourstorageaccountname"
container_name = "your-container-name"
key = "path/to/your/terraform.tfstate"
}
}For Google Cloud Storage:
terraform {
backend "gcs" {
bucket = "your-new-gcs-bucket-name"
prefix = "path/to/your/terraform.tfstate"
}
}# Navigate to Terraform configuration directory
cd /path/to/terraform
# Initialize new backend
terraform init
# If automatic migration is offered, review carefully and confirm
# If manual control preferred (recommended for critical workloads):
terraform init -migrate-state
# Push backed-up state to new backend
terraform state push downloaded_workspace.tfstateVerification:
# Run plan to verify successful migration
terraform plan
# Should show "No changes" or only intentional changes
# Any unexpected destructions indicate migration issuesVariables previously in TFC/TFE UI now require new management:
Options:
.tfvars files (non-sensitive, committed to VCS)terraform plan to verify backend connectivity and state interpretationIf using CI/CD pipelines with TFC/TFE API integration:
Once confident in migration:
After backend migration, reconfigure VCS integration:
If your repositories live on an on-prem Git server reached through a VCS agent pool, test module publishing separately from workspace runs. A team in this setup had workspace plans working while module publishing failed with Failed to synchronize a module. The Git operation failed to complete via the VCS agent pool proxy… — and the console showed the agent as "not used," two signals pointing in opposite directions. A manual full re-sync pulled the module versions through. Workspace runs exercising the VCS connection successfully does not prove module sync works; verify both paths before declaring the integration done.
If implementing GitOps patterns:
Depending on chosen platform, update CI/CD workflows:
name: Terraform Plan and Apply
on:
pull_request:
paths:
- 'terraform/**'
push:
branches:
- main
paths:
- 'terraform/**'
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.0
- name: Terraform Init
run: terraform init
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Plan
run: terraform plan -out=tfplan
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}.gitlab-ci.yml with Terraform runner configurationsUse the TFC/TFE API to export variables:
#!/bin/bash
# Export TFC workspace variables
curl -s -H "Authorization: Bearer $TFC_API_TOKEN" \
"https://app.terraform.io/api/v2/workspaces/$WORKSPACE_ID/vars" | \
jq '.data[] | {key, value, sensitive, hcl}'Never commit secrets to VCS. Implement secure secret management:
AWS Secrets Manager:
aws secretsmanager create-secret \
--name terraform/prod/db-password \
--secret-string 'password-value'HashiCorp Vault:
vault kv put secret/terraform/prod/db-password value=password-valueGitHub Encrypted Secrets:
gh secret set TF_VAR_db_password --body 'password-value'Implement variable scoping appropriate to your platform:

Self-Hosting Nature: Fully self-managed Terraform platform within your infrastructure
Pros:
Cons:
Self-Hosting Nature: Fully self-hosted deployment within your AWS account
Pros:
Cons:
Self-Hosting Nature: SaaS control plane with self-hosted run and VCS agents

Pros:
Cons:
Self-hosted agents carry far less burden than a full self-hosted platform, but they are not zero operations, and the failure modes are specific. A team we worked with at Scalr ran agents in Kubernetes controller mode on GKE behind a mandatory corporate proxy. The agent registered with the control plane fine, then crash-looped: every call to the in-cluster Kubernetes API came back with OSError('Tunnel connection failed: 403 Forbidden') and Cannot connect to the Kubernetes API ... Max retries exceeded with url: /version/ (ProxyError). Their no_proxy looked correct — it listed the API server IP and the RFC1918 CIDR ranges. But the agent resolves the API server as kubernetes.default.svc, a hostname, and urllib3 does not CIDR-match no_proxy entries, so the hostname matched nothing in the list and the request fell through to the corporate proxy, which refused to tunnel to an internal address. The fix was a one-line proxy exclusion (kubernetes.default.svc and the cluster-local suffixes in no_proxy) — found after time spent auditing RBAC and IAM permissions that were never the problem. If you run agents behind a proxy, exclude in-cluster hostnames by name, not by CIDR.
Private registries are the other recurring source of agent friction in Scalr's support queue. One SRE team mirroring runner images into a private Azure Container Registry had every run time out with The task container wasn't acquired by the agent worker… within 300 seconds; the task pods sat in ImagePullBackOff trying to pull <acr>/scalr/runner:0.2.0 — a tag they had never defined, let alone mirrored. The workspace's Terraform version (1.8.8) wasn't pinned at the level the agent reads, so the agent fell back to a default image tag that didn't exist in their mirror. Hardcoding SCALR_AGENT_CONTAINER_TASK_IMAGE fixed it instantly, with the trade-off that runner version management now lives on the agent instead of the platform. A second team building custom images hit the inverse problem: they rebuilt a custom runner image under the same tag, and because task pods use imagePullPolicy: IfNotPresent, nodes kept serving the stale cached image. The choice is between always-pull (more registry load) and a unique tag suffix per build (more pipeline work); mutable tags and IfNotPresent do not mix.
If you're scoping an agent rollout on EKS, the questions enterprises ask us on day one are consistent: a custom runner image carrying their own CA certificates, separate node pools for controller pods versus task pods, VCS connections routed through the agent pool, and EFS volumes for run caches. All four are configuration rather than custom engineering — but they belong in the rollout plan, not discovered mid-migration.
Key Features:
Cost Model: No resource limits; scales with activity, not infrastructure size
Key Features:
Cost Model: Unlimited deployments; cost based on parallelism
Concurrency-based pricing — structural trade-offs. Spacelift's pricing metric is parallel run slots: the bill scales with the number of slots purchased. The structural problems compared with usage-based, per-run pricing: the customer is always paying the wrong amount (too few slots queues engineers during releases, too many rents idle capacity, no setting is correct); capacity planning is offloaded to the buyer (forecasting peaks, monitoring utilization, engaging procurement to purchase additional slots as needs increase); it fails hardest exactly when you need it most, with the slot cap throttling parallel fixes during incident response and additional slots requiring procurement under pressure; and slot jumps are discontinuous — one more slot can push the customer into the next bracket and a much larger bill. Usage-based, per-run pricing scales smoothly with one more run costing one more run, with no slot to mis-provision.
Key Features:
Cost Model: Deployments + run minutes; free tier: 3 users, 50 deployments, 500 min
| Feature | Self-Hosted TFE | Spacelift Self-Hosted | Scalr (Hybrid) | Cloud Platforms |
|---|---|---|---|---|
| Control | Maximum | Very High | High | Medium-High |
| Operational Overhead | Very High | Very High | Medium | Low |
| Setup Time | Weeks-Months | Weeks-Months | Days | Hours-Days |
| Cost Model | License + Operations | License + Operations | SaaS + Operations | SaaS |
| Vendor Lock-in | Lowest | Low | Medium | Medium |
| Built-in Governance | Extensive | Extensive | Extensive | Varies |
Scalr offers the fastest migration path with automated tooling and drop-in replacement capabilities.
Scalr provides a Terraform module that automates migration of most objects:
# Clone migration module
git clone https://github.com/Scalr/terraform-scalr-migrate-tfc
cd terraform-scalr-migrate-tfc
# Make the migration script executable
chmod +x migrate.sh
# Run migration script (see the repo README for the full flag reference)
./migrate.sh \
--tfc-token "$TFC_API_TOKEN" \
--tfc-organization "$TFC_ORG" \
--scalr-hostname "$SCALR_ACCOUNT.scalr.io" \
--scalr-token "$SCALR_API_TOKEN"Automatically Migrates:
For selective migration or greater control:
# Step 1: Pull state from Terraform Cloud
terraform state pull > terraform.state
# Step 2: Get Scalr API token
terraform login account-name.scalr.io
# Step 3: Update Terraform backend configuration
# Modify your terraform block:
terraform {
backend "remote" {
hostname = "account-name.scalr.io"
organization = "scalr-environment-name"
workspaces {
name = "workspace-name"
}
}
}
# Step 4: Initialize Scalr as backend
terraform init
# Step 5: Push state to Scalr
terraform state push terraform.statePrerequisites:
Steps:
terraform state pull > terraform.stateaws s3 cp terraform.state s3://spacelift-backend-bucket/Differences to Note:
Prerequisites:
Steps:
Differences to Note:
Architecture:
Implementation Steps:
# values.yaml for Helm
atlantis:
enabled: true
repoConfig:
- id: /.*github.com\/.*/
vcs: github
checkout:
mode: branchterraform {
backend "s3" {
bucket = "atlantis-state"
key = "environments/${ENVIRONMENT}/${COMPONENT}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}# policy/main.rego
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.actions[_] == "create"
rule := resource.change.after
rule.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf("Security group rule allows unrestricted access: %v", [resource.address])
}# .github/workflows/terraform.yml
- name: Atlantis Plan
uses: runatlantis/github-action@v1
with:
command: 'plan'# Verify state contents
terraform state list
terraform state show
# Compare resource counts
terraform state list | wc -l
# Should match pre-migration count# Run plan to check for unintended drift
terraform plan -detailed-exitcode
# Exit code 0: No changes
# Exit code 1: Error
# Exit code 2: Changes detectedReal-world migration examples: See how organizations have successfully migrated from Terraform Cloud to Scalr — Sierra-Cedar's customer journey, Ably's migration story, and TV4's migration experience.
Migrating off HCP Terraform is a strategic decision that can bring greater flexibility, cost savings, and control. Your choice depends on:
Speed and Minimal Disruption: Scalr or Spacelift SaaS
Maximum Control and Cost Optimization: Open-Source Stack
Hybrid Approach: Scalr or Spacelift with Self-Hosted Agents
Plan the agent lifecycle beyond initial setup. A team in a regulated industry replacing EC2-based agents asked for drain mode on day one: stop new runs from landing on an old agent so the instance can be terminated without killing an in-flight apply. They had scripted exactly this against their previous self-hosted install, and blue/green agent replacement falls apart without it. Whatever platform you choose, confirm how it retires an agent gracefully before you automate instance turnover.
Existing GitHub Investment: GitHub Actions-Centric
The Terraform ecosystem continues to evolve. By choosing a platform aligned with your team's strengths and your organization's needs, you'll be positioned for success regardless of future industry changes.
See how Sierra Cedar migrated to Scalr in less than a day from homegrown Terraform tooling.
