Validate AWS ACM Certificates

The aws:acm/certificateValidation:CertificateValidation resource, part of the Pulumi AWS provider, represents a waiter that blocks Pulumi deployment until ACM certificate validation completes. This guide focuses on three validation workflows: DNS validation with Route 53, multi-domain certificate validation, and email-based validation.

This resource doesn’t create infrastructure; it orchestrates timing. It depends on aws.acm.Certificate and either Route 53 records (for DNS validation) or manual email approval (for email validation). The examples are intentionally small. Combine them with your own certificate requests and load balancer or CloudFront configurations.

Wait for DNS validation to complete

Most certificate workflows request a certificate, create DNS validation records in Route 53, and then wait for AWS to verify domain ownership before using the certificate.

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

const exampleCertificate = new aws.acm.Certificate("example", {
    domainName: "example.com",
    validationMethod: "DNS",
});
const example = aws.route53.getZone({
    name: "example.com",
    privateZone: false,
});
const exampleRecord: aws.route53.Record[] = [];
exampleCertificate.domainValidationOptions.apply(domainValidationOptions => {
    for (const range of Object.entries(domainValidationOptions.reduce((__obj, dvo) => ({ ...__obj, [dvo.domainName]: {
        name: dvo.resourceRecordName,
        record: dvo.resourceRecordValue,
        type: dvo.resourceRecordType,
    } }))).map(([k, v]) => ({key: k, value: v}))) {
        exampleRecord.push(new aws.route53.Record(`example-${range.key}`, {
            allowOverwrite: true,
            name: range.value.name,
            records: [range.value.record],
            ttl: 60,
            type: aws.route53.RecordType[range.value.type],
            zoneId: example.then(example => example.zoneId),
        }));
    }
});
const exampleCertificateValidation = new aws.acm.CertificateValidation("example", {
    certificateArn: exampleCertificate.arn,
    validationRecordFqdns: exampleRecord.apply(exampleRecord => exampleRecord.map(record => (record.fqdn))),
});
const exampleListener = new aws.lb.Listener("example", {certificateArn: exampleCertificateValidation.certificateArn});
import pulumi
import pulumi_aws as aws

example_certificate = aws.acm.Certificate("example",
    domain_name="example.com",
    validation_method="DNS")
example = aws.route53.get_zone(name="example.com",
    private_zone=False)
example_record = []
def create_example(range_body):
    for range in [{"key": k, "value": v} for [k, v] in enumerate(range_body)]:
        example_record.append(aws.route53.Record(f"example-{range['key']}",
            allow_overwrite=True,
            name=range["value"]["name"],
            records=[range["value"]["record"]],
            ttl=60,
            type=aws.route53.RecordType(range["value"]["type"]),
            zone_id=example.zone_id))

example_certificate.domain_validation_options.apply(lambda resolved_outputs: create_example({dvo.domain_name: {
    "name": dvo.resource_record_name,
    "record": dvo.resource_record_value,
    "type": dvo.resource_record_type,
} for dvo in resolved_outputs['domain_validation_options']}))
example_certificate_validation = aws.acm.CertificateValidation("example",
    certificate_arn=example_certificate.arn,
    validation_record_fqdns=example_record.apply(lambda example_record: [record.fqdn for record in example_record]))
example_listener = aws.lb.Listener("example", certificate_arn=example_certificate_validation.certificate_arn)
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var exampleCertificate = new Aws.Acm.Certificate("example", new()
    {
        DomainName = "example.com",
        ValidationMethod = "DNS",
    });

    var example = Aws.Route53.GetZone.Invoke(new()
    {
        Name = "example.com",
        PrivateZone = false,
    });

    var exampleRecord = new List<Aws.Route53.Record>();
    foreach (var range in exampleCertificate.DomainValidationOptions.Apply(domainValidationOptions => domainValidationOptions.ToDictionary(item => {
        var dvo = item.Value;
        return dvo.DomainName;
    }, item => {
        var dvo = item.Value;
        return 
        {
            { "name", dvo.ResourceRecordName },
            { "record", dvo.ResourceRecordValue },
            { "type", dvo.ResourceRecordType },
        };
    })).Select(pair => new { pair.Key, pair.Value }))
    {
        exampleRecord.Add(new Aws.Route53.Record($"example-{range.Key}", new()
        {
            AllowOverwrite = true,
            Name = range.Value.Name,
            Records = new[]
            {
                range.Value.Record,
            },
            Ttl = 60,
            Type = System.Enum.Parse<Aws.Route53.RecordType>(range.Value.Type),
            ZoneId = example.Apply(getZoneResult => getZoneResult.ZoneId),
        }));
    }
    var exampleCertificateValidation = new Aws.Acm.CertificateValidation("example", new()
    {
        CertificateArn = exampleCertificate.Arn,
        ValidationRecordFqdns = exampleRecord.Apply(exampleRecord => exampleRecord.Select(record => 
        {
            return record.Fqdn;
        }).ToList()),
    });

    var exampleListener = new Aws.LB.Listener("example", new()
    {
        CertificateArn = exampleCertificateValidation.CertificateArn,
    });

});

The certificateArn property references the certificate being validated. The validationRecordFqdns property lists the FQDNs of Route 53 records created from domainValidationOptions, establishing an explicit dependency that ensures records exist before validation begins. The resource blocks until AWS confirms domain ownership, then outputs the validated certificate ARN for use in load balancers or CloudFront.

Validate certificates with multiple domains

Certificates covering multiple domains via subjectAlternativeNames require validation records in each domain’s hosted zone.

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

const example = new aws.acm.Certificate("example", {
    domainName: "example.com",
    subjectAlternativeNames: [
        "www.example.com",
        "example.org",
    ],
    validationMethod: "DNS",
});
const exampleCom = aws.route53.getZone({
    name: "example.com",
    privateZone: false,
});
const exampleOrg = aws.route53.getZone({
    name: "example.org",
    privateZone: false,
});
const exampleRecord: aws.route53.Record[] = [];
pulumi.all([example.domainValidationOptions, dvo.domainName == "example.org" ? exampleOrg.then(exampleOrg => exampleOrg.zoneId) : exampleCom.then(exampleCom => exampleCom.zoneId)]).apply(([domainValidationOptions, value]) => {
    for (const range of Object.entries(domainValidationOptions.reduce((__obj, dvo) => ({ ...__obj, [dvo.domainName]: {
        name: dvo.resourceRecordName,
        record: dvo.resourceRecordValue,
        type: dvo.resourceRecordType,
        zoneId: value,
    } }))).map(([k, v]) => ({key: k, value: v}))) {
        exampleRecord.push(new aws.route53.Record(`example-${range.key}`, {
            allowOverwrite: true,
            name: range.value.name,
            records: [range.value.record],
            ttl: 60,
            type: aws.route53.RecordType[range.value.type],
            zoneId: range.value.zoneId,
        }));
    }
});
const exampleCertificateValidation = new aws.acm.CertificateValidation("example", {
    certificateArn: example.arn,
    validationRecordFqdns: exampleRecord.apply(exampleRecord => exampleRecord.map(record => (record.fqdn))),
});
const exampleListener = new aws.lb.Listener("example", {certificateArn: exampleCertificateValidation.certificateArn});
import pulumi
import pulumi_aws as aws

example = aws.acm.Certificate("example",
    domain_name="example.com",
    subject_alternative_names=[
        "www.example.com",
        "example.org",
    ],
    validation_method="DNS")
example_com = aws.route53.get_zone(name="example.com",
    private_zone=False)
example_org = aws.route53.get_zone(name="example.org",
    private_zone=False)
example_record = []
def create_example(range_body):
    for range in [{"key": k, "value": v} for [k, v] in enumerate(range_body)]:
        example_record.append(aws.route53.Record(f"example-{range['key']}",
            allow_overwrite=True,
            name=range["value"]["name"],
            records=[range["value"]["record"]],
            ttl=60,
            type=aws.route53.RecordType(range["value"]["type"]),
            zone_id=range["value"]["zoneId"]))

example.domain_validation_options.apply(lambda resolved_outputs: create_example({dvo.domain_name: {
    "name": dvo.resource_record_name,
    "record": dvo.resource_record_value,
    "type": dvo.resource_record_type,
    "zoneId": example_org.zone_id if dvo.domain_name == "example.org" else example_com.zone_id,
} for dvo in resolved_outputs['domain_validation_options']}))
example_certificate_validation = aws.acm.CertificateValidation("example",
    certificate_arn=example.arn,
    validation_record_fqdns=example_record.apply(lambda example_record: [record.fqdn for record in example_record]))
example_listener = aws.lb.Listener("example", certificate_arn=example_certificate_validation.certificate_arn)
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var example = new Aws.Acm.Certificate("example", new()
    {
        DomainName = "example.com",
        SubjectAlternativeNames = new[]
        {
            "www.example.com",
            "example.org",
        },
        ValidationMethod = "DNS",
    });

    var exampleCom = Aws.Route53.GetZone.Invoke(new()
    {
        Name = "example.com",
        PrivateZone = false,
    });

    var exampleOrg = Aws.Route53.GetZone.Invoke(new()
    {
        Name = "example.org",
        PrivateZone = false,
    });

    var exampleRecord = new List<Aws.Route53.Record>();
    foreach (var range in Output.Tuple(example.DomainValidationOptions, dvo.DomainName == "example.org" ? exampleOrg.Apply(getZoneResult => getZoneResult.ZoneId) : exampleCom.Apply(getZoneResult => getZoneResult.ZoneId)).Apply(values =>
    {
        var domainValidationOptions = values.Item1;
        var @value = values.Item2;
        return domainValidationOptions.ToDictionary(item => {
            var dvo = item.Value;
            return dvo.DomainName;
        }, item => {
            var dvo = item.Value;
            return 
            {
                { "name", dvo.ResourceRecordName },
                { "record", dvo.ResourceRecordValue },
                { "type", dvo.ResourceRecordType },
                { "zoneId", @value },
            };
        });
    }).Select(pair => new { pair.Key, pair.Value }))
    {
        exampleRecord.Add(new Aws.Route53.Record($"example-{range.Key}", new()
        {
            AllowOverwrite = true,
            Name = range.Value.Name,
            Records = new[]
            {
                range.Value.Record,
            },
            Ttl = 60,
            Type = System.Enum.Parse<Aws.Route53.RecordType>(range.Value.Type),
            ZoneId = range.Value.ZoneId,
        }));
    }
    var exampleCertificateValidation = new Aws.Acm.CertificateValidation("example", new()
    {
        CertificateArn = example.Arn,
        ValidationRecordFqdns = exampleRecord.Apply(exampleRecord => exampleRecord.Select(record => 
        {
            return record.Fqdn;
        }).ToList()),
    });

    var exampleListener = new Aws.LB.Listener("example", new()
    {
        CertificateArn = exampleCertificateValidation.CertificateArn,
    });

});

When a certificate includes subjectAlternativeNames, domainValidationOptions contains entries for each domain. The example uses conditional logic to route validation records to the correct Route 53 zones (example.com vs example.org). The validationRecordFqdns property collects all validation records, and the resource waits until AWS validates all domains before completing.

Wait for manual email validation

When DNS validation isn’t possible, ACM sends validation emails to domain contacts and waits for manual approval.

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

const example = new aws.acm.Certificate("example", {
    domainName: "example.com",
    validationMethod: "EMAIL",
});
const exampleCertificateValidation = new aws.acm.CertificateValidation("example", {certificateArn: example.arn});
import pulumi
import pulumi_aws as aws

example = aws.acm.Certificate("example",
    domain_name="example.com",
    validation_method="EMAIL")
example_certificate_validation = aws.acm.CertificateValidation("example", certificate_arn=example.arn)
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/acm"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		example, err := acm.NewCertificate(ctx, "example", &acm.CertificateArgs{
			DomainName:       pulumi.String("example.com"),
			ValidationMethod: pulumi.String("EMAIL"),
		})
		if err != nil {
			return err
		}
		_, err = acm.NewCertificateValidation(ctx, "example", &acm.CertificateValidationArgs{
			CertificateArn: example.Arn,
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var example = new Aws.Acm.Certificate("example", new()
    {
        DomainName = "example.com",
        ValidationMethod = "EMAIL",
    });

    var exampleCertificateValidation = new Aws.Acm.CertificateValidation("example", new()
    {
        CertificateArn = example.Arn,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.acm.Certificate;
import com.pulumi.aws.acm.CertificateArgs;
import com.pulumi.aws.acm.CertificateValidation;
import com.pulumi.aws.acm.CertificateValidationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var example = new Certificate("example", CertificateArgs.builder()
            .domainName("example.com")
            .validationMethod("EMAIL")
            .build());

        var exampleCertificateValidation = new CertificateValidation("exampleCertificateValidation", CertificateValidationArgs.builder()
            .certificateArn(example.arn())
            .build());

    }
}
resources:
  example:
    type: aws:acm:Certificate
    properties:
      domainName: example.com
      validationMethod: EMAIL
  exampleCertificateValidation:
    type: aws:acm:CertificateValidation
    name: example
    properties:
      certificateArn: ${example.arn}

For email validation, you omit validationRecordFqdns entirely. The resource simply waits for someone to click the approval link in the validation email AWS sends. This blocks deployment until manual approval completes, ensuring the certificate is valid before dependent resources (like load balancers) try to use it.

Beyond these examples

These snippets focus on specific validation workflows: DNS validation with Route 53 records, multi-domain certificate validation, and email-based validation. They’re intentionally minimal rather than full certificate deployment modules.

The examples rely on pre-existing infrastructure such as ACM certificates (aws.acm.Certificate), Route 53 hosted zones for DNS validation, and load balancers or CloudFront distributions that consume certificates. They focus on orchestrating validation timing rather than provisioning the surrounding infrastructure.

To keep things focused, validation patterns are omitted, including:

  • Cross-region certificate validation (region property)
  • Validation timeout configuration
  • Certificate renewal workflows
  • Wildcard certificate validation

These omissions are intentional: the goal is to illustrate how validation orchestration is wired, not provide drop-in certificate modules. See the ACM CertificateValidation resource reference for all available configuration options.

Let's validate AWS ACM Certificates

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Understanding the Resource
What does CertificateValidation actually do in my infrastructure?
CertificateValidation is a Pulumi workflow helper that waits for ACM certificate validation to complete. It doesn’t represent a real AWS resource, so changing or deleting it has no immediate effect on AWS itself. For DNS validation, it waits for validation records to propagate; for email validation, it waits for manual approval.
What's the difference between DNS and email validation?
DNS validation uses aws.route53.Record resources to automatically prove domain ownership, while email validation requires manual approval via email sent to domain contacts. The CertificateValidation resource acts as a waiter for both methods.
DNS Validation Configuration
Why should I use validationRecordFqdns with DNS validation?
Setting validationRecordFqdns enables additional sanity checks and creates an explicit dependency on the Route53 records implementing validation. This ensures proper ordering in your infrastructure deployment.
How do I validate a certificate with multiple domains?
Add domains to subjectAlternativeNames on your aws.acm.Certificate, then create validation records in the appropriate Route53 zones for each domain. The example shows handling example.com and example.org in different zones.
Can I use CertificateValidation with certificates in different regions?
Yes, specify the region property to match where your certificate is managed. It defaults to your provider’s configured region.
Immutability & Updates
What properties can't I change after creating CertificateValidation?
Both certificateArn and validationRecordFqdns are immutable. Changing either requires recreating the resource.

Using a different cloud?

Explore security guides for other cloud providers: