Learn Vault - Vault Integration with Infrastructure as Code (IaC - Terraform & Ansible)
Episode 19 of 26

Learn Vault - Vault Integration with Infrastructure as Code (IaC - Terraform & Ansible)

Let your infrastructure issue its own dynamic database credentials and PKI certificates at provisioning time — via the Terraform Vault Provider for infrastructure declaration and the community.hashi_vault lookup plugin for server configuration with Ansible.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Introduction

After covering in episode 18 how CI/CD pipelines borrow temporary credentials from Vault instead of storing permanent API keys, this episode applies the same philosophy to a more fundamental layer: the infrastructure itself. We'll cover Vault integration with the two most popular IaC tools in the industry — Terraform and Ansible.

Why is this topic so important? Because so far there's a great irony in the IaC world: we automate the creation of servers, networks, and databases, but the credentials that infrastructure itself needs are still hardcoded or stored in config files. A real example: a Terraform module that creates a PostgreSQL database then stores its admin password as a plaintext variable in the repository — and worse, that password is stored forever in the terraform state file. Or an Ansible playbook writing static credentials to /etc/myapp/config.yml on thousands of servers with no rotation mechanism.

With Vault integration, the correct pattern changes completely: infrastructure requests its credentials from Vault at provisioning time, and those credentials are born with a limited lifetime. A database created by Terraform can use dynamic credentials that expire within hours. Internal TLS certificates for apps can be issued on-demand with a 24-hour TTL and rotated automatically. And servers managed by Ansible never store secrets on disk — they read directly from Vault every time the playbook runs. Let's cover them one by one.

Main Discussion

Why IaC Needs Vault Integration

Before diving into code, let's dissect the three main problems this integration solves:

ProblemWithout VaultWith Vault
Secrets in state/logPassword visible in plaintext in tfstate & logsSecrets never "settle" — only fetched at runtime
RotationManual, rarely doneAutomatic via dynamic secrets / PKI TTL
Credentials persisting on server disksConfig file contains permanent secretsSecret fetched per run, not stored
AuditNo knowledge of who read a secret and whenEvery read recorded in Vault's audit log

The essence: Vault breaks the cycle of "settling secrets." Terraform and Ansible remain the source of truth for infrastructure, but Vault becomes the source of truth for its credentials.

Terraform: Vault Provider

HashiCorp provides the official hashicorp/vault provider for Terraform. There are three main capabilities we'll use:

  1. Reading secrets — fetching values from Vault at provisioning time (data sources).
  2. Issuing credentials — dynamic database credentials and PKI certificates with short TTLs (data sources).
  3. Writing data — creating secrets, mounts, and configuring secrets engines (resources).

Provider Configuration & Authentication

The Vault provider needs to know the server address and how to log in. In production environments, never put a token in code — use the auth_login block with AppRole, or let the provider read VAULT_TOKEN from the environment:

providers.tf
provider "vault" {
  address = var.vault_addr
 
  auth_login {
    path = "auth/approle/login"
    params = {
      role_id   = var.vault_role_id
      secret_id = var.vault_secret_id
    }
  }
}
 
variable "vault_addr" {
  default = "https://vault.example.com:8200"
}
 
variable "vault_role_id" {}
variable "vault_secret_id" {}

The vault_role_id and vault_secret_id values should be passed via environment variables (TF_VAR_vault_role_id) or taken from the CI/CD pipeline we built in episode 18 — not written in terraform.tfvars.

Tip

The Vault provider reads VAULT_ADDR and VAULT_TOKEN automatically from the environment if available. For the purest bootstrap scenario, give the provider access via VAULT_TOKEN from the pipeline (that token itself is short-lived), or use auth_login with AppRole if your pipeline already holds the RoleID/SecretID.

Reading KV Secrets: vault_kv_secret_v2 vs vault_generic_secret

For reading secrets from the KV secrets engine, the data source choice depends on the engine version:

data "vault_kv_secret_v2" "app" {
  mount = "kv"
  name  = "ci/app"
}
 
# Access the value: data.vault_kv_secret_v2.app.data["DB_PASSWORD"]

vault_kv_secret_v2 is the recommended choice because it supports versioning, metadata, and rollback — features we covered in episode 4. A real-world example: filling an app password into a database resource created by Terraform:

main.tf
data "vault_kv_secret_v2" "app" {
  mount = "kv"
  name  = "postgres/app"
}
 
resource "aws_db_instance" "app_db" {
  identifier     = "app-db"
  engine         = "postgres"
  instance_class = "db.t3.micro"
 
  username = data.vault_kv_secret_v2.app.data["DB_USERNAME"]
  password = data.vault_kv_secret_v2.app.data["DB_PASSWORD"]
 
  skip_final_snapshot = true
}

Dynamic Database Credentials: vault_database_creds

This is the feature that makes Vault+Terraform integration extremely powerful. Instead of reading a static password from KV, we can ask Vault to create new database credentials that expire automatically:

db-creds.tf
data "vault_database_creds" "app" {
  backend = "database"
  role    = "deploy-reader"
}
 
# Dynamic username & password, valid for the role's TTL
resource "aws_db_instance" "app_db" {
  ...
  username = data.vault_database_creds.app.username
  password = data.vault_database_creds.app.password
}

Every time terraform apply runs, Vault creates a new user in PostgreSQL/MySQL with privileges matching the role (e.g. deploy-reader), then removes it automatically when the lease ends. This is the episode 5 paradigm applied to IaC: a leaked credential is a credential that's already invalid.

Warning

Be careful: data "vault_database_creds" produces new credentials every time Terraform reads the data source. Because of that, the referenced value will force a replacement of resources depending on it (e.g. aws_db_instance). If you need stable credentials over the long term, use a KV secret filled by a separate rotation process — or make sure the provider writes the credentials into Vault KV after creating the resource (the pattern covered in the "Writing Data" section).

PKI Certificates: vault_pki_secret

The same pattern applies to internal TLS certificates (episode 7). Terraform can issue a certificate from Vault PKI with a short TTL when creating a resource that needs it:

pki.tf
data "vault_pki_secret" "app_cert" {
  backend     = "pki_int"
  name        = "internal-app"
  common_name = "api.internal.example.com"
  alt_names   = ["api.internal.example.com"]
  ttl         = "24h"
}
 
resource "aws_lb_listener" "app" {
  load_balancer_arn = aws_lb.app.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
 
  certificate_arn = data.vault_pki_secret.app_cert.cert_chain
}

Because the TTL is short, Terraform will automatically issue a new certificate whenever the data source is refreshed (for example on each re-plan/apply). This enforces periodic rotation without humans — precisely eliminating the "certificate expired overnight" problem we covered in episode 7.

Writing Data to Vault from Terraform

Terraform can also be a manager for Vault itself — creating mounts, writing KV secrets, and configuring secrets engines. Example: creating a kv mount (KV v2) and writing an app bootstrap secret:

vault-config.tf
resource "vault_mount" "kv" {
  path        = "kv"
  type        = "kv-v2"
  description = "KV v2 for application secrets"
}
 
resource "vault_kv_secret_v2" "bootstrap" {
  mount = vault_mount.kv.path
  name  = "postgres/app"
 
  data_json = jsonencode({
    DB_USERNAME = "app_user"
    DB_PASSWORD = random_password.app_password.result
  })
}
 
resource "random_password" "app_password" {
  length  = 24
  special = true
}

Note the interesting pattern above: the password is created by the random_password resource, written to Vault, and never appears as a literal in code. Other resources (like EC2 user-data or pipeline secrets) just read from Vault. This is a neat chicken-and-egg bootstrap: Vault is configured and filled by Terraform, then other Terraform reads from Vault.

Note

Because vault_kv_secret_v2 also stores the secret value in Terraform state (for idempotency), never put highly sensitive secret data through this resource without realizing the consequences. For key material or secrets that must "never be seen by Terraform," use the direct API or Response Wrapping (episode 13).

Ansible: Lookup Plugin community.hashi_vault

Ansible handles a different layer than Terraform: Terraform creates infrastructure, Ansible configures existing servers. To fetch secrets from Vault while the playbook runs, we use the official collection community.hashi_vault, which provides the hashi_vault lookup plugin and several modules (vault_read, vault_write, and others).

First, make sure the collection is installed:

Install the collection
ansible-galaxy collection install community.hashi_vault

Fetching a Secret with the Lookup Plugin

The lookup plugin is evaluated when the task runs, so its value is always fresh from Vault — not from a file cache. A simple example with AppRole authentication:

playbooks/fetch-secret.yml
- name: Fetch a secret from Vault
  hosts: app-servers
  vars:
    vault_url: "https://vault.example.com:8200"
    vault_role_id: "{{ lookup('env', 'VAULT_ROLE_ID') }}"
    vault_secret_id: "{{ lookup('env', 'VAULT_SECRET_ID') }}"
  tasks:
    - name: Read database credentials from Vault
      ansible.builtin.set_fact:
        db_password: "{{ lookup('community.hashi_vault.hashi_vault',
          'kv/data/postgres/app',
          url=vault_url,
          auth_method='approle',
          role_id=vault_role_id,
          secret_id=vault_secret_id,
          mount_point='kv')['data']['data']['DB_PASSWORD'] }}"
 
    - name: Write the application config
      ansible.builtin.template:
        src: app.config.j2
        dest: /etc/myapp/config.yaml
        mode: "0600"

Breaking down the important parts:

  • Path kv/data/postgres/app — as in previous episodes, KV v2 needs the data/ segment. The mount_point='kv' parameter tells the plugin which mount the path lives on.
  • ['data']['data']['DB_PASSWORD'] — the lookup returns the full JSON response from Vault. For KV v2, the response is structured as data.data.<field>; don't forget this double layer.
  • role_id and secret_id taken from the environment — not hardcoded in the playbook or in the inventory group. This prevents credentials from being committed to the Ansible repository.

The Cleaner Approach with the vault_read Module

For clarity and error-handling capabilities, use the vault_read module instead of a lookup. Modules can be registered and their results referenced, plus they're easier to debug:

playbooks/vault-module.yml
- name: Read a secret from Vault (module)
  community.hashi_vault.vault_read:
    url: "https://vault.example.com:8200"
    auth_method: approle
    role_id: "{{ vault_role_id }}"
    secret_id: "{{ vault_secret_id }}"
    path: kv/data/postgres/app
  register: vault_result
  no_log: true
 
- name: Store the values as facts
  ansible.builtin.set_fact:
    db_username: "{{ vault_result.data.data.DB_USERNAME }}"
    db_password: "{{ vault_result.data.data.DB_PASSWORD }}"
 
- name: Run the app with secrets from Vault
  ansible.builtin.systemd:
    name: myapp
    state: restarted
  environment:
    DB_USERNAME: "{{ db_username }}"
    DB_PASSWORD: "{{ db_password }}"

Two crucial details here:

  • no_log: true on tasks that interact with secrets — ensures secret values never appear in Ansible output even if you run ansible-playbook -vvv.
  • Secrets are injected via environment: on specific tasks only, not written permanently to a config file — unless a long-running process genuinely needs them, and even then with strict permissions.

Important

If the app on the server needs secrets continuously (not just at provisioning), don't write secrets to a permanent config file — use the Vault Agent we covered in episodes 14 & 15 to dynamically render secret templates and automatically update files when secrets are rotated. Ansible should only be used for static configuration and bootstrap.

Comparison: Terraform vs Ansible

The two tools complement each other, not compete. Here's the comparison:

AspectTerraformAnsible
Primary roleInfrastructure provisioning (declarative, stateful)Server configuration management (imperative)
Plugin/CollectionProvider hashicorp/vaultCollection community.hashi_vault
Fetching secretsData sources (vault_kv_secret_v2, vault_database_creds)Lookup plugin / vault_read module
Writing secretsResources (vault_kv_secret_v2, vault_mount)vault_write module
Dynamic DB / PKIvault_database_creds, vault_pki_secretDirect lookup to the same paths
Main riskSecrets stored in tfstateSecrets leak to logs/output without no_log
IdempotencyYes, compared against stateYes, per task

The best production pattern is a combination of both: Terraform creates infrastructure and fills Vault (bootstrap), Ansible configures servers by reading from Vault, and Vault Agent keeps secrets fresh for the server's lifetime.

Common Pitfalls IaC + Vault

Finally, let's discuss the traps that most often trip teams up:

MistakeSymptomSolution
RoleID/SecretID committedAppRole credentials leaked to the repoFetch from env vars / pipeline (episode 18); rotate immediately
Secret in tfstatePassword visible in state file & plan outputEncrypted remote state; consider dynamic creds
KV v2 path without data/permission denied or not foundUse kv/data/... + the correct mount_point
Forgetting no_log: true in AnsibleSecret appears in -vvv outputAdd no_log: true on sensitive tasks
vault_database_creds used for long-lived resourcesResource constantly replaced due to new credentialsWrite credentials to KV after creating the resource, or use a static role
Careless auth method choiceEternal token used by the Terraform providerUse auth_login AppRole / short-lived token from the pipeline
Vault down during provisioningapply/playbook fails completelyRetry, circuit breaker, and HA secrets engines (episode 20)
Storing secrets in a permanent config fileSecrets persist on server disksVault Agent + template sink (episodes 14-15)

Caution

Many believe sensitive = true on Terraform variables/outputs hides secrets from state — that's wrong. That attribute only masks the value in console output; the state file still stores the real value, encrypted-at-rest but decryptable. So never rely on it as protection. The real solution: remote state with strict backend encryption, and use short-lived dynamic credentials so whatever leaks from state becomes useless.

Conclusion

In this episode 19 we've covered how Terraform and Ansible — the two backbones of IaC — can integrate with Vault to break the cycle of settling secrets. With the Terraform Vault Provider, infrastructure can issue dynamic database credentials and PKI certificates on-demand at provisioning time, while also writing and configuring data in Vault declaratively. With Ansible, existing servers can read secrets directly from Vault via the lookup plugin or the vault_read module, without ever storing credentials on disk.

The key points to take home:

  • Terraform uses data sources (vault_kv_secret_v2, vault_database_creds, vault_pki_secret) to read, and resources (vault_mount, vault_kv_secret_v2) to write to Vault.
  • Ansible uses community.hashi_vault with the hashi_vault lookup or the vault_read module; always use no_log: true on sensitive tasks.
  • A secret in tfstate is not a secret — use encrypted remote state and prioritize dynamic credentials.
  • Never commit RoleID/SecretID; fetch from the environment or pipeline.

We've covered integration with applications, Kubernetes, CI/CD, and IaC. In episode 20, we rise to the architecture level: Vault High Availability (HA) Cluster with Raft Integrated Storage — how to build a 3-node or 5-node cluster with no single point of failure, complete with vault operator raft join and leader election. Keep your enthusiasm up!

Learn Vault - Vault Integration with Infrastructure as Code (IaC - Terraform & Ansible) | Learn Secret Management with HashiCorp Vault