Infrastructure that exists only in someone's head or a ClickOps history is infrastructure that cannot be reproduced, audited, or trusted.
Infrastructure as code (IaC) means your cloud resources -- servers, databases, networking, load balancers, DNS, IAM roles -- are defined in version-controlled configuration files rather than created manually through the AWS or GCP console. Every change goes through code review. Every environment is reproducible. Spinning up a new environment takes minutes rather than days of ClickOps.
We write and implement infrastructure as code using Terraform and Pulumi for AWS, GCP, and Azure. Greenfield IaC for new infrastructure, and migration of existing click-ops-created infrastructure into Terraform state without rebuilding resources from scratch.
Terraform modules for your entire infrastructure -- VPCs, EKS clusters, RDS databases, S3 buckets, IAM roles -- version-controlled in git
Identical dev, staging, and production environments created from the same configuration with environment-specific variables
Existing ClickOps infrastructure imported into Terraform state so it is managed going forward without recreation
Pull request workflow for infrastructure changes -- reviewed, planned, and applied through the same process as code
RaftLabs writes and implements infrastructure as code using Terraform and Pulumi on AWS, GCP, and Azure. Greenfield IaC for new infrastructure and migration of existing ClickOps-created infrastructure into Terraform state without rebuilding resources. Writing Terraform for new infrastructure costs $15,000 to $40,000. Migrating existing ClickOps infrastructure into Terraform state runs $20,000 to $60,000. Most projects deliver in 6 to 12 weeks at a fixed cost.
Trusted by
The problem with infrastructure created through the AWS or GCP console is that the console leaves no record of intent -- only a record of what currently exists. You can see the resource. You cannot see why it was configured that way, what it was created alongside, or what the safe change procedure is. When something breaks, the investigation starts from scratch. When you need a second environment, someone recreates the first one from memory and judgment calls.
Infrastructure as code makes the configuration the artifact. The git history shows who changed what and why. The pull request shows the proposed change before it's applied, with the Terraform plan output showing exactly which resources will be added, modified, or destroyed. A new environment is created by running the same configuration with different variable values. The infrastructure is no longer someone's undocumented knowledge -- it is a codebase with the same review and version control practices as application code.
Capabilities
What we build
Terraform module development
Reusable Terraform modules for your infrastructure components -- each module encapsulating a logical infrastructure unit with a clean input/output interface that serves dev, staging, and production environments with different variable values rather than copy-pasted configuration blocks that diverge as each environment evolves differently. VPC and networking module: multi-AZ VPC with public and private subnets, NAT gateway configuration for private subnet egress, security group baseline (no inbound from 0.0.0.0/0 to any port on any resource except load balancer ports 80 and 443), VPC flow logs to S3 for network traffic audit. EKS cluster module: managed node groups with configurable instance type and autoscaling bounds, IRSA (IAM Roles for Service Accounts) setup so pod-level AWS permissions use short-lived federated credentials rather than access keys, cluster logging to CloudWatch for API server and audit log streams. RDS and Aurora module: Multi-AZ deployment for production, single-AZ for dev/staging to reduce cost, automated backups with configurable retention period (7 days dev, 30 days production), Performance Insights enabled, deletion protection enabled on production and explicitly disabled on dev/staging. S3 and CloudFront module: bucket policy blocking all public access by default, versioning enabled, lifecycle rules for intelligent tiering on objects older than 90 days, CloudFront distribution with OAC (Origin Access Control) for private S3 origin. IAM roles and policies module: least-privilege role definitions with specific resource ARNs rather than wildcard resources, permission boundaries on roles that can be assumed by applications, and an automated policy checker using aws-iam-access-analyzer that flags overly-permissive policies during plan. HCL linting with tflint and security scanning with Checkov in the CI pipeline to catch common misconfigurations (unencrypted EBS volumes, public S3 buckets, unrestricted security groups) before apply.
State management and remote backends
Terraform state stored in a remote backend so the state file is shared across the team rather than living on one engineer's machine, with locking to prevent concurrent applies from corrupting state. AWS backend: S3 bucket with versioning enabled (for state file recovery), server-side encryption with KMS CMK, and DynamoDB table for state locking -- the standard AWS IaC backend configuration, provisioned by a bootstrap Terraform configuration that is committed to the repository and applied once before any other Terraform. GCP backend: Google Cloud Storage bucket with versioning and bucket-level IAM controlling which service accounts can read and write state. State separation by environment: separate state files per environment using either the directory-per-environment pattern (environments/dev, environments/staging, environments/production each with their own state backend configuration) or Terraform Cloud workspaces -- the critical isolation that prevents a staging apply from acquiring a lock that blocks production, and prevents a staging state corruption from affecting production's source of truth about what exists. State encryption at rest: S3 KMS encryption means the state file (which may contain sensitive outputs like database passwords) is encrypted with a CMK whose usage is auditable via CloudTrail. State recovery: S3 versioning enables restoration of any previous state version using terraform state push if a partial apply leaves state in an inconsistent intermediate condition -- the recovery path documented in the runbook as part of project handoff. State access auditing: CloudTrail logs every S3 GET and PUT on the state bucket with the IAM identity and timestamp, providing a complete audit trail of who accessed or modified state and when.
Existing infrastructure import
Import of existing AWS or GCP resources created via the console into Terraform state without destroying and recreating them -- the migration path that brings click-ops infrastructure under IaC management without the downtime and data loss risk that recreation would require for production databases and stateful resources. Import process per resource: write the Terraform resource block describing the resource's expected configuration, run terraform import resource_type.resource_name resource_id to associate the configuration block with the live resource in state, then run terraform plan and iteratively adjust the configuration until the plan shows zero planned changes (meaning Terraform agrees the configuration matches reality). Terraform 1.5+ import blocks: the newer declarative import syntax that allows import operations to be version-controlled and reviewed in pull requests like any other configuration change, replacing the imperative terraform import CLI commands that were undocumented and unreviewable. Resources that cannot be imported (some legacy console resources with configuration that Terraform cannot fully represent) handled by migration -- a new resource created by Terraform alongside the old one, traffic migrated, old resource decommissioned -- documented as a planned migration step rather than a surprise. Drift detection configured after import: a scheduled terraform plan run (daily or on each CI pipeline execution) that alerts when the plan is non-empty, indicating someone has made a console change outside the IaC process. IAM policy to remove console write access after import for production resources: the mechanism that enforces the IaC discipline beyond team agreement, by making the console read-only for infrastructure resources that are now managed by Terraform -- optional but recommended for teams with strong compliance requirements.
CI/CD integration for infrastructure
Terraform plan run automatically on every infrastructure pull request -- the plan output shows the exact resources that will be added, modified, or destroyed, posted as a PR comment for reviewer inspection before any change is applied. The plan comment highlights destructive operations (resources marked for destruction appear with a distinct indicator) so reviewers do not miss the RDS instance deletion buried in a plan that also creates 40 other resources. Atlantis or Terraform Cloud for the CI integration: Atlantis runs as a self-hosted GitHub app that listens for PR events and runs terraform plan in the repository workspace, posting results to the PR and running terraform apply on merge; Terraform Cloud provides managed execution with run history, SSO, and team-based access controls for teams that prefer a managed service. Plan output parsing to extract a structured change summary: number of resources to add, change, and destroy -- the summary line that gives reviewers an immediate signal about the scope of the change before reading the full plan. Separate pipeline jobs for staging and production with promotion gate: staging apply runs automatically on merge to main; production apply requires an explicit approval step (a named reviewer or team) before the apply job runs -- the same gate pattern as application deployment pipelines. Plan and apply logs retained in the CI system for 90 days: the audit record that answers "what Terraform changes were made and by whom on the day before the production outage?" without requiring CloudTrail analysis. Sentinel policies (Terraform Cloud) or Open Policy Agent (Atlantis) as guardrails: policy-as-code rules that block applies containing specific patterns -- no resources with deletion_protection = false in production, no IAM policies with "Action": "*", no EBS volumes without encryption -- enforced automatically before apply rather than caught in review.
Multi-environment configuration
Environment separation structured so the same Terraform module code serves all environments and the differences between environments are explicit, version-controlled variables rather than undocumented console choices made independently in each account. Directory-per-environment pattern for teams that need hard account-level isolation: environments/dev, environments/staging, environments/production each point to the same shared modules but with environment-specific terraform.tfvars files containing instance types, replica counts, retention periods, and autoscaling bounds -- the pattern that makes "what is different about production?" a diff command rather than a question that requires manual investigation. Terraform workspaces for simpler configurations where account-level isolation isn't required: a single configuration with workspace-aware expressions like var.environment == "production" ? "db.r6g.xlarge" : "db.t4g.medium" selecting the right resource size per environment. Variable file structure per environment: a common.tfvars shared across all environments for values that should be identical (region, project name, tagging conventions), and an environment.tfvars per environment for the intentional differences. Environment parity validation: a script or CI step that compares the module versions and key variable values across environment configurations, alerting when environments diverge in ways that indicate accidental drift rather than intentional environment-specific configuration -- catching the case where a security group rule was added to production via the console but never replicated to staging, creating environments that behave differently in ways that break staging as a pre-production validation environment. Tagging strategy enforced via Terraform: every resource tagged with Environment, Team, Service, and ManagedBy = "terraform" as mandatory tags, enforced by a required_tags pre-condition in the module that fails the plan if any tag is missing.
Secrets and sensitive variable management
Sensitive Terraform variables -- database master passwords, API keys, certificate private keys -- managed so they never appear in version-controlled configuration files, plan output, or CI logs, and so rotation does not require a Terraform apply to update a hardcoded value. AWS Secrets Manager integration via the aws_secretsmanager_secret_version data source: Terraform reads the secret value at apply time rather than storing it in state or variables, with IAM policy granting only the CI role access to the specific secret ARN used by Terraform. sensitive = true annotation on all output values and variable definitions that contain credentials: Terraform marks these values as sensitive in plan output, replacing the value with (sensitive value) rather than printing the database password in the CI log or the PR comment. HashiCorp Vault dynamic secrets for database credentials in high-compliance environments: Vault generates a time-limited PostgreSQL or MySQL username and password on each Terraform apply rather than using a static credential -- the generated credential expires after a configurable TTL (typically 1-24 hours), limiting the blast radius of a credential leak. OIDC-based CI authentication for the CI pipeline's cloud credentials: GitHub Actions authenticates to AWS via the aws-actions/configure-aws-credentials OIDC action rather than using a stored AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY that never expire -- the CI pipeline receives a short-lived STS token scoped to only the IAM permissions required for the Terraform apply. Secret rotation procedures documented as runbooks: for each secret managed by Terraform, a documented procedure for rotating the value that specifies which Terraform terraform apply sequence is required and which downstream services need restart to pick up the new credential. Terraform state file encrypted with KMS CMK so sensitive values in state (database passwords are stored in state even when not shown in plan output) are protected at rest and KMS key usage is auditable.
Have infrastructure that lives in someone's head?
Tell us your current cloud setup, how environments are provisioned today, and what breaks when you need to recreate something. We'll scope the IaC migration and give you a fixed cost.
Terraform is the default choice for most infrastructure as code projects -- it has the largest community, the most provider coverage, and the most engineers who know it. HCL (Terraform's configuration language) is readable and purpose-built for declaring infrastructure. Pulumi is the better choice when your team wants to write infrastructure configuration in a general-purpose language (TypeScript, Python, Go) and take advantage of loops, conditionals, and abstractions that HCL handles awkwardly. For most projects, Terraform is the right starting point unless your team has a strong preference for writing infrastructure in their application language.
Existing resources created via the AWS or GCP console can be imported into Terraform state using terraform import. The process: write Terraform configuration that describes the existing resource, run terraform import to associate the configuration with the running resource, then run terraform plan to verify the configuration matches the actual state with no diff. Resources that show a diff need their configuration adjusted until plan shows no changes. The resource is now managed by Terraform without recreation. Complex existing infrastructure with many resources is imported incrementally, starting with the most critical resources.
Manual changes (drift) are detected the next time terraform plan runs -- the plan will show the change required to bring the actual state back into alignment with the configured state. The team then decides: update the Terraform configuration to reflect the intended change (making it permanent) or run terraform apply to revert the manual change. Drift alerts can be automated by running terraform plan on a schedule and alerting when the plan is non-empty. Preventing drift entirely requires removing console access for production infrastructure changes.
Writing Terraform for a new greenfield infrastructure typically runs $15,000 to $40,000 depending on the number of components and environments. Migrating existing click-ops infrastructure into Terraform state typically runs $20,000 to $60,000 depending on the complexity and size of the existing infrastructure. Fixed cost agreed before development starts.
Work with us
Tell us what you need. We'll tell you what it would take.
We scope Infrastructure as Code in 30 minutes. You walk away with a clear cost, timeline, and approach. No commitment required.
Scope and cost agreed before work starts. No surprises. No obligation.
Working prototype within 3 weeks of kickoff.
Pay by milestone. You see progress before each invoice.
60-day post-launch warranty. Bug fixes, UI tweaks, and deployment support. No retainer.