Simulated CloudFormation
Yulin includes a simulated CloudFormation service for tests and local development.
Sim CloudFormation creates simulated AWS resources from CloudFormation templates. It can be used with
hand-written templates, AWS SDK-style CreateStackCommand calls, or synthesized CDK template files.
Basic usage
Section titled “Basic usage”Create a simulated AWS environment, get simulated CloudFormation, and deploy a template.
/** * Deploying a simple CloudFormation template into simulated AWS. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const simCfn = simAws.cloudFormation();
const stack = await simCfn.deployTemplate({ stackName: "site-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "example-site-bucket", WebsiteConfiguration: { IndexDocument: "index.html", }, }, }, }, },});
await stack.waitForDeployComplete();
const bucket = simAws.s3().getSimBucketByName("example-site-bucket");console.log(bucket?.bucketName);deployTemplate(...) returns the simulated stack object. If your test needs the created resources
to be available, wait for deployment to complete before asserting final state.
Naming the template type
Section titled “Naming the template type”A template written inline is typed by deployTemplate(...) itself. A test that builds a template up
somewhere else can name that type as CfnTemplateBodyRecord.
/** * Naming the type of a template a test builds somewhere other than the call. */
import { SimAws } from "@kensio/yulin";import type { CfnTemplateBodyRecord } from "@kensio/yulin/cloudformation";
function siteTemplate(bucketName: string): CfnTemplateBodyRecord { return { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: bucketName, }, }, }, };}
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "typed-site-stack", template: siteTemplate("typed-site-bucket"),});
await stack.waitForDeployComplete();
console.log(simAws.s3().getSimBucketByName("typed-site-bucket")?.bucketName);Creating stacks with AWS SDK command shapes
Section titled “Creating stacks with AWS SDK command shapes”You can also use AWS SDK-style CloudFormation commands.
/** * Creating a simulated CloudFormation Stack with CreateStackCommand. */
import { CreateStackCommand, DescribeStacksCommand,} from "@aws-sdk/client-cloudformation";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const simCfn = simAws.cloudFormation();
await simCfn.createStack( new CreateStackCommand({ StackName: "command-stack", TemplateBody: JSON.stringify({ Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "command-stack-bucket", }, }, }, }), }),);
await simCfn.waitForStackDeployComplete("command-stack");
const describeOutput = await simCfn.describeStacks( new DescribeStacksCommand({ StackName: "command-stack", }),);
console.log(describeOutput.Stacks?.[0]?.StackStatus);createStack(...) starts deployment and returns once the stack has been accepted. Resource creation
continues asynchronously, similar to real CloudFormation. Use waitForStackDeployComplete(...) when
you need final stack state.
Stack deployment is asynchronous
Section titled “Stack deployment is asynchronous”A stack may be visible before all resources have finished creating.
/** * Waiting for a simulated CloudFormation deployment to finish. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.cloudFormation().deployTemplate({ stackName: "async-stack", template: { Resources: { WaitHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", }, }, },});
await simAws.cloudFormation().waitForStackDeployComplete("async-stack");You can also wait through the returned stack object:
/** * Waiting via the returned simulated CloudFormation Stack object. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "stack-object-wait", template: { Resources: { WaitHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", }, }, },});
await stack.waitForDeployComplete();If your scenario also depends on asynchronous work scheduled by the created services, you can drain the broader simulator background tasks:
/** * Waiting for simulated AWS background tasks to complete. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
// Deploy stacks and interact with simulated services...
await simAws.backgroundTasksComplete();Updating a stack
Section titled “Updating a stack”UpdateStackCommand applies a changed template to a stack that is already deployed. Resources the
new template adds are created, resources it drops are deleted, and resources it changed are
replaced. Everything else is left alone, holding whatever it holds in simulated S3, DynamoDB or
anywhere else. That is what lets a long-running local process pick up an infrastructure change
without restarting and losing its data.
/** * Applying a changed template with UpdateStackCommand. */
import { CreateStackCommand, UpdateStackCommand,} from "@aws-sdk/client-cloudformation";import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const simCfn = simAws.cloudFormation();
const siteBucket = { Type: "AWS::S3::Bucket", Properties: { BucketName: "site-content" },};
const deployedTemplate = JSON.stringify({ Resources: { SiteBucket: siteBucket },});
const changedTemplate = JSON.stringify({ Resources: { SiteBucket: siteBucket, UploadsBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "site-uploads" }, }, },});
await simCfn.createStack( new CreateStackCommand({ StackName: "site", TemplateBody: deployedTemplate, }),);await simCfn.waitForStackDeployComplete("site");
await simAws.s3().putObject( new PutObjectCommand({ Bucket: "site-content", Key: "index.html", Body: "<h1>Hello</h1>", }),);
// Apply the changed template to the stack that is already there.await simCfn.updateStack( new UpdateStackCommand({ StackName: "site", TemplateBody: changedTemplate, }),);await simCfn.waitForStackUpdateComplete("site");
// The bucket the new template adds is in simulated S3.console.log(simAws.s3().getSimBucketByName("site-uploads"));
// And the bucket the template did not change still holds its object.const page = await simAws .s3() .getObject( new GetObjectCommand({ Bucket: "site-content", Key: "index.html" }), );console.log(page.Body);The resource work runs in the background, as deployment does. updateStack(...) returns once the
stack has moved to UPDATE_IN_PROGRESS, and waitForStackUpdateComplete(...) waits for the
resources to change. DescribeStacksCommand reports UPDATE_IN_PROGRESS in between and
UPDATE_COMPLETE after, along with the outputs resolved again against the new template.
Updating a stack name that is not there is refused with the same ValidationError that
DescribeStacksCommand refuses it with, because there is nothing else an update of a stack that is
not there could mean.
What counts as a change
Section titled “What counts as a change”Resources are compared as they resolve rather than as they are written, so a changed parameter value
shows up as a changed resource even when the template body is identical, and a template reordered
without being changed does not. Outputs are compared the same way. The rest of the template body is
compared as written, so a change to a section the simulator does not act on, such as Description,
is still an update.
A template that changes nothing at all is refused with a ValidationError reading
No updates are to be performed., which is what CloudFormation answers. So is an update asked for
while another is still running.
Changed resources are replaced
Section titled “Changed resources are replaced”A resource whose template entry changed is deleted and created again from the new template. Real CloudFormation updates most properties in place and keeps what the resource holds, so this is a divergence worth knowing about: a bucket that gains a property loses its objects here, where in AWS it would keep them. In-place update is the obvious next step and is not implemented yet.
Two things follow from replacement:
- A resource naming a replaced resource is replaced too, all the way up the dependency chain, so nothing is left pointing at a resource that has gone. Real CloudFormation hands the dependent the new physical name instead of recreating it.
UpdateReplacePolicyis not read. HonouringRetainwould leave the old resource holding the name the replacement needs, and CDK marks buckets and tables with it as a matter of course, so every such update would fail. The old resource is deleted whatever the policy says.
A failed update leaves the stack in UPDATE_FAILED with the reason on it, and leaves the resources
where the update got to. There is no rollback to the previous template.
waitForStackUpdateComplete(...) rethrows the error, and DescribeStacksCommand reports it as
StackStatusReason. Dealing with the cause and sending UpdateStackCommand again applies the rest
of the change.
Deleting a stack
Section titled “Deleting a stack”DeleteStackCommand deletes the resources a stack created, in the reverse of the order they were
created in, and then releases the stack name.
/** * Deleting a simulated CloudFormation Stack with DeleteStackCommand. */
import { CreateStackCommand, DeleteStackCommand,} from "@aws-sdk/client-cloudformation";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const simCfn = simAws.cloudFormation();
const templateBody = JSON.stringify({ Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "deletable-stack-bucket", }, }, },});
await simCfn.createStack( new CreateStackCommand({ StackName: "deletable-stack", TemplateBody: templateBody, }),);await simCfn.waitForStackDeployComplete("deletable-stack");
await simCfn.deleteStack( new DeleteStackCommand({ StackName: "deletable-stack" }),);await simCfn.waitForStackDeleteComplete("deletable-stack");
// The Bucket has gone from simulated S3.console.log(simAws.s3().getSimBucketByName("deletable-stack-bucket"));
// And the Stack name is free, so the same Stack can be deployed again.await simCfn.createStack( new CreateStackCommand({ StackName: "deletable-stack", TemplateBody: templateBody, }),);await simCfn.waitForStackDeployComplete("deletable-stack");Deletion runs in the background, as deployment does. deleteStack(...) returns once the stack has
moved to DELETE_IN_PROGRESS, and waitForStackDeleteComplete(...) waits for the resources to go.
DescribeStacksCommand reports DELETE_IN_PROGRESS in between, and then refuses the stack name with
a ValidationError once the deletion has finished, which is how CloudFormation answers a name it no
longer holds.
Deleting a stack name that is not there succeeds rather than failing, as it does in CloudFormation.
When a resource cannot be deleted
Section titled “When a resource cannot be deleted”Some resources refuse to go, the same way they do in AWS. An S3 bucket that still holds objects is
the common one: CloudFormation fails there rather than emptying the bucket first, which is why CDK
ships an autoDeleteObjects custom resource.
A refusal leaves the stack in DELETE_FAILED with the reason on it, and keeps the stack name in use.
waitForStackDeleteComplete(...) rethrows the error, and DescribeStacksCommand reports it as
StackStatusReason. Dealing with the cause and sending DeleteStackCommand again deletes the stack.
DeletionPolicy
Section titled “DeletionPolicy”A resource declared with DeletionPolicy: Retain is left in simulated AWS and reported as
DELETE_SKIPPED, which is what CloudFormation does with it. The rest of the stack still deletes
around it, and the stack name is still released. RetainExceptOnCreate is treated the same way,
because the two differ only in what a rolled back creation does, and sim CloudFormation does not roll
a deployment back.
Retained resources are readable from the stack:
console.log(stack.retainedResources.map((resource) => resource.logicalId));Parameters
Section titled “Parameters”Template parameters can be supplied when creating a stack.
/** * Supplying simulated CloudFormation Parameters. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "parameter-stack", template: { Parameters: { BucketName: { Type: "String", Default: "default-parameter-bucket", }, }, Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { Ref: "BucketName", }, }, }, }, }, parameters: { BucketName: "configured-parameter-bucket", },});
await stack.waitForDeployComplete();
console.log( simAws.s3().getSimBucketByName("configured-parameter-bucket")?.bucketName,);When a parameter value is not supplied, the template default is used if present.
Intrinsic functions
Section titled “Intrinsic functions”Sim CloudFormation supports common intrinsic functions used by supported resources.
/** * Using Ref between simulated CFN resources. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "ref-stack", template: { Resources: { SourceBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "source-ref-bucket", }, }, WebsiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { "Fn::Join": ["-", [{ Ref: "SourceBucket" }, "website"]], }, }, }, }, },});
await stack.waitForDeployComplete();
console.log( simAws.s3().getSimBucketByName("source-ref-bucket-website")?.bucketName,);For supported resource types, Ref returns the resource-specific CloudFormation value. For example,
an S3 Bucket Ref returns the Bucket name.
Fn::GetAtt
Section titled “Fn::GetAtt”/** * Using Fn::GetAtt with a simulated CloudFront Distribution. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "get-att-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "get-att-site-bucket", }, }, SiteDistribution: { Type: "AWS::CloudFront::Distribution", Properties: { DistributionConfig: { Enabled: true, Origins: [ { Id: "SiteOrigin", DomainName: "get-att-site-bucket.s3.amazonaws.com", S3OriginConfig: {}, }, ], DefaultCacheBehavior: { TargetOriginId: "SiteOrigin", ViewerProtocolPolicy: "allow-all", }, }, }, }, DistributionNameHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", Properties: { Value: { "Fn::GetAtt": ["SiteDistribution", "DomainName"], }, }, }, }, },});
await stack.waitForDeployComplete();For AWS::CloudFront::Distribution, Fn::GetAtt: ["Distribution", "DomainName"] returns the
simulated CloudFront hostname, such as e123example.cloudfront.net.
Values from a skipped Resource
Section titled “Values from a skipped Resource”A Resource that was skipped, because its type is not simulated or because there is no simulated
Resource to create at all, still answers both intrinsics. Ref returns the logical ID, and
Fn::GetAtt returns the string <logical ID>.<attribute name>.
/** * The stand-in values a skipped CloudFormation Resource answers with. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "stand-in-stack", template: { Resources: { AlarmRule: { Type: "AWS::Events::Rule", }, }, Outputs: { RuleRef: { Value: { Ref: "AlarmRule" } }, RuleArn: { Value: { "Fn::GetAtt": ["AlarmRule", "Arn"] } }, }, },});
await stack.waitForDeployComplete();
console.log(stack.outputs.get("RuleRef")?.value);// "AlarmRule"
console.log(stack.outputs.get("RuleArn")?.value);// "AlarmRule.Arn"
for (const skipped of stack.skippedResources) { console.log(skipped.logicalId, skipped.skippedReason); // "AlarmRule Unsupported sim CloudFormation Resource service Events"}The stand-ins are what lets a template with unsimulated Resources in it deploy at all. Without them,
every Resource holding a Ref or Fn::GetAtt to a skipped Resource would fail too, and so would
every Resource depending on those, until one EventBridge rule took the whole stack down with it. The skip
stays where it happened.
A stand-in is deliberately not ARN-shaped, so it fails closed wherever the simulator reads it.
- In an IAM policy
Resourceit matches no ARN, so a caller relying on that statement is denied. - In a property that is parsed as an ARN it is refused as malformed, and that Resource fails.
Handing
Fn::GetAtt: ["Orders", "StreamArn"]from a skipped DynamoDB table to anAWS::Lambda::EventSourceMappingfails withEventSourceArn Orders.StreamArn names no simulated Lambda event source. - Handed to a Lambda function through its environment, it names something that is not there, so the
function’s own SDK call fails as a call for a missing resource does. A
PutItemnaming the skipped table getsResourceNotFoundException: No DynamoDB Table named Orders.
A stand-in stands in for something absent, so it is not a value to rely on. A test asserting against
one is asserting on a Resource that was never created. stack.skippedResources is where to find out
which Resources those are and why, under
Inspecting stacks and resources.
Fn::Join
Section titled “Fn::Join”/** * Joining literal values and Refs in a simulated CFN template. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.cloudFormation().deployTemplate({ stackName: "join-stack", template: { Parameters: { BucketPrefix: { Type: "String", Default: "joined", }, }, Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { "Fn::Join": ["-", [{ Ref: "BucketPrefix" }, "site", "bucket"]], }, }, }, }, },});Fn::Sub
Section titled “Fn::Sub”/** * Substituting parameter and resource values in a simulated CFN template. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "sub-stack", template: { Parameters: { SiteName: { Type: "String", Default: "docs", }, }, Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { // eslint-disable-next-line no-template-curly-in-string "Fn::Sub": "${SiteName}-site-bucket", }, }, }, }, },});
await stack.waitForDeployComplete();
console.log(simAws.s3().getSimBucketByName("docs-site-bucket")?.bucketName);Fn::FindInMap
Section titled “Fn::FindInMap”A template Mappings section holds two levels of keys against a value. Fn::FindInMap reads one of
those values, given the map name, the top-level key and the second-level key.
/** * Reading a value from template Mappings in a simulated CFN template. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "find-in-map-stack", template: { Parameters: { Environment: { Type: "String", Default: "staging", }, }, Mappings: { EnvironmentMap: { staging: { BucketName: "staging-site-bucket" }, production: { BucketName: "production-site-bucket" }, }, }, Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { "Fn::FindInMap": [ "EnvironmentMap", { Ref: "Environment" }, "BucketName", ], }, }, }, }, },});
await stack.waitForDeployComplete();
console.log(simAws.s3().getSimBucketByName("staging-site-bucket")?.bucketName);Each of the three arguments can be a nested expression rather than a literal string, as long as it
resolves to a string. The example above uses a Ref to a parameter for the top-level key. A Ref to
the AWS::Region pseudo parameter works the same way, for the per-region maps that Fn::FindInMap
is most often used for, and a nested Fn::FindInMap can supply any of the three arguments.
The value a lookup returns does not have to be a string. A list value is returned as a list.
Fn::FindInMap is resolved when the template is read, before any resource is created, so it can be
used in resource properties and in Outputs. A map name or key that is not in Mappings fails the
deployment with an error naming the path that could not be found.
Fn::Split and Fn::Select
Section titled “Fn::Split and Fn::Select”Fn::Split cuts a string into a list on a delimiter. Fn::Select reads one value out of a list by
its zero-based index. They are usually written together, to pull one part out of a string another
resource gave.
/** * Naming a bucket after part of another bucket's domain name. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "select-split-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "site-bucket" }, }, LogsBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { "Fn::Join": [ "-", [ { "Fn::Select": [ 0, { "Fn::Split": [ ".", { "Fn::GetAtt": ["SiteBucket", "DomainName"] }, ], }, ], }, "logs", ], ], }, }, }, }, },});
await stack.waitForDeployComplete();
// site-bucket-logs, from the first part of site-bucket.s3.amazonaws.comconsole.log(simAws.s3().getSimBucketByName("site-bucket-logs")?.bucketName);The delimiter is a literal string. The string being split can be any expression that resolves to a
string, including a Ref, an Fn::GetAtt or another function. A delimiter the string does not
contain gives a one-element list, and a delimiter at the start or end of the string gives an empty
element there, as CloudFormation does.
Fn::Select takes its list from a literal list, from Fn::Split, or from anything else that
resolves to a list, such as an Fn::FindInMap of a list value. The index is a number or a string of
digits, so a Ref to a parameter can supply it.
That pair is how a host is read out of a URL. CDK writes this shape when a CloudFront origin points at a Lambda function URL:
{ "DomainName": { "Fn::Select": [ 2, { "Fn::Split": ["/", { "Fn::GetAtt": ["Url", "FunctionUrl"] }] } ] }}https://abc123.lambda-url.eu-west-2.on.aws/ splits into
["https:", "", "abc123.lambda-url.eu-west-2.on.aws", ""], so index 2 is the host.
An index past the end of the list, a negative or fractional index, and a second argument that is not
a list all fail the deployment, as they are all templates AWS rejects. The error names the resource
and the property path the value sat at, for example
Sim CloudFormation Resource LogsBucket value at Properties.BucketName.
Conditions
Section titled “Conditions”A template Conditions section names boolean expressions over the stack’s parameter values. A
condition decides whether a resource is created, and which value Fn::If gives a property or an
output.
/** * Choosing resources and property values by condition in a simulated CFN template. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "conditions-stack", template: { Parameters: { EnvName: { Type: "String" }, }, Conditions: { IsProd: { "Fn::Equals": [{ Ref: "EnvName" }, "prod"] }, }, Resources: { Backups: { Type: "AWS::S3::Bucket", Condition: "IsProd", Properties: { BucketName: "site-backups" }, }, Site: { Type: "AWS::S3::Bucket", Properties: { BucketName: { // eslint-disable-next-line no-template-curly-in-string "Fn::If": ["IsProd", "site", { "Fn::Sub": "site-${EnvName}" }], }, }, }, }, }, parameters: { EnvName: "dev" },});
await stack.waitForDeployComplete();
// site-devconsole.log(simAws.s3().getSimBucketByName("site-dev")?.bucketName);
// false, because IsProd is falseconsole.log(stack.resources.has("Backups"));Writing a condition
Section titled “Writing a condition”A condition is built from Fn::Equals, Fn::And, Fn::Or and Fn::Not. Fn::And and Fn::Or
take a list of two to ten conditions, and Fn::Not takes a list of exactly one. A condition can
name another condition with { "Condition": "OtherCondition" }, in any order, so a condition may
name one written below it in the section.
{ "IsProd": { "Fn::Equals": [{ "Ref": "EnvName" }, "prod"] }, "IsStaging": { "Fn::Equals": [{ "Ref": "EnvName" }, "staging"] }, "IsDeployed": { "Fn::Or": [{ "Condition": "IsProd" }, { "Condition": "IsStaging" }] }}Fn::Equals compares its two values as strings, as CloudFormation does, so a JSON number in the
template matches the string a parameter carries.
The whole section is evaluated once per deployment, before any resource is created, so a condition
can read parameters and pseudo parameters and nothing else. A comparison that would need a created
resource, such as an Fn::GetAtt, fails the deployment rather than reading as false.
Fn::If
Section titled “Fn::If”Fn::If takes a condition name, a value to use when it is true, and a value to use when it is
false. It works anywhere a resource property or an output value is read.
Only the branch the condition selects is resolved. The other branch is left alone, so it can name a resource this deployment does not create.
The resource Condition attribute
Section titled “The resource Condition attribute”A resource carrying a Condition attribute whose condition is false is not created at all. It is
absent from stack.resources, which is different from a resource sim CloudFormation skips: a
skipped resource stays in the stack and answers Ref and Fn::GetAtt with
stand-in values.
Because the resource does not exist, another resource naming it fails the deployment, with an error
naming both resources and the condition. That covers a Ref or Fn::GetAtt that is actually
reached, and a DependsOn. A name carried only by the branch of an Fn::If the condition did not
select is not reached, so it does not fail.
A Condition attribute naming a condition the template does not define fails the same way.
Resource dependencies
Section titled “Resource dependencies”Resources can depend on each other explicitly with DependsOn.
/** * Explicit resource dependencies in a simulated CFN template. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "depends-on-stack", template: { Resources: { SourceBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "dependency-source-bucket", }, }, DependentBucket: { Type: "AWS::S3::Bucket", DependsOn: "SourceBucket", Properties: { BucketName: "dependency-target-bucket", }, }, }, },});
await stack.waitForDeployComplete();Resources that reference another resource with Ref are also created after the referenced resource
is ready.
Deploying synthesized CDK templates
Section titled “Deploying synthesized CDK templates”Use deployTemplateFile(...) to deploy a JSON template file, including templates produced by CDK
synthesis.
/** * Deploying a synthesized CDK template file into simulated AWS. */
import path from "node:path";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws .cloudFormation() .deployTemplateFile( path.join(process.cwd(), "cdk.out", "TestStack.template.json"), );
await stack.waitForDeployComplete();You can also pass an object when you need extra deployment options:
import path from "node:path";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplateFile({ templatePath: path.join(process.cwd(), "cdk.out", "TestStack.template.json"), stackName: "local-cdk-stack",});
await stack.waitForDeployComplete();This is useful for local integration tests where you want CDK to produce the template, then Yulin to create the simulated resources from that synthesized output template.
Editing a synthesized template before deploying it
Section titled “Editing a synthesized template before deploying it”Sometimes a synthesized template needs a change before Yulin will deploy it, such as dropping a
resource or property that this simulator does not accept. Read the file, edit the parsed object,
then deploy it with deployTemplate(...), naming the file it came from:
/** * Deploying a synthesized CDK template edited in memory. */
import { readFile } from "node:fs/promises";import path from "node:path";
import { SimAws } from "@kensio/yulin";
const templatePath = path.join( process.cwd(), "cdk.out", "TestStack.template.json",);
const synthesized = JSON.parse(await readFile(templatePath, "utf8")) as { Resources: Record<string, { Type: string }>;};
const resources = Object.fromEntries( Object.entries(synthesized.Resources).filter( ([logicalId]) => logicalId !== "AnalyticsQueue", ),);
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "local-cdk-stack", template: { ...synthesized, Resources: resources }, templatePath,});
await stack.waitForDeployComplete();The template file is not read as the template: the template object is what gets deployed.
templatePath only tells Yulin which cloud assembly the template came from, so it can find the
sibling TestStack.assets.json manifest and the staged asset directories beside it. Without it,
anything that needs a CDK asset, such as a Custom::CDKBucketDeployment or a Lambda function
bundled with Code.fromAsset, fails with No CDK assets manifest is available.
Adapting a synthesized template on the way in
Section titled “Adapting a synthesized template on the way in”Editing the parsed object works for a template you deploy once. A template you keep reading, because
it is watched or applied again as an update, needs the same change made
every time it is read. transform is that: it is given the parsed template and answers with the one
to deploy, on the deployment and again on every change:
/** * Adapting a synthesized template every time it is read. */
import { SimAws } from "@kensio/yulin";import type { CfnTemplateBodyRecord } from "@kensio/yulin/cloudformation";
const simAws = new SimAws();
/** * Drop the records pointing at a hosted zone that only exists in the real * account. */function withoutDnsRecords( template: CfnTemplateBodyRecord,): CfnTemplateBodyRecord { const resources = Object.fromEntries( Object.entries(template.Resources).filter( ([, resource]) => (resource as { Type?: string }).Type !== "AWS::Route53::RecordSet", ), );
return { ...template, Resources: resources };}
await simAws.cloudFormation().deployTemplateFile({ templatePath: "cdk.out/TestStack.template.json", transform: withoutDnsRecords, watch: true,});This is for what a simulation cannot resolve at all, such as an ARN carrying a real account or a
hosted zone ID that came from HostedZone.fromLookup. A property Yulin does not model is usually not
one you need this for: S3, DynamoDB, Cognito, API Gateway v2, SQS and KMS
record it and carry on.
The template file is still the real one, so there is no derived .local.template.json in cdk.out
to keep in step with it. Staged assets resolve as they always did, from the assets manifest beside
templatePath, since the cloud assembly is found by path rather than read out of the template.
updateTemplateFile(...) takes it too, for a consumer driving updates itself. Give it the same
deployment object, so the difference applied is the difference in the file.
A transform that throws fails the deployment, with what it threw as the cause. On a watched change it is reported the way a failed update is: the stack keeps the resources it had, and the watch carries on to the next save.
Applying a changed template file
Section titled “Applying a changed template file”updateTemplateFile(...) reads a deployed template file again and applies it to its stack, the same
way UpdateStackCommand applies a changed template body. Give it what the
deployment was given, since parameters are part of what an update applies:
/** * Applying a synthesized template file to the stack it was deployed as. */
import path from "node:path";
import { SimAws } from "@kensio/yulin";
const templatePath = path.join( process.cwd(), "cdk.out", "TestStack.template.json",);
const simAws = new SimAws();
await simAws.cloudFormation().deployTemplateFile({ templatePath });
// Something synthesizes the stack again here.
await simAws.cloudFormation().updateTemplateFile({ templatePath });The sibling assets manifest is read again with the template, so a resource the update replaces reads the assets that synthesis staged rather than the ones the stack was deployed with.
A file written without being changed is refused with No updates are to be performed., and a failed
update leaves the stack in UPDATE_FAILED holding whatever the update reached. There is no rollback
to the template it was deployed from, so a failure part way through has already deleted, replaced or
created some of the resources the change asked for.
Watching a template file
Section titled “Watching a template file”A local dev process holds simulated data that a restart would throw away, and a template file is
data rather than code. watch keeps reading the file, so re-synthesizing the stack updates it in
place while the process carries on:
/** * Updating a deployed stack whenever its template file is synthesized again. */
import path from "node:path";
import { SimAws } from "@kensio/yulin";import { serveSimAws } from "@kensio/yulin/serve";
const simAws = new SimAws();const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
await simAws.cloudFormation().deployTemplateFile({ templatePath: path.join(process.cwd(), "cdk.out", "TestStack.template.json"), watch: { reload: srv, },});watch: true watches with nothing to do afterwards.
reload is the local server, and reloads the browsers connected to it once the update is complete.
It reloads when the resources have changed rather than when the write lands, so a browser reloads
onto the resources the new template asked for. A write that changed nothing is a no-op, so nothing
reloads for it, and neither does an update that failed: a browser should not be sent to a stack the
update did not reach. Anything with a reload() method will do, so a test can watch a template
without serving anything.
A server serving without live reload can never reload anything, and says so as the deployment asks
it to rather than on the first change, which is a long way from the mistake. Serve with
{ liveReload: true }.
onUpdated runs once the update is complete too, for whatever else a change is worth doing, with or
without a reload alongside it. Given both, the callback runs first and the reload follows it, so a
browser arriving on the new resources finds whatever the callback left ready for it.
onFailed is given an update the changed template did not survive. It reports the failure and
nothing else: the stack is left holding whatever the update reached, as an update through the
command is. What a failure does keep is the process, and the resources it never got to, so a
template that no longer deploys leaves a working environment where a restart on it would leave
none.
A burst of writes is one update. Saving a file is several filesystem events, so changes are held
until they stop arriving. settleMs is how long that wait is, and it defaults to the 250ms
yulin watch settles at. A synth that
keeps writing is updated from after five seconds of it, rather than being held off until it stops.
transform runs again on every change, so a
template that needs adapting before Yulin will take it can still be watched as the file synthesis
writes.
Watching holds a filesystem handle open, so the process does not exit on its own. That is what a dev
process wants. Anything with an end, such as a test, calls stopWatchingTemplateFiles() when it is
done. watchedTemplateFiles() names what is being watched. Both are per Account and Region, so a
simulation deploying into more than one has one call each.
simAws.close() is the one that is not: it lets go
of the template watches in every scope, along with everything else the environment is holding, and a
served environment gets that from srv.close().
Yulin never synthesizes anything. It reads the output template, so run your own cdk synth and let
the watch pick up what it writes.
Under yulin watch
Section titled “Under yulin watch”yulin watch restarts the process when a
deployed template changes. A watched template is left to the process that is watching it instead, so
the stack updates in place and everything held in simulated S3, DynamoDB and SQS stays where it is.
Nothing needs configuring for that: the process names the file it is holding.
CDK S3 BucketDeployment
Section titled “CDK S3 BucketDeployment”Yulin can simulate selected CDK custom resources. A common use case is CDK S3 BucketDeployment, where local files are deployed into a simulated S3 Bucket.
/** * Serving CDK BucketDeployment files through simulated S3. */
import { SimAws } from "@kensio/yulin";import { serveSimAws } from "@kensio/yulin/serve";
const simAws = new SimAws();const srv = await serveSimAws({ simAws });
try { await simAws .cloudFormation() .deployTemplateFile("cdk.out/TestStack.template.json");
const response = await fetch( `http://foo-bucket.s3-website.us-east-1.sim-aws.localhost:${srv.port}/`, );
console.log(response.status); console.log(await response.text());} finally { await srv.close();}The files in the staged asset directory become Objects in the destination Bucket, keyed by their path relative to the asset root. When the Bucket is configured for website hosting, or sits behind a CloudFront Distribution, those Objects are what gets served.
This is the aws s3 sync the real provider function shells out to, so the properties CDK synthesizes
around it are read the same way:
DestinationBucketKeyPrefixputs the Objects under a key prefix.ExcludeandIncludechoose which files are copied. EveryExcludepattern is applied first and then everyIncludeone, and the last pattern to match a path decides, which is what makesexclude: ["*"], include: ["*.txt"]mean “only the text files”. A file no pattern matches is copied.*matches across/, sodata/*covers everything under adatadirectory.SystemMetadatasets content headers on every Object the deployment copies, such ascontent-encodingorcache-control. Without it, the content type is guessed from the file extension. See Object system metadata for what comes back on a read. The deployment also tells the destination Bucket what it publishes, so a directory mounted over that Bucket for local development is served with the same headers without restating them.Pruneremoves the Objects the deployment covers and its source no longer holds. It is on unless the deployment turns it off, as the construct is. Pruning only considers what the filters and the key prefix select, so a deployment does not delete Objects it would never have copied.
Several deployments can share one Bucket, which is the usual arrangement when the headers differ by
file type: a BucketDeployment sets them for all of its files at once, so a second deployment is how
the rest of the site gets different ones. Give the second one prune: false, or filters that do not
overlap the first, the same as you would in AWS.
A deployment with more than one entry in SourceObjectKeys copies each source in turn, and a path
two of them share ends up with the later one’s content.
Filter patterns take * and ?. The CLI also takes character classes such as [abc], and a pattern
using one is refused by name rather than matched as written, since a pattern that quietly means
something else would copy the wrong files.
The provider CDK synthesizes
Section titled “The provider CDK synthesizes”One BucketDeployment construct is four resources in the synthesized template, and only one of them
is the Custom::CDKBucketDeployment above. The other three are the provider that would have run it
in AWS: an AWS CLI Lambda Layer, a Python Lambda function, and that function’s log group. A second
deployment adds another Layer and another custom resource, and shares the one function, because CDK
builds it as a singleton.
None of those three do anything here. Yulin makes the copy itself, so the function is never invoked,
the Layer it would have loaded the CLI from is never read, and nothing is ever written to the log
group. They are reported in stack.inertResources rather than
as skipped resources, so a stack whose deployments all worked reports no gaps at all.
That matters beyond tidiness. Sim Lambda declines the provider on its Python runtime with a message saying to bind a real in-process handler to the function, which is sound advice for a Python function of your own and exactly the wrong thing to do here: it would replace a working simulation with a hand-written one.
The provider is found through the ServiceToken its custom resource names it by, not by the logical
ID CDK generated for it, which is a hash of the construct path and not something to match on.
S3 Bucket notifications
Section titled “S3 Bucket notifications”The NotificationConfiguration property of AWS::S3::Bucket deploys through the ordinary
PutBucketNotificationConfiguration path, so an Object put into the deployed Bucket reaches the
deployed function. CloudFormation spells the configuration differently from the SDK in four places,
and Yulin reads the CloudFormation spelling and refuses the others rather than deploying a
configuration that quietly lost its filter.
Real CloudFormation has a circular dependency here. The Bucket needs the function’s ARN and the
function’s permission needs the Bucket’s ARN, so a template hardcodes BucketName, names the Bucket
by ARN literal on the permission, and adds a DependsOn so the permission is in place before S3
validates the destination. Simulated CloudFormation needs the same, and surfaces the alternative as a
dependency resolution failure.
Note that every other AWS::S3::Bucket property the simulator has no behaviour for fails the stack
by name. See Buckets from CloudFormation.
From a CDK app
Section titled “From a CDK app”bucket.addEventNotification(...) synthesizes a Custom::S3BucketNotifications resource rather than
a Bucket property. Sim CloudFormation applies the configuration it carries through the ordinary
PutBucketNotificationConfiguration path, so an Object put into the deployed Bucket reaches the
deployed function.
Deploy into an Account and Region matching the ones the CDK app synthesized for. The SourceAccount
on the AWS::Lambda::Permission CDK writes beside the notification is a synth-time literal, so a
stack deployed into another Account leaves S3 unable to validate the destination, and the stack
fails.
See Event notifications in the S3 docs for the configuration itself and what it refuses.
CloudFront resources from CDK
Section titled “CloudFront resources from CDK”Sim CloudFormation can create CloudFront Distributions from CloudFormation or CDK templates.
/** * Deploying a template with S3 and CloudFront resources. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "cloudfront-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "cloudfront-site-bucket", }, }, SiteDistribution: { Type: "AWS::CloudFront::Distribution", Properties: { DistributionConfig: { Aliases: ["cdn.example.test"], Enabled: true, Origins: [ { Id: "SiteOrigin", DomainName: "cloudfront-site-bucket.s3.amazonaws.com", S3OriginConfig: {}, }, ], DefaultCacheBehavior: { TargetOriginId: "SiteOrigin", ViewerProtocolPolicy: "allow-all", }, }, }, }, }, },});
await stack.waitForDeployComplete();
const distributionResource = stack.getResource("SiteDistribution");console.log(distributionResource?.simResource);When served on localhost, the resulting Distribution can be requested through its simulated CloudFront hostname adapted to the local server.
CloudFront Function bindings
Section titled “CloudFront Function bindings”When a CDK template contains a CloudFront Function, you can bind the template resource to a real local handler function. This lets local integration tests execute the same handler function that will run at the CloudFront edge.
/** * Binding a local CloudFront Function handler during template deployment. */
import { SimAws } from "@kensio/yulin";import type { CloudFrontFunction } from "@kensio/yulin/cloudfront";import { serveSimAws } from "@kensio/yulin/serve";
const simAws = new SimAws();const srv = await serveSimAws({ simAws });
function viewerRequestHandler( event: CloudFrontFunction.ViewerRequestEvent,): CloudFrontFunction.Request | CloudFrontFunction.Response { if (event.request.uri === "/redirect-me.html") { return { statusCode: 302, statusDescription: "Found", headers: { location: { value: "https://example.test/from-bound-handler.html", }, }, }; }
return event.request;}
try { const stack = await simAws.cloudFormation().deployTemplateFile({ templatePath: "cdk.out/TestStack.template.json", bindings: [ { logicalId: "RewriteFunction", handler: viewerRequestHandler, }, ], });
await stack.waitForDeployComplete();
const distributionResource = stack.getResource("SiteDistribution"); const distribution = distributionResource?.simResource;
if ( distribution === undefined || !("distributionId" in distribution) || typeof distribution.distributionId !== "string" ) { throw new Error("Expected simulated CloudFront Distribution"); }
const distributionHost = `${distribution.distributionId.toLowerCase()}.cloudfront.net`; const response = await fetch( srv.localUrl(`http://${distributionHost}/redirect-me.html`), { redirect: "manual" }, );
console.log(response.status); console.log(response.headers.get("location"));} finally { await srv.close();}The bindings array matches a template resource logical ID to a local handler function. Use it when
CDK has embedded or transformed CloudFront Function source in synthesized output, but your test wants
to provide an executable local function directly.
Lambda function bindings
Section titled “Lambda function bindings”AWS::Lambda::Function resources support the same bindings. The deployed function is backed by your
real in-process handler, so tests can close over test state and step through the handler in a
debugger, while the stack still wires roles, grants and references as the template declares. A bound
function may omit template Code and Handler entirely.
/** * Binding a real in-process Lambda handler during template deployment. */
import { InvokeCommand } from "@aws-sdk/client-lambda";import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.cloudFormation().deployTemplate({ stackName: "greeter-stack", template: { Resources: { GreeterFunction: { Type: "AWS::Lambda::Function", Properties: { FunctionName: "greeter", Role: "arn:aws:iam::111111111111:role/GreeterRole", }, }, }, }, bindings: [ { logicalId: "GreeterFunction", handler: (event: { name: string }): string => `Hello ${event.name}`, }, ],});
const output = await simAws.lambda().invoke( new InvokeCommand({ FunctionName: "greeter", Payload: JSON.stringify({ name: "Yulin" }), }),);
console.log(new TextDecoder().decode(output.Payload));Bindings can target the CloudFormation logical ID (or the CDK construct ID recovered from
synthesized metadata), the function name, or the function ARN. Bound handlers still run with the
function’s execution Role as the ambient simulated caller, so downstream calls made through
SimSdk-intercepted clients are authorized by simulated IAM as on real Lambda. Functions without
a matching binding keep their template code, running in the simulated vm runtime.
Serving deployed resources on localhost
Section titled “Serving deployed resources on localhost”CloudFormation itself is not served as an HTTP API. Instead, you deploy infrastructure through Sim
CloudFormation, then serve the simulated AWS environment with serveSimAws.
/** * Deploy with sim CloudFormation, then serve the simulated resources on localhost. */
import { SimAws } from "@kensio/yulin";import { serveSimAws } from "@kensio/yulin/serve";
const simAws = new SimAws();const srv = await serveSimAws({ simAws });
try { const stack = await simAws.cloudFormation().deployTemplate({ stackName: "local-site-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "local-site-bucket", WebsiteConfiguration: { IndexDocument: "index.html", }, // A website Bucket needs a public Bucket policy, which Block // Public Access refuses until the Bucket opts out. PublicAccessBlockConfiguration: { BlockPublicAcls: true, IgnorePublicAcls: true, }, }, }, SiteBucketPolicy: { Type: "AWS::S3::BucketPolicy", Properties: { Bucket: { Ref: "SiteBucket" }, PolicyDocument: { Version: "2012-10-17", Statement: [ { Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: "arn:aws:s3:::local-site-bucket/*", }, ], }, }, }, }, }, });
await stack.waitForDeployComplete();
await simAws.s3().putObject({ input: { Bucket: "local-site-bucket", Key: "index.html", Body: "<h1>Hello from Sim CloudFormation</h1>", ContentType: "text/html; charset=utf-8", }, });
const websiteUrl = simAws.s3().getBucketWebsiteUrl("local-site-bucket"); const response = await fetch(srv.localUrl(websiteUrl));
console.log(response.status); console.log(await response.text());} finally { await srv.close();}Use srv.localUrl(...) to adapt simulated service URLs to the local server while preserving the
simulated hostname and service routing information.
Accounts and Regions
Section titled “Accounts and Regions”Use SimAws scopes to create stacks in different simulated Accounts and Regions.
/** * Deploying stacks in different simulated Accounts and Regions. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const defaultCfn = simAws.cloudFormation();const euWest2Cfn = simAws.region("eu-west-2").cloudFormation();const accountCfn = simAws.account("111111111111").cloudFormation();const scopedCfn = simAws .account("222222222222") .region("ap-east-1") .cloudFormation();
await defaultCfn.deployTemplate({ stackName: "default-stack", template: { Resources: { DefaultHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", }, }, },});
await euWest2Cfn.deployTemplate({ stackName: "regional-stack", template: { Resources: { RegionalHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", }, }, },});
await accountCfn.deployTemplate({ stackName: "account-stack", template: { Resources: { AccountHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", }, }, },});
await scopedCfn.deployTemplate({ stackName: "scoped-stack", template: { Resources: { ScopedHandle: { Type: "AWS::CloudFormation::WaitConditionHandle", }, }, },});Stacks are scoped to the selected simulated account and region. Resources created by a stack are created through that same simulated account/region scope unless the underlying simulated service has different AWS-like scoping behaviour.
An Account ID can always be written as a plain string, as above. Code that wants to name the type
can get a SimAwsAccountId from simAwsAccountId("111111111111"), which refuses anything that is
not a 12-digit AWS Account ID.
Inspecting stacks and resources
Section titled “Inspecting stacks and resources”After deployment, you can inspect the returned stack and its resources.
/** * Inspecting resources created by a simulated CloudFormation Stack. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "inspect-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "inspect-site-bucket", }, }, }, },});
await stack.waitForDeployComplete();
const bucketResource = stack.getResource("SiteBucket");
console.log(bucketResource?.simResource);This is useful in tests when you want to assert that a specific template resource created the expected simulated service resource.
stack.skippedResources lists the Resources the deployment did not create. Each one carries a
skippedReason naming the type that is not simulated, so a test that expected a resource to exist
can find out why it does not.
/** * Finding out which Resources a simulated CloudFormation Stack skipped. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "skipped-stack", template: { Resources: { SiteBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "skipped-site-bucket", }, }, AlarmRule: { Type: "AWS::Events::Rule", }, }, },});
await stack.waitForDeployComplete();
console.log(stack.skippedResources.map((resource) => resource.logicalId));// ["AlarmRule"]
console.log(stack.getResource("AlarmRule")?.skippedReason);// "Unsupported sim CloudFormation Resource service Events"A skipped Resource is still in stack.resources, and still answers Ref and Fn::GetAtt with
stand-in values.
A skip is not always a whole Resource type nothing simulates. A service can decline one Resource of a
type it does create, when that Resource asks for something the service cannot model, and the
skippedReason says which part it was. An AWS::Route53::RecordSet declaring a record type sim
Route53 does not store is skipped with the record type named, so a DNS stack carrying a record the
test is not about still deploys. See record types.
A Resource that was skipped on create is stepped over by a teardown rather than deleted, because
nothing reached simulated AWS to delete. It reaches DELETE_COMPLETE and stays out of
stack.skippedResourceDeletions, which is for Resources that were created and could not be removed.
Resources deliberately left out
Section titled “Resources deliberately left out”stack.skippedResources is for gaps. A Resource it names is one a test written against would find
missing, so some Resources are deliberately kept out of it: the ones the simulator left uncreated on
purpose, because nothing it models could tell them apart from Resources it had created. Those are in
stack.inertResources instead, each with an inertReason for what it would take for the difference
to start mattering.
/** * Telling a Resource a Stack is missing from one it left out on purpose. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "inert-stack", template: { Resources: { AwsCliLayer: { Type: "AWS::Lambda::LayerVersion", Properties: { Description: "/opt/awscli/aws", }, }, AlarmRule: { Type: "AWS::Events::Rule", }, }, },});
await stack.waitForDeployComplete();
console.log(stack.skippedResources.map((resource) => resource.logicalId));// ["AlarmRule"]
console.log(stack.inertResources.map((resource) => resource.logicalId));// ["AwsCliLayer"]
console.log(stack.getResource("AwsCliLayer")?.inertReason);// "sim Lambda runs a function's own code archive, or a real in-process handler// bound to it, so nothing a Layer carries is ever on a simulated function's// module path"An inert Resource behaves in every other way like a skipped one. It is still in stack.resources,
it answers Ref and Fn::GetAtt with the same stand-in values,
and a teardown steps over it.
Two things make a Resource inert. Its type can be one no simulated service reads:
AWS::Lambda::LayerVersion, because sim Lambda runs a function’s own code archive, or a real in-process handler bound to it, and never assembles a Layer onto a function’s module path.AWS::CDK::Metadata, the construct-library analytics CDK adds to every synthesized stack.
Or the stack around it can: the provider Lambda function for a CDK custom resource the simulator carries out itself is inert, and so is that function’s log group. See the provider CDK synthesizes.
Properties a Resource was created without
Section titled “Properties a Resource was created without”Deployment is best effort. A Resource type that is not simulated is skipped and the rest of the
stack still deploys, and the same goes one level down: a property the Resource’s own service cannot
act on does not stop the Resource being created. It is left out, and the omission is recorded in
stack.ignoredProperties.
/** * Finding out which properties a Stack created its Resources without. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "uploads-stack", template: { Resources: { UploadsBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "uploads", VersioningConfiguration: { Status: "Enabled" }, }, }, }, },});
await stack.waitForDeployComplete();
// The Bucket exists and is usable, unversioned.console.log(stack.getResource("UploadsBucket")?.deployed);// true
for (const ignored of stack.ignoredProperties) { console.log(ignored.logicalId, ignored.path, ignored.reason); // "UploadsBucket VersioningConfiguration VersioningConfiguration is a real // AWS::S3::Bucket property simulated S3 does not act on: Object versions // are not simulated, ..."}Each entry names the logicalId and resourceType of the Resource, the path to the property, and
a reason. The path is the whole way down, so a setting on one entry of a list says which entry it
was on, such as GlobalSecondaryIndexes.1.WarmThroughput. The same list is on each Resource as
resource.ignoredProperties.
An ignored property means the simulated Resource behaves differently to the one the template describes. That is the trade this makes: a template deploys as far as it can, and the record is where to check whether what it could not do matters to the test you are writing. A test asserting on object versions, on a dead-letter queue, or on a rotated key needs to look here before trusting the result.
A property name that is not one AWS has is recorded the same way rather than failing the stack. A typo and a property AWS added after this simulator read the docs look identical from here, and a Resource that deploys with the unread name reported is more useful than a stack that fails over either.
Two things are still refused outright, and fail the Resource:
- A property that leaves nothing coherent to create, such as an
AWS::S3::BucketwhoseBucketNameis not a string, or anAWS::DynamoDB::GlobalTablewhose replica list does not include the region the stack is deploying into. Real CloudFormation refuses these templates too. - A value the simulated service itself refuses, which is refused in the same words an SDK caller
gets. An
AWS::SQS::QueuewithFifoQueue: trueis one: a FIFO queue is named<name>.fifo, which simulated SQS refuses, so there is no queue to create under the name the template gave it.
Properties nothing simulated could tell apart are not listed. There is no simulated KMS and Object
bytes are stored as they arrive, so an AWS::S3::Bucket carrying BucketEncryption and Tags, as
almost every Bucket CDK synthesizes does, records nothing: a report of differences that make no
difference is one nobody can read.
Handling deployment failures
Section titled “Handling deployment failures”Some deployment failures happen asynchronously after stack creation has started. To observe those failures in tests, wait for deployment completion.
/** * Observing simulated CloudFormation deployment failures. */
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "failure-stack", template: { Resources: { InvalidBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: "Invalid_Bucket_Name", }, }, }, },});
try { await stack.waitForDeployComplete();} catch (error) { console.error("Stack deployment failed", error);}If you use waitForStackDeployComplete(...), deployment errors are also rethrown there.
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
// Create a stack...
try { await simAws.cloudFormation().waitForStackDeployComplete("failure-stack");} catch (error) { console.error("Stack deployment failed", error);}Standalone SimCloudFormation
Section titled “Standalone SimCloudFormation”Most users should access CloudFormation through SimAws so that CloudFormation can create resources
in the same simulated AWS environment as S3, CloudFront, and other services.
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const simCfn = simAws.cloudFormation();SimCloudFormation is also exported from @kensio/yulin/cloudformation for advanced cases. In
normal application tests, prefer the SimAws entry point.
Available functionality
Section titled “Available functionality”Sim CloudFormation currently supports:
CreateStackCommand,DescribeStacksCommand,UpdateStackCommandandDeleteStackCommand- Waiting for simulated stack deployment, update and deletion completion
- The resource
DeletionPolicyattribute, forRetainandRetainExceptOnCreate deployTemplate(...)for parsed template objects, optionally naming the synthesized template file a template edited in memory came fromdeployTemplateFile(...)for synthesized JSON template filesupdateTemplateFile(...)for applying a synthesized template file to the stack it was deployed as- Watching a deployed template file, updating its stack in place whenever the file changes
- Template
Parameterswith supplied values and defaults - Template
Outputs, resolved after resource creation and read fromstack.outputs - Template
Mappings, read withFn::FindInMap - Template
Conditions, built fromFn::Equals,Fn::And,Fn::OrandFn::Not - The resource
Conditionattribute, which decides whether a resource is created - The
Ref,Fn::GetAtt,Fn::Join,Fn::Sub,Fn::FindInMap,Fn::If,Fn::SplitandFn::Selectintrinsic functions - Explicit resource dependencies with
DependsOn - Implicit dependencies from resource
Refexpressions
The resource types it creates are:
AWS::ApiGatewayV2::Api,AWS::ApiGatewayV2::Integration,AWS::ApiGatewayV2::RouteandAWS::ApiGatewayV2::StageAWS::CertificateManager::CertificateAWS::CloudFormation::WaitConditionHandleAWS::CloudFront::Distribution,AWS::CloudFront::FunctionandAWS::CloudFront::ResponseHeadersPolicyAWS::Cognito::UserPool,AWS::Cognito::UserPoolClientandAWS::Cognito::UserPoolGroupAWS::DynamoDB::TableandAWS::DynamoDB::GlobalTableAWS::IAM::Role,AWS::IAM::ManagedPolicyandAWS::IAM::PolicyAWS::KMS::KeyandAWS::KMS::AliasAWS::Lambda::Function,AWS::Lambda::UrlandAWS::Lambda::PermissionAWS::Route53::HostedZoneandAWS::Route53::RecordSetAWS::S3::BucketandAWS::S3::BucketPolicyAWS::SecretsManager::SecretAWS::SQS::QueueAWS::SSM::Parameter- selected CDK custom resources:
Custom::CDKBucketDeploymentandCustom::S3BucketNotifications
Each service’s own docs describe what its resource types support.
Limitations
Section titled “Limitations”TemplateBodymust be JSON when usingCreateStackCommandorUpdateStackCommand. YAML parsing is not currently provided by the CloudFormation service.- Only supported resource types create simulated service resources. An unsupported resource may be
skipped or may fail the stack, depending on how safely the simulator can model it. A skipped
resource answers
RefandFn::GetAttwith stand-in values rather than the value a created resource would have given. stack.skippedResourcesdeliberately leaves out the resources the simulator did not create on purpose, because nothing it models could tell them apart from ones it had. Those are instack.inertResourcesinstead, and are listed under resources deliberately left out. Read both when accounting for every resource in a template.AWS::Logs::LogGroupis not simulated. The one CDK writes for a custom resource provider is reported as inert, because that provider is never invoked, but a log group a stack declares for itself is skipped like any other unsupported resource type, and nothing simulated writes to it.- A resource property that is not simulated is left out and recorded in
stack.ignoredPropertiesrather than failing the stack, so the resource is created behaving differently to the one the template describes. See properties a Resource was created without for what is still refused outright. - A stack update replaces a changed resource rather than updating it in place, so what the resource held is lost. See changed resources are replaced.
- A watched template file updates its stack in place, which does not make the update itself any gentler: a changed resource is still replaced and loses what it holds, the same as any other update.
- Yulin never synthesizes a CDK app. It watches the synthesized output template, so a change to the
app itself only reaches the stack once something has run
cdk synthover it. - A stack update applies a whole template directly. Change sets are not supported, so
CreateChangeSetCommandandExecuteChangeSetCommandhave nothing behind them, and neither does drift detection. - A failed stack update is not rolled back to the template the stack was deployed from. The stack is
left in
UPDATE_FAILEDholding whatever the update managed. UpdateStackCommandreadsStackName,TemplateBodyandParameters.UsePreviousTemplateandUsePreviousValueare not read, so an update has to be given the whole new template as JSON.- An update asked for while another is still running is refused, as CloudFormation refuses it. There is no queue behind it.
- A stack deletion deletes only the resource types the simulator can delete. A resource type it
creates but cannot delete is recorded in
stack.skippedResourceDeletionsand stepped over, the same way an unsupported resource type is on create, so the stack still deletes with that resource left behind. DeletionPolicyis read forRetainandRetainExceptOnCreateonly.Snapshotis treated asDelete, because no simulated service takes snapshots.UpdateReplacePolicyis not read. A replaced resource is deleted whatever it says, for the reason given under changed resources are replaced.DeleteStackCommandreads onlyStackName.RetainResources,DeletionMode,RoleARNandClientRequestTokenare not read, so a stack left inDELETE_FAILEDcannot be forced through the wayFORCE_DELETE_STACKforces it in AWS.- A deleted stack cannot be described. Real CloudFormation keeps a deleted stack readable by its unique stack ID, and the simulator identifies a stack by its name alone.
Fn::FindInMapaccepts only the three-argument form. The four-argument form, where the fourth argument is{ "DefaultValue": ... }, is rejected.Fn::FindInMaparguments are resolved from literals,Parametersand pseudo parameters. An argument that depends on a created resource, such as aRefto a resource logical ID, fails the resource with a “could not find map” error rather than being resolved. Real CloudFormation allows onlyRefand a nestedFn::FindInMapinsideFn::FindInMap, so this only affects templates real CloudFormation would reject as well, but the simulator does not reject them up front.Fn::Ifis not supported inside theConditionssection itself. It is rejected there rather than read against a half-evaluated section.Fn::SplitandFn::Selectaccept any argument that resolves to the type they need. Real CloudFormation allows only a named set of functions inside each of them, so a template the simulator resolves may still be one CloudFormation rejects.- The
Conditionattribute is read on resources but not on outputs. An output carrying one is resolved and present instack.outputswhichever way its condition falls, where real CloudFormation would leave it out. - Many advanced CloudFormation features are not supported.
Software Engineering by Kensio Software
This page as plain text: llms.txt
Documenting Yulin v1.10.2
