I've found a very good senior PHP opportunity! If anyone wants to have some fun, ping me please.

Get in touch
Building Cloud-Agnostic Infrastructure with Terraform and Ansible
Engineering

Building Cloud-Agnostic Infrastructure with Terraform and Ansible

Yoana Borissova
7 min
DevOps Terraform Ansible Infrastructure Cloud

The Challenge

I’ve done many deployments the old-fashioned way—SSH into a server, install packages manually, configure things by hand, hope nothing breaks. Not very proud of it, but that’s life. The real problem? Give it about 3 hours and you’ve completely forgotten what you installed, which config files you touched, and how you got it all working in the first place.

I was working on a real-time tracking application with an Ionic/Angular frontend and NestJS backend. Like many modern applications, it needed:

  • Multi-environment support (development and production)
  • Automated deployments via CI/CD
  • Easy scalability without vendor lock-in
  • Quick setup for new environments
  • Flexibility to change cloud providers without rewriting everything

The infrastructure needed to be just right — simple enough to understand and maintain, yet sophisticated enough to handle real-world requirements. And critically, documented in code so I wouldn’t forget what I did.

The Solution: Separation of Concerns

The Approach

I separated infrastructure provisioning from configuration management. This meant:

  • Terraform for creating and managing cloud resources
  • Ansible for installing and configuring software
  • Reverse proxy with automatic HTTPS
  • Process manager for application lifecycle
  • CI/CD pipeline for orchestration

Each tool has a single, clear responsibility. No overlapping concerns, no vendor lock-in.

The Architecture

┌─────────────────────────────────────────────┐
│            CI/CD Pipeline                   │
│                                             │
│  validate → plan → apply → configure        │
└──────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────┐
│              Terraform                      │
│                                             │
│  • Provisions VMs (cloud-agnostic)          │
│  • Creates SSH users                        │
│  • Outputs server IPs                       │
│  • Stores state remotely                    │
└──────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────┐
│              Ansible                        │
│                                             │
│  • Installs packages                        │
│  • Configures services                      │
│  • Sets up systemd units                    │
│  • Deploys application code                 │
│  • Manages environment variables            │
└──────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────┐
│          Application Server                 │
│                                             │
│  ┌─────────────────────────────────────┐   │
│  │  Reverse Proxy (Port 80/443)        │   │
│  │  • Automatic HTTPS                  │   │
│  │  • Static file serving              │   │
│  │  • API routing                      │   │
│  │  • WebSocket support                │   │
│  └──────────────┬──────────────────────┘   │
│                 │                           │
│                 ▼                           │
│  ┌─────────────────────────────────────┐   │
│  │  Application Backend                │   │
│  │  • REST API                         │   │
│  │  • Real-time features               │   │
│  │  • OAuth authentication             │   │
│  └──────────────┬──────────────────────┘   │
│                 │                           │
│                 ▼                           │
│  ┌─────────────────────────────────────┐   │
│  │  Database                           │   │
│  │  • Application data                 │   │
│  │  • User authentication              │   │
│  └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

What Made This Work

1. Environment Variables as First-Class Citizens

Instead of hardcoding configuration, everything flows through environment variables managed by CI/CD variables and Ansible group variables. The pipeline automatically selects the right configuration based on the branch:

  • dev branch → dev environment
  • master branch → production environment

No manual intervention. No opportunity for human error. Different log levels, CORS policies, and resource allocations for each environment.

2. Provider-Agnostic Terraform Definitions

The Terraform code uses variables to define server characteristics:

  • Server naming based on environment
  • Resource sizing that scales with environment (dev gets smaller instances, production gets more power)
  • Location/region selection
  • OS image selection

This pattern means the same Terraform modules work across different cloud providers with minimal changes.

3. Zero-Configuration HTTPS

Using a modern reverse proxy that handles TLS certificates automatically eliminated an entire category of operational complexity. No certificate renewal scripts, no manual Let’s Encrypt configuration—just declare your domain and it works.

The reverse proxy configuration handles:

  • Automatic HTTPS with certificate management
  • WebSocket upgrades for real-time features
  • Static file serving for the frontend
  • API routing to the backend
  • Compression and modern HTTP features

4. Idempotent Ansible Playbooks

The Ansible playbook is organized into logical sections:

  • System setup: packages, firewall, security hardening
  • Runtime environment: language runtimes and process managers
  • Database: database installation with proper authentication
  • Web server: reverse proxy with templated configuration
  • Application services: systemd unit files for automatic restarts
  • Observability: logging and metrics collection

Each section is idempotent — run it 100 times, get the same result. This makes deployments predictable and safe.

The Magic Moment: Changing Cloud Providers

Here’s where the design really paid off. Six months into production, I needed to switch VM providers for cost optimization.

How long did it take?

About 15 minutes.

What changed:

  • The Terraform provider configuration
  • The Terraform resource definitions (from one cloud provider’s VM format to another’s)
  • The authentication credentials in CI/CD variables

What didn’t change:

  • The Ansible playbooks — zero modifications
  • The application deployment pipeline
  • The monitoring setup
  • Any application code

Ansible doesn’t care what cloud provider it’s talking to. It just needs an IP address and SSH access. This is the power of proper separation of concerns.

This wasn’t hypothetical—I actually did this. Changed providers, ran the pipeline, and everything came up on the new infrastructure without touching any configuration management code.

Metrics That Matter

Let’s talk numbers:

MetricValue
Initial setup time~4 hours (including learning curve)
Time to provision new environment~15 minutes
Deployment time (app updates)~3 minutes
Infrastructure code~400 lines total
Configuration code~300 lines
Time to switch cloud providers~15 minutes
Cloud provider lock-inZero

The setup was fast, but more importantly, it’s been maintainable. Months later, I can still read the code and understand exactly what’s happening. No magic, no hidden complexity.

Lessons Learned

1. Separation of Concerns Enables Flexibility

By keeping infrastructure provisioning and configuration management separate, I gained true cloud portability. The configuration layer doesn’t know or care about the cloud provider.

2. Automation Pays Off Immediately

Even for smaller projects, the initial time investment in CI/CD and Infrastructure as Code pays dividends from day one. No more “works on my machine” issues, and every deployment is documented in code.

3. Start Simple, Scale When Needed

Choosing the right level of complexity doesn’t mean building poorly. It means understanding your requirements and choosing tools that match your current needs while allowing for future growth.

4. Environment Parity Matters

Having dev and production environments that are truly identical (just different sizes) eliminates an entire class of deployment bugs. What works in dev works in production.

5. Provider Independence Is Freedom

The ability to evaluate different cloud providers without fear of migration pain is incredibly valuable. It keeps vendors honest and costs under control.

The Bottom Line

This infrastructure isn’t fancy. It doesn’t use the latest hype technology. It won’t win any architecture beauty contests.

But it works. It’s fast. It’s maintainable. And when I needed to change the cloud provider, it bent instead of breaking.

The Terraform + Ansible pattern taught me what each layer does and why it matters. Container orchestration platforms are powerful, but understanding the fundamentals—provisioning, configuration, process management, reverse proxies—makes you a better engineer regardless of which tools you eventually use.

That’s what pragmatic infrastructure looks like.


Want to Learn More?

This approach follows standard patterns you can apply to any web application:

  • Terraform for declarative infrastructure provisioning
  • Ansible for imperative system configuration
  • CI/CD pipelines for orchestration and automation
  • Environment-based branching for safe, predictable deployments
  • Native service managers for process lifecycle management