Regex in Terraform, Ansible and Prometheus: Functions, Filters and Matchers

· 2 min read · Terraform & OpenTofu Tutorials

Regex in infrastructure code #

The syntax from the regular expressions tutorial is the same, but each tool has its own functions and its own flavor. This page shows the three you will meet most: Terraform and OpenTofu, Ansible and Prometheus.

Terraform #

Terraform uses the RE2 syntax (no lookahead, no backreferences).

regex(): extract #

regex(pattern, string) returns the match. If the pattern has no groups the result is a string, with groups it is a list, with named groups a map. It fails if there is no match.

locals {
  arn = "arn:aws:s3:::my-bucket"

  bucket = regex("arn:aws:s3:::(.+)", local.arn)[0]       # "my-bucket"

  parts = regex("^(?P<major>[0-9]+)\\.(?P<minor>[0-9]+)", "1.9.5")
  # { major = "1", minor = "9" }
}

output "minor" {
  value = local.parts.minor
}

In HCL strings a backslash must be doubled (\\.), or use a heredoc. Test expressions in terraform console.

regexall(): find every match #

regexall returns a list, and an empty list when nothing matches, so it never fails:

locals {
  has_prod = length(regexall("prod", var.environment)) > 0
  numbers  = regexall("[0-9]+", "web-12-db-7")        # ["12", "7"]
}

replace(): substitute with regex #

If the second argument is wrapped in /.../ it is a regex, and $1 refers to a group:

locals {
  safe_name = replace(lower(var.name), "/[^a-z0-9-]/", "-")
  swapped   = replace("2026-10-01", "/([0-9]{4})-([0-9]{2})-([0-9]{2})/", "$3/$2/$1")   # 01/10/2026
}

For simple cases prefer startswith(), endswith() and strcontains(): they are clearer than a pattern.

Validate variables #

can() turns the error of regex() into false, which is what a validation condition needs:

variable "bucket_name" {
  type = string

  validation {
    condition     = can(regex("^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", var.bucket_name))
    error_message = "The name must be 3-63 characters of lowercase letters, numbers, dots and hyphens."
  }
}

variable "ami_id" {
  type = string

  validation {
    condition     = can(regex("^ami-[0-9a-f]{8}([0-9a-f]{9})?$", var.ami_id))
    error_message = "Use an AMI id such as ami-0123456789abcdef0."
  }
}

See variables, outputs and locals and functions.

Ansible #

Ansible uses Python regex. There are filters and tests for variables, and modules for files.

regex.yml
- hosts: localhost
  gather_facts: false
  vars:
    version_line: "nginx version: nginx/1.27.1"
  tasks:
    - name: Extract the version
      ansible.builtin.debug:
        msg: "{{ version_line | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') }}"

    - name: Replace with groups
      ansible.builtin.debug:
        msg: "{{ '2026-10-01' | regex_replace('([0-9]{4})-([0-9]{2})-([0-9]{2})', '\\3/\\2/\\1') }}"

    - name: Every number
      ansible.builtin.debug:
        msg: "{{ 'web-12-db-7' | regex_findall('[0-9]+') }}"

    - name: Condition with a regex test
      ansible.builtin.debug:
        msg: "production host"
      when: inventory_hostname is match('^prod-')
  • regex_search, regex_replace and regex_findall are filters.
  • Tests: is match('...') anchors at the start, is search('...') looks anywhere, is regex('...') is a general test with options.
  • In double-quoted YAML a backslash is doubled; in single-quoted YAML it is not.

Change files with a pattern:

- name: Disable root login over SSH
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PermitRootLogin'
    line: 'PermitRootLogin no'
  notify: Restart sshd

- name: Update the version in a file
  ansible.builtin.replace:
    path: /etc/myapp/app.conf
    regexp: '^version=.*$'
    replace: 'version=2.0'

lineinfile replaces the last line that matches regexp, or appends line if none does. replace changes every match. Both are idempotent, so they are safe to run repeatedly (see Install Ansible).

Prometheus #

Label matchers accept regex with =~ (match) and !~ (no match). The expression is fully anchored, as if it had ^(...)$, and uses RE2.

up{job=~"node|prometheus"}                      # one of two jobs
http_requests_total{status=~"5.."}              # status codes 500-599
http_requests_total{path!~"/health|/metrics"}   # exclude paths
node_filesystem_avail_bytes{mountpoint=~"/(var|home).*"}

Because of the implicit anchors, job=~"node" only matches the job called exactly node; to match a prefix write node.*. Regex is also used in relabel_configs to rewrite labels:

relabel_configs:
  - source_labels: [__address__]
    regex: '([^:]+):\d+'
    target_label: host
    replacement: '$1'

This stores the address without its port in the label host. See Prometheus with Docker and Grafana for the setup.

Quick comparison #

Tool Flavor Extract Replace Anchored by default
grep -E, sed -E POSIX ERE grep -o s/…/…/ No
Terraform RE2 regex(), regexall() replace(s, "/…/", "$1") No
Ansible Python regex_search regex_replace match yes, search no
Prometheus RE2 =~ matcher relabel_configs Yes

#Regex #Terraform #Ansible #Prometheus