ByteGuideHub
DevOps & Infrastructure • • 10 min read

Self-Hosted Infrastructure as Code with Pulumi and Ansible: A Unified Approach

Learn how to combine Pulumi and Ansible for declarative management of cloud and on-premises resources. Step-by-step guide for hybrid environments with idempotent configuration.

Sponsored / Ad Space

Self-Hosted Infrastructure as Code with Pulumi and Ansible: A Unified Approach

Quick Summary (TL;DR)

  • Pulumi provisions cloud infrastructure using real programming languages (TypeScript, Python, Go).
  • Ansible configures servers—cloud or on-prem—over SSH with idempotent playbooks.
  • Combine them for a hybrid IaC workflow: Pulumi builds the boxes, Ansible fills them.
  • Wire them together via stack outputs → Ansible inventory, or trigger Ansible from Pulumi using the Command provider.
  • Keep state clean: Pulumi state in a remote backend, Ansible state (facts, Vault) in encrypted files.

Managing infrastructure across both cloud and on-premises environments is where most “single-tool” IaC strategies fall apart. Terraform is great at provisioning but weak at server configuration. Ansible is superb at configuration but not designed to create cloud resources declaratively. Pulumi sits in the middle—it can do both, but for traditional servers, Ansible still wins on ergonomics.

This guide shows you how to build a unified, idempotent, hybrid IaC pipeline using Pulumi for provisioning and Ansible for configuration.

Introduction to Unified Infrastructure as Code

Infrastructure as Code (IaC) has matured into two distinct disciplines:

  1. Provisioning — creating the resources themselves (VMs, networks, load balancers, databases).
  2. Configuration management — installing packages, managing files, starting services, and enforcing the desired state of a running machine.

Most teams pick one tool and stretch it. That works until you hit a hybrid environment: a cloud VPC on one side, a rack of bare-metal servers on the other, and a mandate to manage both from a single pipeline.

A unified approach means one repository, one CI/CD pipeline, one source of truth—even if two tools do the actual work.

Why Combine Pulumi and Ansible?

CapabilityPulumiAnsible
Cloud resource provisioning✅ Excellent⚠️ Limited (via modules)
On-prem server configuration⚠️ Requires APIs✅ Excellent (SSH, agentless)
Language modelTypeScript, Python, Go, C#YAML playbooks
State tracking✅ Explicit state file❌ Stateless (idempotent by design)
SecretsPulumi ESC, Vault, cloud KMSAnsible Vault
Best atDeclarative cloud APIsImperative-ish config drift correction

Pulumi gives you strong typing, loops, conditionals, and real test frameworks. Ansible gives you agentless reach into anything with SSH and Python—including legacy systems that will never expose a REST API.

Combining them means: Pulumi defines what exists, Ansible defines what’s running on it.

Prerequisites and Setup

Before you start, ensure you have:

  • Pulumi CLI (curl -fsSL https://get.pulumi.com | sh)
  • Ansible (pip install ansible-core)
  • A cloud account (AWS, Azure, or GCP) with credentials configured
  • SSH access to at least one on-prem or self-hosted server
  • A remote state backend (S3, Azure Blob, or Pulumi Cloud)

Initialize your project:

mkdir hybrid-iac && cd hybrid-iac
pulumi new aws-typescript
mkdir ansible && cd ansible
ansible-galaxy init roles/common

Your repository layout should look like this:

hybrid-iac/
├── index.ts              # Pulumi program
├── Pulumi.yaml
├── ansible/
│   ├── inventory.ini
│   ├── site.yml
│   └── roles/
└── .github/workflows/    # CI/CD

Step 1: Provision Cloud Resources with Pulumi

Start with a simple Pulumi program that creates a VPC, a subnet, and two EC2 instances.

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const vpc = new aws.ec2.Vpc("hybrid-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    tags: { Environment: "hybrid" },
});

const subnet = new aws.ec2.Subnet("hybrid-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    mapPublicIpOnLaunch: true,
});

const sg = new aws.ec2.SecurityGroup("hybrid-sg", {
    vpcId: vpc.id,
    ingress: [
        { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
        { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
    ],
    egress: [{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }],
});

const ami = aws.ec2.getAmi({
    mostRecent: true,
    owners: ["099720109477"], // Canonical
    filters: [{ name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] }],
});

const servers: aws.ec2.Instance[] = [];
for (let i = 0; i < 2; i++) {
    servers.push(new aws.ec2.Instance(`web-${i}`, {
        instanceType: "t3.micro",
        ami: ami.then(a => a.id),
        subnetId: subnet.id,
        vpcSecurityGroupIds: [sg.id],
        tags: { Name: `web-${i}`, Role: "webserver" },
    }));
}

// Export IPs for Ansible inventory
export const webIps = servers.map(s => s.publicIp);

Run it:

pulumi up

Pulumi now outputs webIps—the public IPs your Ansible inventory will consume.

Step 2: Configure On-Premises Servers with Ansible

Ansible’s job is to take any server—cloud or bare-metal—and bring it to a known state. Here’s a playbook that installs Nginx and configures a static site on both the cloud instances and your on-prem host.

# ansible/site.yml
- hosts: webservers
  become: true
  vars:
    site_root: /var/www/html
  tasks:
    - name: Ensure Nginx is installed
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: Deploy index page
      copy:
        dest: "{{ site_root }}/index.html"
        content: "<h1>Managed by Ansible</h1>"

    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: true

Because every task uses an idempotent module (apt, copy, service), you can run this playbook a hundred times and the system converges to the same state.

Step 3: Integrating Pulumi and Ansible for Hybrid Workflows

This is where the unified approach pays off. You have two integration patterns:

Generate the Ansible inventory dynamically from Pulumi outputs.

pulumi stack output webIps --json | jq -r '.[]' > ansible/cloud_hosts.txt

Then build a dynamic inventory file:

# ansible/inventory.ini
[webservers]
web-cloud-0 ansible_host=54.12.34.56
web-cloud-1 ansible_host=54.12.34.57
onprem-01    ansible_host=192.168.1.10

[webservers:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/id_rsa

Pattern B: Trigger Ansible from Pulumi

Use Pulumi’s Command provider to run Ansible as part of the deployment.

import { local } from "@pulumi/command";

const configure = new local.Command("run-ansible", {
    create: "ansible-playbook -i ansible/inventory.ini ansible/site.yml",
    triggers: [pulumi.output(webIps).apply(JSON.stringify)],
});

The triggers field ensures Ansible re-runs whenever the IPs change—keeping configuration in sync with provisioning.

The Flow

flowchart TD
    A[Pulumi Program] -->|pulumi up| B[Cloud Provider API]
    B --> C[VPC, Subnets, EC2]
    C --> D[Stack Outputs: IPs]
    D --> E{Integration Pattern}
    E -->|Pattern A| F[Dynamic Ansible Inventory]
    E -->|Pattern B| G[Pulumi Command Provider]
    F --> H[ansible-playbook]
    G --> H
    H --> I[On-Prem Servers]
    H --> J[Cloud Servers]
    I --> K[Configured Fleet]
    J --> K

Step 4: Managing State and Idempotency

Idempotency is the property that running the same operation twice produces the same result. Both tools support it, but they track state differently.

Pulumi State

Pulumi maintains an explicit state file. Store it remotely:

pulumi login s3://my-pulumi-state

Never commit state to Git. Use a locking backend (S3 + DynamoDB, or Pulumi Cloud) to prevent concurrent runs from corrupting state.

Ansible State

Ansible is stateless—it reads the current machine state each run and only applies changes when needed. This makes it naturally idempotent, but you must:

  • Use modules, not raw shell/command, whenever possible.
  • Add creates: or changed_when: guards to shell tasks.
  • Keep facts fresh (gather_facts: true) so conditionals work correctly.

Secrets

Secret TypeToolStorage
Cloud credentialsPulumiESC / cloud KMS
SSH keysAnsibleAnsible Vault
API tokensBothHashiCorp Vault
DB passwordsPulumi → AnsiblePulumi secret output → env var

Pulumi secrets flow into Ansible via environment variables:

export const dbPassword = pulumi.secret("super-secret-value");
- name: Configure app with DB password
  template:
    src: config.j2
    dest: /etc/app/config.yml
  vars:
    db_password: "{{ lookup('env', 'DB_PASSWORD') }}"

Comparison: Pulumi vs. Ansible vs. Terraform

FeaturePulumiAnsibleTerraform
LanguageTypeScript, Python, Go, C#YAMLHCL
Provisioning✅ Native⚠️ Via modules✅ Native
Config management⚠️ Limited✅ Native❌ Not designed for it
AgentlessN/A✅ SSHN/A
StateExplicit (backend)NoneExplicit (backend)
Learning curveMedium (needs a language)LowMedium
Hybrid cloud + on-prem✅ With Ansible✅ With Pulumi⚠️ Requires extra tooling
Ideal roleProvisionerConfiguratorProvisioner

Verdict: Use Pulumi + Ansible for hybrid environments. Use Terraform + Ansible if your team already standardizes on HCL.

Best Practices for Hybrid IaC

  1. One repo, two tools. Keep Pulumi code and Ansible playbooks versioned together so a single PR reflects an end-to-end change.
  2. Never hardcode IPs. Always derive Ansible inventory from Pulumi outputs.
  3. Use pulumi up --diff in CI to review changes before applying.
  4. Run Ansible with --check in pull requests to preview configuration drift.
  5. Separate concerns. Pulumi should not install packages; Ansible should not create VPCs.
  6. Tag everything. Consistent tags (Environment, Role, Owner) let Ansible target hosts by role rather than by hardcoded lists.
  7. Encrypt secrets at rest. Pulumi ESC for cloud secrets, Ansible Vault for playbook variables.
  8. Test idempotency. Run playbooks twice in CI; the second run should report zero changes.

A minimal CI pipeline looks like:

sequenceDiagram
    participant Dev as Developer
    participant CI as CI Pipeline
    participant Pulumi as Pulumi Backend
    participant Ansible as Ansible Runner
    participant Infra as Infrastructure

    Dev->>CI: Push PR
    CI->>Pulumi: pulumi preview
    Pulumi-->>CI: Diff
    CI->>Ansible: ansible-playbook --check
    Ansible-->>CI: Drift report
    Dev->>CI: Merge
    CI->>Pulumi: pulumi up
    Pulumi->>Infra: Provision resources
    CI->>Ansible: ansible-playbook site.yml
    Ansible->>Infra: Configure servers
    Infra-->>CI: Converged

Conclusion

Pulumi and Ansible are not competitors—they’re complements. Pulumi’s strength is declarative cloud provisioning in real programming languages; Ansible’s strength is agentless, idempotent configuration of any server that speaks SSH. Together they give you a single pipeline that spans cloud and on-premises infrastructure without forcing either tool to do something it wasn’t designed for.

Start small: provision one VPC with Pulumi, wire the outputs into an Ansible inventory, and run a single playbook. Once that loop is stable, expand it into CI, add Vault for secrets, and let the pipeline drive every change.

FAQ

What is the main benefit of using Pulumi and Ansible together? Pulumi excels at provisioning and managing cloud infrastructure using general-purpose programming languages, while Ansible is ideal for configuring and managing on-premises servers and applications. Combining them provides a unified workflow for hybrid environments, leveraging the strengths of both tools.

Can I use Pulumi to manage on-premises servers? Pulumi can manage on-premises resources if they are exposed via APIs (e.g., VMware, OpenStack), but for configuration management of traditional servers (e.g., installing packages, managing files), Ansible is more suitable due to its agentless, SSH-based approach.

How do I ensure idempotency when combining Pulumi and Ansible? Both Pulumi and Ansible are idempotent by design. Pulumi tracks state and only makes changes when necessary. Ansible modules are idempotent, meaning they can be run multiple times without changing the system if it’s already in the desired state. Ensure your playbooks and Pulumi code follow idempotent patterns.

What are some common use cases for hybrid IaC with Pulumi and Ansible? Common use cases include deploying applications that span cloud and on-premises environments, managing legacy systems alongside cloud-native services, and implementing disaster recovery setups where on-premises serves as a fallback.

How do I handle secrets management in a unified Pulumi and Ansible workflow? Use Pulumi’s secret management (e.g., Pulumi ESC, AWS Secrets Manager, HashiCorp Vault) to store sensitive data. For Ansible, use Ansible Vault to encrypt sensitive variables. Integrate them by having Pulumi output secrets that Ansible can consume via environment variables or encrypted files.

Is it possible to trigger Ansible playbooks from Pulumi? Yes, you can use Pulumi’s Command provider or dynamic providers to execute Ansible playbooks as part of your Pulumi deployment. This allows you to orchestrate both provisioning and configuration in a single workflow.

Sponsored / Ad Space