Platform engineering has emerged as the discipline that sits between DevOps tooling and the application teams that consume it. Its goal is deceptively simple, make it easy for developers to do the right thing, provision infrastructure, deploy services, manage environments, without requiring deep infrastructure expertise or opening a ticket to a platform team.
The gap between that goal and reality is often filled by a patchwork of CI/CD pipelines, manual approvals, Slack messages, and Terraform scripts that only two people understand. Developer experience suffers. Compliance and security controls exist only in documentation that nobody reads. Infrastructure sprawls across environments with no consistent enforcement.
Spacelift is a platform engineering tool designed specifically to close this gap, providing a collaborative, policy-driven, GitOps-native control plane for Terraform, OpenTofu, Pulumi, Ansible, and Kubernetes. It sits above your IaC tooling and gives platform teams the controls they need to enforce standards, while giving application teams the self-service access they need to move fast without breaking things.
This guide covers how to build a self-service infrastructure platform with Spacelift, from stack architecture and policy enforcement to drift detection, module registries, and the developer experience patterns that make platform engineering actually deliver on its promise.
What Makes Spacelift Different
Before diving into implementation, it helps to understand what Spacelift provides that raw CI/CD pipelines don't:
| Capability | Raw CI/CD (GitHub Actions) | Spacelift |
|---|---|---|
| Terraform plan/apply | Manual pipeline config | Native, zero-config |
| Policy enforcement | Shell scripts or OPA sidecars | Built-in OPA policies (plan, access, trigger) |
| Drift detection | Custom cron jobs | Native, scheduled |
| Module registry | Terraform Registry or S3 | Built-in, versioned |
| Stack dependencies | Manual pipeline ordering | Native dependency graph |
| Self-service provisioning | PR-based or ticket-based | Self-service catalog |
| Audit trail | CI logs | Dedicated, tamper-evident |
| Secret management | GitHub Secrets | Native Vault/cloud integration |
| Cost estimation | None | Infracost integration |
The fundamental difference is intent, CI/CD pipelines are general-purpose automation. Spacelift is purpose-built infrastructure orchestration, with the safety, compliance, and developer experience features that platform engineering requires.
Core Concepts
Before building, understand Spacelift's key abstractions:
Stack, the fundamental unit in Spacelift. A stack wraps a single IaC configuration (a Terraform root module, a Pulumi program, an Ansible playbook) with its state backend, environment variables, policies, and deployment settings. One stack equals one deployable infrastructure unit.
Space, a hierarchical namespace for grouping and access-controlling stacks. Spaces mirror your organizational structure: root → platform → team-a, root → platform → team-b. Policies and access rules defined at a parent space are inherited by child spaces.
Policy, an OPA (Open Policy Agent) Rego rule that governs stack behavior. Spacelift ships four policy types: plan (approve or reject based on plan output), access (control who can do what), trigger (control when stacks run), and git push (control which commits trigger runs).
Module, a versioned, reusable Terraform module published to Spacelift's built-in registry. Modules are the mechanism through which platform teams publish approved, tested infrastructure patterns that application teams consume via self-service.
Run, a single execution of a stack's IaC code. Every run is tracked with full input context, plan output, policy decisions, approval state, and apply output, providing an immutable audit trail.
Step 1 - Repository and Stack Architecture
Design your repository structure before creating stacks. A common pattern for multi-team platform engineering:
/infrastructure
/platform ← platform team owns this
/networking
main.tf ← VPCs, subnets, TGW
/security
main.tf ← IAM roles, security groups, KMS keys
/eks
main.tf ← shared EKS clusters
/modules ← approved module library
/vpc
/rds
/s3-secure
/eks-nodegroup
/teams ← application teams own this (self-service)
/team-alpha
/staging
main.tf
/production
main.tf
/team-beta
/staging
main.tf
/production
main.tf
/policies ← Spacelift policies as code
plan-enforce-tagging.rego
plan-cost-guard.rego
access-team-boundaries.rego
trigger-dependency-chain.rego
This structure enforces a clear ownership boundary, the platform team manages foundational infrastructure and approved modules. Application teams self-service within /teams/{team-name}, constrained by policies that prevent them from touching what they shouldn't.
Step 2 - Creating and Configuring Stacks
Create stacks programmatically using the Spacelift Terraform provider, treating your platform configuration as code:
# spacelift/stacks/platform-networking.tf
resource "spacelift_stack" "platform_networking" {
name = "platform-networking"
description = "Core VPC and network infrastructure"
space_id = spacelift_space.platform.id
repository = "your-org/infrastructure"
branch = "main"
project_root = "platform/networking"
terraform_version = "1.6.0"
autodeploy = false # require explicit approval for platform changes
protect_from_deletion = true
labels = ["platform", "networking", "critical"]
}
resource "spacelift_stack" "team_alpha_staging" {
name = "team-alpha-staging"
description = "Team Alpha staging environment"
space_id = spacelift_space.team_alpha.id
repository = "your-org/infrastructure"
branch = "main"
project_root = "teams/team-alpha/staging"
terraform_version = "1.6.0"
autodeploy = true # staging auto-deploys on merge
labels = ["team-alpha", "staging"]
}
Configure environment variables and secret injection per stack:
# Environment variables, non-sensitive configuration
resource "spacelift_environment_variable" "team_alpha_region" {
stack_id = spacelift_stack.team_alpha_staging.id
name = "AWS_DEFAULT_REGION"
value = "us-east-1"
write_only = false
}
# Secret injection from AWS Secrets Manager via Spacelift context
resource "spacelift_context" "aws_credentials" {
name = "aws-production-credentials"
description = "AWS credentials for production deployments"
space_id = spacelift_space.platform.id
}
resource "spacelift_context_attachment" "team_alpha_prod" {
context_id = spacelift_context.aws_credentials.id
stack_id = spacelift_stack.team_alpha_staging.id
priority = 1
}
Contexts are the Spacelift equivalent of shared environment variable sets, define credentials once at the platform level, attach to any number of stacks, and rotate in one place.
Step 3 - Policies as Code with OPA
Policies are where platform engineering leverage lives. A policy written once enforces a standard across every stack in your platform, no per-team configuration, no tribal knowledge, no exceptions that slip through code review.
Plan Policy - Enforce Resource Tagging
Reject any Terraform plan that creates resources without mandatory tags:
# policies/plan-enforce-tagging.rego
package spacelift
# Required tags for all resources in production
required_tags := {"Environment", "Team", "CostCenter", "ManagedBy"}
# Deny if any resource is missing required tags
deny[sprintf("Resource '%s' is missing required tags: %v", [resource.address, missing])] {
resource := input.terraform.resource_changes[_]
resource.change.actions[_] == "create"
# Identify which required tags are missing
provided_tags := {tag | resource.change.after.tags[tag]}
missing := required_tags - provided_tags
count(missing) > 0
}
# Warn if tags exist but Environment doesn't match a known value
warn[sprintf("Resource '%s' has unrecognized Environment tag: '%s'", [resource.address, env])] {
resource := input.terraform.resource_changes[_]
env := resource.change.after.tags.Environment
not env in {"staging", "production", "development"}
}
Plan Policy - Cost Guard
Reject plans that would exceed a per-run cost threshold:
# policies/plan-cost-guard.rego
package spacelift
# Block plans with monthly cost increase above $500
deny["Monthly cost increase exceeds $500 threshold — escalate to platform team for approval"] {
input.spacelift.run.cost_estimate.monthly_cost_increase > 500
}
warn["Monthly cost increase above $100 — ensure this is expected"] {
input.spacelift.run.cost_estimate.monthly_cost_increase > 100
input.spacelift.run.cost_estimate.monthly_cost_increase <= 500
}
Access Policy - Team Boundary Enforcement
Prevent teams from reading or modifying stacks they don't own:
# policies/access-team-boundaries.rego
package spacelift
# Extract team from the user's IdP groups
user_teams := {team |
group := input.session.teams[_]
team := trim_prefix(group, "team-")
}
# Allow access only to stacks labeled with the user's team
allow {
stack_team := input.stack.labels[_]
stack_team in user_teams
}
# Platform team members have access to all stacks
allow {
"platform" in input.session.teams
}
# Block all other access
deny {
not allow
}
Trigger Policy - Stack Dependency Chains
Automatically trigger downstream stacks when upstream infrastructure changes:
# policies/trigger-dependency-chain.rego
package spacelift
# When platform-networking applies successfully, trigger EKS stack
trigger["platform-eks"] {
input.run.stack.id == "platform-networking"
input.run.state == "FINISHED"
input.run.type == "TRACKED"
}
# When platform-eks applies successfully, trigger team stacks
trigger["team-alpha-staging"] {
input.run.stack.id == "platform-eks"
input.run.state == "FINISHED"
}
Attach policies to stacks or spaces:
resource "spacelift_policy" "enforce_tagging" {
name = "enforce-resource-tagging"
type = "PLAN"
body = file("policies/plan-enforce-tagging.rego")
}
# Attach to the entire platform space — all child stacks inherit
resource "spacelift_policy_attachment" "enforce_tagging_platform" {
policy_id = spacelift_policy.enforce_tagging.id
stack_id = "*"
space_id = spacelift_space.platform.id
}
Step 4 - Module Registry for Self-Service
The Spacelift module registry is the mechanism through which platform teams publish approved infrastructure patterns for self-service consumption. A module published to the registry is versioned, tested, and policy-compliant by definition, application teams consume it without needing to understand the underlying infrastructure design.
Publish a module to Spacelift's registry:
# spacelift/modules/rds-module.tf
resource "spacelift_module" "rds_postgres" {
name = "rds-postgres"
space_id = spacelift_space.platform.id
repository = "your-org/infrastructure"
project_root = "modules/rds"
branch = "main"
description = "Production-grade PostgreSQL RDS with encryption, backups, and parameter group"
terraform_provider = "aws"
labels = ["database", "postgres", "approved"]
}
Application teams consume it in their stacks like any Terraform module:
# teams/team-alpha/staging/main.tf
module "orders_database" {
source = "spacelift.io/your-org/rds-postgres/aws"
version = "~> 2.0"
identifier = "orders-staging"
instance_class = "db.t3.medium"
allocated_storage = 20
environment = "staging"
team = "team-alpha"
}
The version constraint (~> 2.0) means Team Alpha gets patch updates automatically but won't be silently upgraded to a breaking major version. The platform team controls what's published, application teams control what version they pin to.
Step 5 - Drift Detection
Infrastructure drift, the gap between what Terraform's state says exists and what actually exists in your cloud account, is one of the most persistent operational problems in IaC. Manual changes through the AWS console, emergency hotfixes applied directly, or race conditions between concurrent applies all cause drift.
Spacelift's built-in drift detection runs Terraform plan on a schedule and alerts when the live state diverges from the declared state:
resource "spacelift_stack" "platform_networking" {
# ... other config ...
# Run drift detection daily at 2am UTC
drift_detection {
enabled = true
reconcile = false # alert on drift but don't auto-remediate — require human review
schedule = ["0 2 * * *"]
}
}
For non-critical stacks where auto-remediation is safe:
drift_detection {
enabled = true
reconcile = true # automatically apply to reconcile drift
schedule = ["0 */6 * * *"] # every 6 hours
}
Configure Slack or PagerDuty notifications for drift alerts:
resource "spacelift_webhook" "drift_alert" {
stack_id = spacelift_stack.platform_networking.id
endpoint = var.slack_webhook_url
enabled = true
# Trigger on drift detection and failed reconciliation
secret = var.webhook_secret
}
Step 6 - Self-Service Developer Experience
The final layer is the developer experience that makes self-service actually self-service. Spacelift's self-service catalog (Spaces + module registry + approval workflows) lets application teams provision infrastructure through a UI or API without opening tickets:
For application teams: a curated list of available modules (RDS, S3, SQS, ElastiCache) with a form-based provisioning interface, no Terraform knowledge required.
For platform teams: full visibility into what every team has provisioned, policy enforcement that fires automatically, drift detection that runs continuously, and an audit trail that captures every change with the identity of who made it and what the plan output was.
Connect Spacelift to your IdP (Okta, GitHub, Google) to enforce team boundaries automatically via SSO group membership, the access policies defined in Step 3 apply automatically as team membership changes.
Step 7 - CI/CD Integration
Spacelift integrates with GitHub, GitLab, Bitbucket, and Azure DevOps for Git-driven runs. A push to main in the infrastructure repo triggers plan runs on affected stacks automatically:
# .github/workflows/spacelift-preview.yml
name: Spacelift Plan Preview
on: [pull_request]
jobs:
plan-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Comment plan output on PR
uses: spacelift-io/spacelift-pull-request-action@v1
with:
spacelift-api-key-id: ${{ secrets.SPACELIFT_API_KEY_ID }}
spacelift-api-key-secret: ${{ secrets.SPACELIFT_API_KEY_SECRET }}
For stacks with autodeploy: false, Spacelift posts the plan output as a PR comment, requiring a human to review and approve before the apply runs. For stacks with autodeploy: true (staging environments), the apply runs automatically on merge to main.
Common Pitfalls to Avoid
Flat space hierarchy. A single space for all stacks gives no access boundary between teams. Design your space hierarchy to mirror your team structure from day one, retrofitting access controls onto a flat hierarchy is painful.
Policies without testing. A deny policy that fires incorrectly can block all deployments across your entire platform. Test policies against representative plan inputs using OPA's opa eval before attaching them to production stacks.
Auto-reconciling critical infrastructure. Drift auto-remediation is powerful but dangerous for foundational infrastructure. Networking, security groups, and IAM roles should alert on drift and require human review, not automatically apply. Reserve auto-reconciliation for stateless, idempotent resource sets.
No module versioning discipline. Publishing a breaking change to a module and expecting all consumers to upgrade simultaneously doesn't scale. Adopt semantic versioning for modules and maintain at least one prior major version while teams migrate.
Conclusion
Platform engineering is not about giving developers access to infrastructure tooling, it's about designing guardrails and self-service patterns so that developers can provision what they need safely, consistently, and without creating operational debt for the platform team.
Spacelift makes this achievable by providing a purpose-built control plane for IaC, policy enforcement that fires at plan time, a module registry that codifies approved patterns, drift detection that catches manual changes, and a space hierarchy that enforces team boundaries without manual intervention.
The platform team's job evolves from running infrastructure to building the platform that lets others run theirs safely, at scale, and with a complete audit trail.
Running OpenTofu instead of Terraform, or mixing Pulumi with Ansible? Spacelift's multi-IaC support handles heterogeneous toolchains in the same platform all under the same policy and audit model. Drop your stack in the comments.
Top comments (0)