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

Resource Behavior & Lifecycle
What does the CertificateValidation resource actually do?
This resource waits for ACM certificate validation to complete. It doesn’t represent a real AWS entity, so changing or deleting it has no immediate effect on AWS infrastructure.
When should I use this resource?
Use it when you need to wait for certificate validation before using the certificate (e.g., attaching to a load balancer). It’s most commonly used with aws.acm.Certificate and aws.route53.Record for DNS validation.
DNS Validation
How do I validate an ACM certificate with DNS records?
Create Route53 records using the certificate’s domainValidationOptions, then pass the record FQDNs to validationRecordFqdns. The resource waits until validation completes.
Why should I use validationRecordFqdns?
Setting validationRecordFqdns enables additional sanity checks and creates an explicit dependency on the validation records, ensuring they’re created before validation starts.
How do I handle certificates with multiple domains in different hosted zones?
Map each domain to its correct Route53 zone ID when creating validation records. Use conditional logic to select the appropriate zone based on the domain name.
Validation Methods
Can I use email validation instead of DNS?
Yes, set the certificate’s validationMethod to EMAIL. The CertificateValidation resource acts as a waiter for manual email approval in this case.
Configuration & Immutability
What properties can't I change after creating the resource?
Both certificateArn and validationRecordFqdns are immutable and cannot be changed after creation.

Using a different cloud?

Explore security guides for other cloud providers: