Infrastructure as Code: The Platform Foundation
Infrastructure as Code (IaC) is the principle that infrastructure is defined, versioned, and managed through declarative or imperative code, rather than manual configurations. In the context of Platform Engineering, IaC is not just a best practice: it is a fundamental requirement for building an IDP that is repeatable, scalable, and auditable.
In this article, we will compare the two dominant approaches - Terraform (declarative) and Pulumi (imperative with general-purpose languages) - and explore how to implement Policy as Code with OPA and Sentinel to ensure automated compliance and security.
What You'll Learn
- IaC paradigms: declarative (Terraform/HCL) vs imperative (Pulumi/TypeScript)
- State management: remote state, locking, disaster recovery
- Reusable Terraform modules and composition
- Pulumi: IaC with TypeScript, Python, and Go
- Policy as Code: OPA/Rego for compliance and Sentinel for Terraform Cloud
- Migration strategies toward IaC for existing infrastructure
Terraform: The Declarative Approach
HashiCorp's Terraform is the most widely used IaC tool in the world. It uses a declarative language called HCL (HashiCorp Configuration Language) to describe the desired infrastructure state. Terraform compares the desired state with the current state and computes an execution plan to reach the target configuration.
Terraform's strengths include:
- Provider ecosystem: supports hundreds of cloud and SaaS providers (AWS, Azure, GCP, Kubernetes, GitHub, Datadog...)
- Plan before apply: shows changes before executing them, reducing the risk of errors
- State management: tracks real infrastructure state for managing incremental updates
- Modules: reusable components that encapsulate complex configurations
# Terraform: reusable module for microservice infrastructure
# modules/microservice-infra/main.tf
variable "service_name" {
type = string
description = "Microservice name"
}
variable "environment" {
type = string
description = "Environment (dev, staging, production)"
}
variable "cpu_limit" {
type = string
default = "500m"
}
variable "memory_limit" {
type = string
default = "512Mi"
}
# Kubernetes namespace with standard labels
resource "kubernetes_namespace" "service" {
metadata {
name = "${var.service_name}-${var.environment}"
labels = {
"app.kubernetes.io/name" = var.service_name
"app.kubernetes.io/env" = var.environment
"platform.company.io/managed" = "true"
}
}
}
# PostgreSQL database (RDS)
resource "aws_db_instance" "database" {
identifier = "${var.service_name}-${var.environment}"
engine = "postgres"
engine_version = "15.4"
instance_class = var.environment == "production" ? "db.r6g.large" : "db.t3.micro"
allocated_storage = 20
max_allocated_storage = var.environment == "production" ? 100 : 50
storage_encrypted = true
db_name = replace(var.service_name, "-", "_")
username = "app_user"
password = random_password.db_password.result
backup_retention_period = var.environment == "production" ? 30 : 7
deletion_protection = var.environment == "production"
tags = {
Service = var.service_name
Environment = var.environment
ManagedBy = "terraform"
}
}
# Redis cache (ElastiCache)
resource "aws_elasticache_cluster" "cache" {
cluster_id = "${var.service_name}-${var.environment}"
engine = "redis"
node_type = var.environment == "production" ? "cache.r6g.large" : "cache.t3.micro"
num_cache_nodes = 1
parameter_group_name = "default.redis7"
port = 6379
}
output "namespace" {
value = kubernetes_namespace.service.metadata[0].name
}
output "database_endpoint" {
value = aws_db_instance.database.endpoint
}
output "cache_endpoint" {
value = aws_elasticache_cluster.cache.cache_nodes[0].address
}
Pulumi: IaC with General-Purpose Languages
Pulumi takes a different approach: instead of a dedicated language (like HCL), it allows defining infrastructure using standard programming languages such as TypeScript, Python, Go, C#, and Java. This means having access to constructs like loops, conditionals, functions, classes, and the entire library ecosystem of the chosen language.
Pulumi's advantages include:
- Familiar languages: no need to learn a new language; use the one your team already knows
- Native testing: unit tests and integration tests for infrastructure with standard testing frameworks
- Powerful abstractions: classes, interfaces, and composition for creating complex IaC components
- IDE support: autocomplete, type checking, and refactoring with language tools
// Pulumi: reusable component in TypeScript
// microservice-infra.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as k8s from "@pulumi/kubernetes";
interface MicroserviceInfraArgs {
serviceName: string;
environment: string;
cpuLimit?: string;
memoryLimit?: string;
enableDatabase?: boolean;
enableCache?: boolean;
}
export class MicroserviceInfra extends pulumi.ComponentResource {
public readonly namespace: k8s.core.v1.Namespace;
public readonly databaseEndpoint?: pulumi.Output<string>;
public readonly cacheEndpoint?: pulumi.Output<string>;
constructor(
name: string,
args: MicroserviceInfraArgs,
opts?: pulumi.ComponentResourceOptions
) {
super("company:platform:MicroserviceInfra", name, {}, opts);
const isProd = args.environment === "production";
// Kubernetes namespace
this.namespace = new k8s.core.v1.Namespace(
`${args.serviceName}-ns`,
{
metadata: {
name: `${args.serviceName}-${args.environment}`,
labels: {
"app.kubernetes.io/name": args.serviceName,
"app.kubernetes.io/env": args.environment,
"platform.company.io/managed": "true",
},
},
},
{ parent: this }
);
// Conditional database
if (args.enableDatabase !== false) {
const db = new aws.rds.Instance(
`${args.serviceName}-db`,
{
identifier: `${args.serviceName}-${args.environment}`,
engine: "postgres",
engineVersion: "15.4",
instanceClass: isProd ? "db.r6g.large" : "db.t3.micro",
allocatedStorage: 20,
storageEncrypted: true,
deletionProtection: isProd,
},
{ parent: this }
);
this.databaseEndpoint = db.endpoint;
}
this.registerOutputs({
namespaceName: this.namespace.metadata.name,
});
}
}
State Management
State management is one of the most critical aspects of IaC. The state file contains the mapping between resources defined in code and actual resources in the cloud. Poor state management can lead to drift, conflicts, and even resource loss.
- Remote state: state must be stored in a remote backend (S3, GCS, Azure Blob, Terraform Cloud) to be shared among team members
- State locking: locking mechanism (DynamoDB for AWS, GCS for GCP) to prevent concurrent modifications
- State encryption: state contains sensitive information and must be encrypted at rest and in transit
- Disaster recovery: regular state backups with versioning enabled on the bucket
Fundamental Rule
Never commit the state file to the Git repository. State contains sensitive information (passwords, connection strings, keys) and must be managed exclusively through a remote backend with appropriate encryption and access control.
Policy as Code with OPA and Sentinel
Policy as Code is the principle of defining organizational policies as versioned, testable, and automatically enforceable code. In the IaC context, policies verify that defined infrastructure meets security, compliance, and cost requirements before deployment.
- OPA (Open Policy Agent): general-purpose policy engine with Rego language. Can validate Terraform plans, Kubernetes manifests, API requests
- Sentinel: HashiCorp's policy framework integrated into Terraform Cloud/Enterprise. Uses a dedicated language optimized for infrastructure policies
- Kyverno: Kubernetes-native policy engine that uses YAML to define policies (simpler than OPA for K8s-specific use cases)
# OPA/Rego: policy for validating Terraform plans
# policy/terraform/mandatory_tags.rego
package terraform.mandatory_tags
import rego.v1
# All resources must have mandatory tags
mandatory_tags := ["Environment", "Service", "ManagedBy", "Team"]
# Find resources missing mandatory tags
deny contains msg if {
resource := input.planned_values.root_module.resources[_]
tags := object.get(resource.values, "tags", {})
required_tag := mandatory_tags[_]
not tags[required_tag]
msg := sprintf(
"Resource '%s' is missing mandatory tag '%s'",
[resource.address, required_tag]
)
}
# Production databases must have encryption enabled
deny contains msg if {
resource := input.planned_values.root_module.resources[_]
resource.type == "aws_db_instance"
resource.values.storage_encrypted != true
msg := sprintf(
"Database '%s' must have storage_encrypted = true",
[resource.address]
)
}
# No public resources without approval
deny contains msg if {
resource := input.planned_values.root_module.resources[_]
resource.type == "aws_s3_bucket"
acl := object.get(resource.values, "acl", "private")
acl == "public-read"
msg := sprintf(
"S3 bucket '%s' cannot be public without approval",
[resource.address]
)
}
GitOps and PR-Based Workflows
Integrating IaC with GitOps workflows is essential for a mature IDP. The most effective pattern is the PR-based workflow: every infrastructure change happens through a Pull Request that is automatically validated before merging.
- Atlantis: tool that automates the Terraform workflow on Pull Requests (automatic plan, apply after approval)
- Spacelift: SaaS platform for IaC management with policies, drift detection, and module registry
- Env0: platform that adds governance, cost tracking, and TTL to IaC environments
The advantage of PR-based workflows is complete traceability: every infrastructure change is documented in a PR with reviews, comments, and approvals. This naturally satisfies audit and compliance requirements.
Migration Strategies
Migrating existing infrastructure (created manually or with ad-hoc scripts) to IaC is a process that requires planning and gradual implementation:
- Phase 1 - Import: import existing resources into Terraform state without modifying them
- Phase 2 - Codify: write IaC code describing imported resources, verifying the plan shows no changes
- Phase 3 - Standardize: progressive refactoring toward shared modules and naming conventions
- Phase 4 - Automate: CI/CD integration for automatic apply and policy enforcement
Migration Advice
Do not try to migrate all infrastructure at once. Start with simpler, lower-risk components (S3 buckets, DNS records, IAM policies), gain experience and confidence, then proceed with more critical resources (databases, Kubernetes clusters, networking).
Related deep dive: 04 - Backstage.io: Platform Portal per Developer Self-Service e Service Catalog.
Related deep dive: 06 - Service Catalog e CMDB: Gestione e Governance dei Servizi.
Frequently asked questions
What is Infrastructure as Code (IaC) and its significance in Platform Engineering?
IaC is the principle that infrastructure is defined, versioned, and managed through declarative or imperative code, rather than manual configurations. In Platform Engineering, it is a fundamental requirement for building an IDP that is repeatable, scalable, and auditable.
What are the two dominant approaches to Infrastructure as Code mentioned in the article?
The two dominant approaches are Terraform (declarative) and Pulumi (imperative with general-purpose languages).
Why should state management be handled carefully when using IaC tools?
State management is critical, and poor handling can lead to drift, conflicts, and even resource loss. It's recommended not to commit the state file to the Git repository.
What is the advantage of integrating IaC with GitOps workflows in a mature IDP?
The most effective pattern is the PR-based workflow, which provides complete traceability for every infrastructure change, naturally satisfying audit and compliance requirements.
Commenti
Caricamento commenti...
Sign in to comment