Skip to content

Simulated ACM

Yulin includes a simulated AWS Certificate Manager (ACM) for tests and local development. In this guide, you’ll request certificates, configure DNS validation, filter certificate lists, and use ACM with simulated CloudFormation.

  • Install @kensio/yulin in your project.
  • Import ACM commands from @aws-sdk/client-acm.
  • Import SimAws from @kensio/yulin.
  1. Create a SimAws instance and get a simulated ACM client.
  2. Call requestCertificate with a RequestCertificateCommand.
  3. Call listCertificates with a ListCertificatesCommand to confirm the certificate exists.
/**
* Requesting a simulated ACM certificate.
*/
import {
ListCertificatesCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.account("555555555555").region("eu-west-1").acm();
const requestOutput = await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "example.test",
}),
);
console.log(requestOutput.CertificateArn);
const listOutput = await acm.listCertificates(new ListCertificatesCommand());
console.log(listOutput.CertificateSummaryList?.[0]?.DomainName);
console.log(listOutput.CertificateSummaryList?.[0]?.Status);

Certificate ARNs include the selected simulated account and region, for example:

flowchart LR
A["RequestCertificateCommand\n(DomainName)"] --> B["Simulated ACM"]
B --> C["Certificate ARN\narn:aws:acm:eu-west-1:555555555555:certificate/00000001"]
B --> D["Status: PENDING_VALIDATION"]
arn:aws:acm:eu-west-1:555555555555:certificate/00000001

You can request multiple certificates for the same domain. Each request receives a distinct ARN.

Pass SubjectAlternativeNames when a certificate must cover more than one DNS name.

/**
* Requesting a simulated ACM certificate with subject alternative names.
*/
import {
ListCertificatesCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
const requestOutput = await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "example.test",
SubjectAlternativeNames: ["www.example.test", "api.example.test"],
}),
);
const listOutput = await acm.listCertificates(new ListCertificatesCommand());
console.log(requestOutput.CertificateArn);
console.log(
listOutput.CertificateSummaryList?.[0]?.SubjectAlternativeNameSummaries,
);

ListCertificatesCommand includes up to 100 subject alternative names in each summary. If a certificate has more than 100 names, HasAdditionalSubjectAlternativeNames is set on the summary.

Describe a certificate and its validation records

Section titled “Describe a certificate and its validation records”

Use DescribeCertificateCommand to inspect certificate details, including validation options.

/**
* Describing a simulated ACM certificate and its DNS validation records.
*/
import {
DescribeCertificateCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
const requestOutput = await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "example.test",
SubjectAlternativeNames: ["www.example.test"],
ValidationMethod: "DNS",
}),
);
const describeOutput = await acm.describeCertificate(
new DescribeCertificateCommand({
CertificateArn: requestOutput.CertificateArn,
}),
);
const certificate = describeOutput.Certificate;
console.log(certificate?.DomainName);
console.log(certificate?.Status);
const domainValidationOptions = certificate?.DomainValidationOptions ?? [];
for (const validation of domainValidationOptions) {
console.log(validation.DomainName);
console.log(validation.ValidationMethod);
console.log(validation.ResourceRecord?.Name);
console.log(validation.ResourceRecord?.Type);
console.log(validation.ResourceRecord?.Value);
}

For DNS validation, simulated ACM returns CNAME validation records for the primary domain and each subject alternative name. The records are deterministic, which makes them suitable for assertions in tests.

For EMAIL validation, the validation method is recorded but no DNS resource record is returned.

/**
* Requesting a simulated ACM certificate with EMAIL validation.
*/
import {
DescribeCertificateCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
const requestOutput = await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "mail.example.test",
ValidationMethod: "EMAIL",
}),
);
const describeOutput = await acm.describeCertificate(
new DescribeCertificateCommand({
CertificateArn: requestOutput.CertificateArn,
}),
);
const validation = describeOutput.Certificate?.DomainValidationOptions?.[0];
console.log(validation?.ValidationMethod);
console.log(validation?.ResourceRecord);

Requested certificates start in PENDING_VALIDATION status. Simulated ACM schedules background work to move them to ISSUED.

If your test needs the issued state, wait for background tasks to complete before describing the certificate.

Note: Where a simulated Route53 hosted zone covers the certificate domain, issuance waits for DNS validation first. See Validate a certificate against simulated Route53 below.

/**
* Waiting for a simulated ACM certificate to be issued.
*/
import {
DescribeCertificateCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
const requestOutput = await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "issued.example.test",
}),
);
await simAws.backgroundTasksComplete();
const describeOutput = await acm.describeCertificate(
new DescribeCertificateCommand({
CertificateArn: requestOutput.CertificateArn,
}),
);
console.log(describeOutput.Certificate?.Status);
console.log(describeOutput.Certificate?.IssuedAt);

Validate a certificate against simulated Route53

Section titled “Validate a certificate against simulated Route53”

Real ACM issues a DNS-validated certificate only once the CNAME it requests is resolvable. Simulated ACM does the same, but only where the simulation can answer for the domain.

By default, the rules are:

  • If a simulated Route53 hosted zone covers the certificate domain, the certificate waits for its validation record.
  • If no hosted zone covers the domain, the certificate is issued as soon as background tasks drain.

Templates commonly reference hosted zones managed by another team or another tool. Those certificates keep working here because the simulation holds no zone for their domain.

Two methods override that default where it doesn’t suit your test.

/**
* Validating a simulated ACM certificate against a simulated Route53 record.
*/
import {
DescribeCertificateCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import {
ChangeResourceRecordSetsCommand,
CreateHostedZoneCommand,
} from "@aws-sdk/client-route-53";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const zoneOutput = await simAws.route53().createHostedZone(
new CreateHostedZoneCommand({
Name: "example.test",
CallerReference: "acm-dns-validation",
}),
);
const requestOutput = await simAws.acm().requestCertificate(
new RequestCertificateCommand({
DomainName: "api.example.test",
}),
);
await simAws.backgroundTasksComplete();
// A hosted zone covers the domain, so the certificate waits for its record.
const pendingOutput = await simAws.acm().describeCertificate(
new DescribeCertificateCommand({
CertificateArn: requestOutput.CertificateArn,
}),
);
console.log(pendingOutput.Certificate?.Status); // PENDING_VALIDATION
const validationRecord =
pendingOutput.Certificate?.DomainValidationOptions?.[0]?.ResourceRecord;
await simAws.route53().changeResourceRecordSets(
new ChangeResourceRecordSetsCommand({
HostedZoneId: zoneOutput.HostedZone?.Id,
ChangeBatch: {
Changes: [
{
Action: "CREATE",
ResourceRecordSet: {
Name: validationRecord?.Name,
Type: "CNAME",
TTL: 300,
ResourceRecords: [{ Value: validationRecord?.Value ?? "" }],
},
},
],
},
}),
);
await simAws.backgroundTasksComplete();
const issuedOutput = await simAws.acm().describeCertificate(
new DescribeCertificateCommand({
CertificateArn: requestOutput.CertificateArn,
}),
);
console.log(issuedOutput.Certificate?.Status); // ISSUED

Each domain on a certificate is validated separately. A certificate with subject alternative names is issued only once every domain that needs DNS validation has its record. Domains that are not covered by any hosted zone don’t need anything published for them. Until all domains are validated, DescribeCertificateCommand reports SUCCESS for validated domains and PENDING_VALIDATION for the rest.

Hosted zones are looked up across every simulated account, matching real ACM validating against public DNS. A certificate in one account can be validated by a hosted zone in another.

Note: Use completeDnsValidation() when your test needs a hosted zone for reasons unrelated to certificate validation and you don’t want to go through the full validation flow.

Call completeDnsValidation() on a pending certificate. It publishes the validation records and resolves once the certificate is issued.

/**
* Completing simulated ACM DNS validation in one call.
*/
import {
DescribeCertificateCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { CreateHostedZoneCommand } from "@aws-sdk/client-route-53";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.route53().createHostedZone(
new CreateHostedZoneCommand({
Name: "example.test",
CallerReference: "acm-shortcut",
}),
);
const requestOutput = await simAws.acm().requestCertificate(
new RequestCertificateCommand({
DomainName: "api.example.test",
}),
);
await simAws.acm().completeDnsValidation(requestOutput.CertificateArn);
const describeOutput = await simAws.acm().describeCertificate(
new DescribeCertificateCommand({
CertificateArn: requestOutput.CertificateArn,
}),
);
console.log(describeOutput.Certificate?.Status); // ISSUED

Two methods override the default behavior when the hosted zone heuristic doesn’t match your test’s needs:

Method Behavior
simAws.acm().autoIssueCertificates() Never requires validation. Use this when a hosted zone exists for unrelated reasons and you don’t care about certificates.
simAws.acm().requireDnsValidation() Always requires DNS validation. Use this to exercise the validation path without creating a hosted zone first.

A standalone new SimAcm() instance has no simulated Route53, and always issues certificates immediately. Calling requireDnsValidation() on one throws, because nothing can publish the record it would then wait for.

Use ListCertificatesCommand to inspect certificates in the selected simulated account and region.

/**
* Listing simulated ACM certificates.
*/
import {
ListCertificatesCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "one.example.test",
}),
);
await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "two.example.test",
}),
);
const listOutput = await acm.listCertificates(
new ListCertificatesCommand({
MaxItems: 10,
}),
);
const certificateSummaries = listOutput.CertificateSummaryList ?? [];
for (const summary of certificateSummaries) {
console.log(summary.CertificateArn);
console.log(summary.DomainName);
console.log(summary.Status);
}

Certificates are listed in creation order. MaxItems must be between 1 and 1000 and defaults to 100. When more results are available, pass NextToken from the response into your next request.

To filter by status, pass CertificateStatuses to ListCertificatesCommand.

/**
* Filtering simulated ACM certificates by status.
*/
import {
ListCertificatesCommand,
RequestCertificateCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "issued.example.test",
}),
);
await simAws.backgroundTasksComplete();
const listOutput = await acm.listCertificates(
new ListCertificatesCommand({
CertificateStatuses: ["ISSUED"],
}),
);
console.log(listOutput.CertificateSummaryList?.map((cert) => cert.DomainName));

Pass Tags when requesting a certificate. Simulated ACM accepts up to 50 tags, matching the ACM request limit. Requests with more than 50 tags throw TooManyTagsException.

/**
* Requesting a simulated ACM certificate with tags.
*/
import { RequestCertificateCommand } from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const acm = simAws.acm();
await acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "tagged.example.test",
Tags: [
{
Key: "Purpose",
Value: "local-test",
},
{
Key: "Owner",
Value: "docs",
},
],
}),
);

Scope certificates to an account and region

Section titled “Scope certificates to an account and region”

Use SimAws scopes to create ACM certificates in different simulated accounts and regions. ACM state is scoped to the selected account and region. Certificates requested in one scope don’t appear in another.

/**
* Simulated ACM account and region scoping.
*/
import { RequestCertificateCommand } from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const defaultAcm = simAws.acm();
const euWest2Acm = simAws.region("eu-west-2").acm();
const accountAcm = simAws.account("111111111111").acm();
const scopedAcm = simAws.account("222222222222").region("ap-east-1").acm();
await defaultAcm.requestCertificate(
new RequestCertificateCommand({
DomainName: "default.example.test",
}),
);
await euWest2Acm.requestCertificate(
new RequestCertificateCommand({
DomainName: "eu-west-2.example.test",
}),
);
await accountAcm.requestCertificate(
new RequestCertificateCommand({
DomainName: "account.example.test",
}),
);
await scopedAcm.requestCertificate(
new RequestCertificateCommand({
DomainName: "scoped.example.test",
}),
);

Each SimAws instance has its own isolated state. Create a fresh instance per test or share one across related local setup.

RequestCertificateCommand allocates its own certificate ARN, as real ACM does, and takes none from you. When something else already decided the ARN, register the certificate as part of your test setup instead.

The usual reason is a CDK app that creates its certificate in one stack and uses it in another. The ARN crosses between the two as a plain string, and the stack using it carries that ARN into its synthesized template. Simulated CloudFront checks the certificate before it creates a Distribution, so registering the certificate first lets the template deploy as it is, with no rewriting.

/**
* Registering a simulated ACM certificate with a chosen certificate ARN.
*/
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
// The certificate ARN a CDK app carried into the template of the stack using it.
const certificateArn =
"arn:aws:acm:us-east-1:111122223333:certificate/3b82191c-b029-4e5f-a94f-038f98a53ede";
// Register it in the account and region the ARN itself names.
simAws
.account("111122223333")
.region("us-east-1")
.acm()
.registerCertificate({
arn: certificateArn,
domainName: "example.test",
subjectAlternativeNames: ["www.example.test"],
});
const stack = await simAws
.account("111122223333")
.region("us-east-1")
.cloudFormation()
.deployTemplate({
stackName: "site-stack",
template: {
Resources: {
SiteDistribution: {
Type: "AWS::CloudFront::Distribution",
Properties: {
DistributionConfig: {
CallerReference: "site-distribution",
Enabled: true,
Aliases: ["www.example.test"],
DefaultCacheBehavior: {
TargetOriginId: "origin",
ViewerProtocolPolicy: "redirect-to-https",
},
ViewerCertificate: {
AcmCertificateArn: certificateArn,
SslSupportMethod: "sni-only",
},
},
},
},
},
},
});
await stack.waitForDeployComplete();
console.log(stack.getResource("SiteDistribution")?.status);

A registered certificate behaves like any other. It answers DescribeCertificateCommand, appears in ListCertificatesCommand under an ISSUED status filter, and satisfies the certificate lookups simulated CloudFront and ELBv2 make. It is ISSUED from the moment it is registered, since the simulation was told it already exists, and it carries no DNS validation records.

Pass status to register a certificate in some other state, such as EXPIRED, to see what a Distribution does with it. An ARN that another certificate already holds is refused with InvalidArgsException, as is a string that is no ACM certificate ARN. So is an ARN naming an account or region other than the ACM’s own, since other services find a certificate through the account and region inside its ARN.

Simulated CloudFormation can create ACM certificates from AWS::CertificateManager::Certificate.

For AWS::CertificateManager::Certificate:

  • Ref returns the certificate ARN.
  • Fn::GetAtt supports CertificateArn and CertificateStatus.

Supported certificate properties:

Property Description
DomainName Primary domain for the certificate.
SubjectAlternativeNames Additional DNS names to cover.
ValidationMethod DNS or EMAIL.
DomainValidationOptions Validation options per domain, including HostedZoneId.
Tags Up to 50 key-value tags.
/**
* Creating an ACM certificate through simulated CloudFormation.
*/
import {
DescribeCertificateCommand,
ListCertificatesCommand,
} from "@aws-sdk/client-acm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({
stackName: "acm-certificate-stack",
template: {
Resources: {
SiteCertificate: {
Type: "AWS::CertificateManager::Certificate",
Properties: {
DomainName: "example.test",
SubjectAlternativeNames: ["www.example.test"],
ValidationMethod: "DNS",
DomainValidationOptions: [
{
DomainName: "example.test",
ValidationDomain: "example.test",
},
],
Tags: [
{
Key: "Purpose",
Value: "local-test",
},
],
},
},
},
Outputs: {
CertificateArn: {
Value: {
Ref: "SiteCertificate",
},
},
CertificateStatus: {
Value: {
"Fn::GetAtt": ["SiteCertificate", "CertificateStatus"],
},
},
},
},
});
const certificateArn = stack.output("CertificateArn");
if (typeof certificateArn !== "string")
throw new Error("No CertificateArn Output");
const listOutput = await simAws
.acm()
.listCertificates(new ListCertificatesCommand());
const describeOutput = await simAws.acm().describeCertificate(
new DescribeCertificateCommand({
CertificateArn: certificateArn,
}),
);
console.log(stack.output("CertificateStatus"));
console.log(listOutput.CertificateSummaryList?.[0]?.DomainName);
console.log(describeOutput.Certificate?.Status);

Give a DomainValidationOptions entry a HostedZoneId and simulated CloudFormation publishes the validation record itself, the same way real CloudFormation does. This is what CDK emits for CertificateValidation.fromDns(zone). A CDK-synthesized template works without changes.

A certificate resource is only complete once the certificate is issued. Anything depending on the certificate is created after it exists, as in real CloudFormation.

/**
* Validating an ACM certificate from a simulated CloudFormation template.
*/
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({
stackName: "acm-dns-validation-stack",
template: {
Resources: {
Zone: {
Type: "AWS::Route53::HostedZone",
Properties: {
Name: "example.test",
},
},
SiteCertificate: {
Type: "AWS::CertificateManager::Certificate",
Properties: {
DomainName: "api.example.test",
ValidationMethod: "DNS",
DomainValidationOptions: [
{
DomainName: "api.example.test",
HostedZoneId: { Ref: "Zone" },
},
],
},
},
},
Outputs: {
CertificateStatus: {
Value: {
"Fn::GetAtt": ["SiteCertificate", "CertificateStatus"],
},
},
},
},
});
await stack.waitForDeployComplete();
// The hosted zone, the validation record and the issued certificate, from one
// template deploy.
console.log(stack.output("CertificateStatus")); // ISSUED

HostedZoneId accepts a Ref to an AWS::Route53::HostedZone in the same template, or the literal ID of a zone created outside the stack.

A HostedZoneId that names a hosted zone the simulator doesn’t hold is skipped rather than failing. Route53 is often managed by another team or another tool. The certificate then follows the usual rule from Validate a certificate against simulated Route53: with nothing authoritative for its domain, it’s issued without validation.

If a hosted zone covers the domain but the validation record never appears, the stack fails rather than hanging. Real CloudFormation sits in CREATE_IN_PROGRESS for hours before timing out, which is of little use in a test. The resource fails immediately and names the record it waited for.

Simulated ACM supports:

  • RequestCertificateCommand, DescribeCertificateCommand, and ListCertificatesCommand
  • DNS validation against records in simulated Route53
  • CloudFormation-published validation records from DomainValidationOptions[].HostedZoneId
  • EMAIL validation method shapes (validation always succeeds regardless)
  • Subject alternative names
  • Certificate tags, up to the ACM limit of 50
  • Deterministic certificate ARNs scoped to account and region
  • Certificates registered under a caller-chosen ARN, for a template naming one another stack created
  • Deterministic DNS validation CNAME records
  • Background certificate issuance from PENDING_VALIDATION to ISSUED
  • Per-domain validation status for multi-domain certificates
  • The AWS::CertificateManager::Certificate CloudFormation resource, with Ref and Fn::GetAtt

Unsupported ACM options might be ignored or might throw errors, depending on whether the simulator needs them to model the requested behavior.

Limitation Detail
Certificate deletion Not supported.
Certificate renewal Not supported.
Imported certificates Not supported.
EMAIL validation Always succeeds; only DNS validation is enforced.
DNS validation scope Checked against simulated Route53 only, never against real DNS.
Validation timeout A certificate requested through the SDK whose record never appears stays PENDING_VALIDATION. A CloudFormation certificate fails its stack instead.
HTTP API ACM is not served as an HTTP API by serveSimAws.

Documenting Yulin v1.20.2