Setting Up a Production-Ready Kubernetes Cluster with Terraform
Provision a secure, scalable Kubernetes cluster with Terraform, including networking, node pools, access control, and deployment considerations.
Setting Up a Production-Ready Kubernetes Cluster with Terraform
Most Kubernetes tutorials stop right after the cluster comes up.
That’s fine for learning. It’s not fine for production.
A cluster that is merely working can still be fragile, expensive, difficult to secure, and painful to operate. Production readiness means more than provisioning control planes and worker nodes. It means designing for failure, keeping blast radius small, making access explicit, and ensuring the platform is maintainable by humans at 2 a.m.
Terraform is a strong foundation because it gives you repeatability and reviewability. But Terraform alone does not make infrastructure production-ready. The real work is in the architecture and operational discipline around it.
Thesis: Terraform gives you deterministic provisioning, but production readiness comes from secure networking, least-privilege access, resilient node design, and a deployment model you can actually support over time.
In this guide, we’ll build a mental model for a production-grade Kubernetes environment and walk through the practical considerations that separate a demo cluster from a platform you’d trust with real workloads.
What “production-ready” actually means
A production-ready Kubernetes cluster should satisfy a few non-negotiable requirements:
- Isolated networking with clear separation between public and private surfaces
- Secure access control using IAM, RBAC, and secrets management
- Scalable compute with node pools and autoscaling
- Operational visibility through logging, metrics, and audit trails
- Safe deployment paths using ingress, rollout strategies, and health checks
- Repeatability so environments can be recreated, patched, and evolved consistently
If any of those are missing, you don’t really have a platform. You have a fragile collection of resources.
Baseline architecture: build the network before the cluster
The biggest mistake I see engineers make is creating the Kubernetes cluster first and deciding on networking later. That reverses the dependency chain.
Your cluster sits inside a network boundary, and that boundary controls everything from node placement to ingress exposure to private service connectivity. Whether you are on AWS, GCP, or Azure, the same core principles apply.
A sensible baseline layout
At minimum, plan for:
- A dedicated VPC/VNet for the environment
- Multiple subnets across availability zones
- Private subnets for worker nodes and internal services
- Public subnets only where internet-facing components must live
- NAT or egress controls for outbound access from private workloads
- Route tables and security groups / firewall rules that are intentionally scoped
For most teams, the right default is:
- Managed Kubernetes control plane outside your direct node management
- Worker nodes in private subnets
- Ingress exposed through a load balancer or ingress controller
- Datastores kept outside the cluster unless you have a strong reason otherwise
Watch Out: Putting worker nodes in public subnets because it is “simpler” often creates long-term security debt. It may work, but it expands your attack surface and makes access control harder to reason about.
Cluster topology matters
A single-node or single-AZ cluster is not production-ready, no matter how elegant the Terraform is. Production workloads need failure-domain awareness.
At a minimum, aim for:
- Multiple availability zones
- At least two worker pools if your workloads have different resource or isolation needs
- Separation between system workloads and application workloads
- Dedicated node pools for latency-sensitive, batch, or high-memory services
Here is the kind of topology I recommend most teams start with:
| Layer | Recommendation | Why it matters |
|---|---|---|
| VPC/VNet | Dedicated environment network | Reduces coupling and accidental exposure |
| Subnets | Multi-AZ private subnets for nodes | Improves resilience |
| Ingress | Load balancer + ingress controller | Centralizes and standardizes exposure |
| Node pools | Separate system and app pools | Prevents noisy-neighbor issues |
| Data services | Managed databases and caches | Lowers operational burden |
Terraform structure: keep it boring and modular
Terraform projects become unmaintainable when they turn into a pile of copy-pasted resources. Production infrastructure needs structure.
The goal is not to make the repository look clever. The goal is to make changes safe, obvious, and reviewable.
A practical Terraform layout
A common layout is:
modules/for reusable infrastructure componentsenvs/dev,envs/staging,envs/prodfor environment-specific stacksvariables.tffor input definitionsoutputs.tffor exported valuesproviders.tffor cloud provider and Kubernetes provider configurationbackend.tffor remote state setup
A simple example:
# envs/prod/main.tf
module "network" {
source = "../../modules/network"
environment = "prod"
cidr_block = "10.20.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
module "kubernetes" {
source = "../../modules/kubernetes"
environment = "prod"
vpc_id = module.network.vpc_id
private_subnet_ids = module.network.private_subnet_ids
cluster_version = "1.30"
node_min_size = 3
node_desired_size = 6
node_max_size = 12
}This is not fancy, and that’s the point.
Use remote state from day one
State is not an implementation detail. It is a critical dependency.
Store Terraform state in a remote backend with locking enabled. If you are on AWS, that usually means S3 plus DynamoDB locking. On GCP or Azure, use the equivalent managed storage and locking mechanisms.
Remote state gives you:
- Team collaboration
- Consistent plan/apply workflows
- State durability
- Reduced risk of accidental local corruption
Pro Tip: Separate state by environment and by blast radius. A networking stack, cluster stack, and application stack do not always need to share the same state file.
Environment separation should be real, not just names
A lot of teams create dev, staging, and prod directories but still share too much underneath.
Good environment separation means:
- Different state files
- Different credentials or access paths
- Different quotas and guardrails
- Different approval policies
- Different assumptions about cost and resiliency
If your staging environment can accidentally mutate production resources, your setup is not isolated enough.
Cluster security: least privilege is not optional
Kubernetes security is a multi-layer problem. Cloud IAM, Kubernetes RBAC, network policy, secrets, and audit logging all need to work together.
IAM and RBAC should be mapped intentionally
Do not hand out cluster-admin to everyone because it is convenient.
Use cloud IAM to control who can reach the cluster and Kubernetes RBAC to control what they can do once inside.
A healthy pattern is:
- SRE/platform team: elevated operational access
- Application teams: namespace-scoped permissions
- CI/CD service accounts: tightly scoped deployment permissions
- Break-glass access: rare, audited, and time-bound
The principle is simple: the fewer identities with broad permissions, the better.
Secrets require a real strategy
A production cluster should not rely on plaintext Kubernetes Secrets as its only control. Base64 encoding is not security.
Prefer one of these patterns:
- External secrets manager integration
- Short-lived credentials issued at runtime
- Secret encryption at rest with cloud-managed keys
- Namespace-scoped access with explicit ownership
Here is a practical rule: if a secret can be rotated without redeploying the entire platform, you are in a much better place.
Important: Never commit cloud credentials, database passwords, or API tokens into Terraform variables, Git history, or container images. If secrets are present in state, treat that state as sensitive data.
Enable audit logging early
Many teams defer audit logging until they have an incident. That is backwards.
You want logs for:
- API server actions
- Authentication and authorization events
- Changes to RBAC and cluster-scoped resources
- Admission decisions if you use policy engines
Without audit logs, investigations become guesswork.
Network controls still matter inside the cluster
Kubernetes namespaces are organizational boundaries, not security boundaries by themselves.
Use:
- Network policies where supported
- Security groups or firewall rules at the cloud layer
- Pod security controls
- Admission policies for risky workloads
The real goal is to make lateral movement expensive.
Scaling and resilience: design for failure, not hope
A production cluster should scale gracefully and survive common failure modes.
Use multiple node pools
Node pools let you match infrastructure to workload characteristics.
Typical pools include:
- System pool for cluster add-ons and controllers
- General app pool for standard services
- Memory-optimized pool for caches or JVM services
- Compute-optimized pool for CPU-bound workloads
- Spot/preemptible pool for fault-tolerant batch jobs
Here’s how I think about the tradeoff:
| Node Pool Type | Cost | Reliability | Best For | Caveat |
|---|---|---|---|---|
| On-demand general | Medium | High | Core services | Less cost efficient |
| Spot / preemptible | Low | Lower | Batch, async jobs | Can be interrupted |
| Memory-optimized | Higher | High | JVM, data processing | Can be expensive |
| Dedicated system | Medium | High | Platform components | Needs careful sizing |
Autoscaling is necessary, but not sufficient
Autoscaling is useful only if your workloads are requests/limits aware and your cluster has room to place new pods.
You typically need both:
- Horizontal Pod Autoscaler for application-level scaling
- Cluster autoscaler or equivalent for node-level scaling
And you need to set realistic resource requests. If every service asks for tiny requests and then bursts unpredictably, the scheduler will make bad decisions under load.
Plan upgrades deliberately
Kubernetes upgrades are normal maintenance, not exceptional events.
You should know:
- How often control plane versions are supported
- How node images are rotated
- Whether add-ons are compatible with the target version
- What the rollback path is if an upgrade exposes a bug
Never let the cluster drift so far behind that upgrading becomes a weekend rescue project.
A good upgrade process is boring: read release notes, test in non-production, upgrade incrementally, and validate workloads before moving on. :::
Failure domains are a design feature
An availability zone is not just a placement option. It is a failure boundary.
Distribute replicas across zones, avoid single-zone dependencies, and verify that your load balancers, storage classes, and node pools actually honor the fault domain assumptions you made on paper.
Deployment considerations: how workloads reach users
A cluster can be perfectly provisioned and still be operationally awkward if the deployment model is weak.
Ingress should be standardized
Most teams should not expose services directly with arbitrary load balancers unless there is a strong reason.
A better approach is:
- One or a small number of ingress controllers
- Standard TLS termination
- Consistent path or host-based routing
- Centralized certificate management
- Explicit timeouts and body-size limits
This creates a predictable surface for apps and makes security review easier.
Service exposure needs policy, not improvisation
Be explicit about which services are:
- Internal only
- Accessible only from other namespaces
- Exposed publicly
- Restricted to admin or partner traffic
Not every service deserves a public endpoint.
Rollout strategy is part of infrastructure design
Terraform provisions the platform, but the deployment strategy determines how safely you change applications on it.
Prefer rollout methods that reduce blast radius:
- Rolling updates with readiness and liveness checks
- Blue/green for high-risk changes
- Canary when you can measure user impact well
- Feature flags for behavioral changes
If your deployment process cannot catch a bad release before it reaches all users, the platform is not giving you enough protection.
Here is a simple deployment decision guide:
| Strategy | Risk | Speed | Operational Complexity | Use When |
|---|---|---|---|---|
| Rolling | Low to medium | Fast | Low | Most services |
| Blue/green | Low | Medium | Medium | High-risk releases |
| Canary | Lowest | Slower | Higher | You have good metrics and traffic shaping |
| Recreate | High | Fast | Low | Only for stateless non-critical systems |
A deployment and provisioning flow that actually works
A healthy sequence looks like this:
This flow is intentionally conservative. Production infrastructure should optimize for correctness before convenience.
Practical Terraform patterns that reduce pain
Let’s get specific about a few Terraform habits that matter.
1. Pin provider and module versions
Use explicit version constraints so upgrades are intentional.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}2. Keep sensitive values out of plan output when possible
Mark variables sensitive and source secrets from a proper secret store rather than hardcoding them into variables files.
3. Make defaults safe
Your module defaults should represent secure, production-minded assumptions.
For example:
- Private subnets by default
- Encryption enabled by default
- Logging enabled by default
- Public exposure disabled by default
That way, the safe path is the easy path.
4. Add policy checks to CI
Your pipeline should catch risky infra before it reaches apply.
Common checks include:
terraform fmtterraform validatetflintcheckovor similar policy scanning- OPA/Conftest policies for custom rules
Common pitfalls and maintenance tasks
A lot of cluster failures are not dramatic. They are the result of neglected basics.
Common pitfalls
- Overly broad IAM permissions
- Worker nodes deployed in public subnets without a strong reason
- No remote state locking
- Single-AZ cluster design
- No version upgrade strategy
- Secrets stored in Git or state without proper controls
- Missing resource requests and limits
- No observability on critical control paths
Ongoing maintenance tasks
Production Kubernetes is not “set and forget.” It needs regular care.
You should schedule:
- Kubernetes version reviews
- Node image and patch updates
- Certificate rotation checks
- IAM/RBAC access reviews
- Secret rotation
- Cost review for overprovisioned node pools
- Backup and restore validation
- Incident drills and rollback rehearsals
If you are not testing restore procedures, you do not really know your recovery posture.
Pro Tip: Build a quarterly infrastructure review checklist. The goal is not to create bureaucracy; it is to keep drift from quietly turning your platform into technical debt.
A practical checklist for production readiness
Use this as a final sanity check before calling your cluster production-ready:
- VPC/VNet and subnets are designed for multi-AZ resilience
- Control plane and worker nodes are not publicly exposed unnecessarily
- Remote Terraform state is enabled with locking
- Terraform code is modular and environment-separated
- IAM and RBAC are scoped to least privilege
- Secrets are managed outside plain Kubernetes manifests
- Audit logging is enabled and retained
- Multiple node pools exist for system and application workloads
- Autoscaling is configured and tested
- Upgrade path is documented and rehearsed
- Ingress and service exposure are standardized
- Deployment strategy includes rollback or canary options
- Monitoring, logging, and tracing are in place
- Backup and restore have been validated
Conclusion
Terraform is an excellent tool for Kubernetes provisioning, but it is only one part of the system. A production-ready cluster is the result of deliberate architecture choices: secure networking, careful access control, resilient scaling, and operational habits that reduce surprises.
If you remember only one thing, make it this: production readiness is a property of the whole platform, not a checkbox on the cluster resource.
Start with a secure network foundation. Keep Terraform modular and stateful in a disciplined way. Treat IAM, RBAC, and secrets as first-class design concerns. Then validate that scaling, upgrades, and deployment workflows will still behave when things go wrong—not just when everything is green.
That is the difference between infrastructure that merely exists and infrastructure you can trust.
Further reading
- Kubernetes documentation on cluster administration and security
- Terraform documentation on remote state and module composition
- Cloud provider guidance for managed Kubernetes services
- CNCF materials on observability, policy, and platform engineering