Unforgettable deploy: keep resources coupled with Cloudformation Nested Stacks

Unforgettable deploy: keep resources coupled with Cloudformation Nested Stacks

Requirement

This website is served by an AWS Cloudfront distribution. The distribution has a cache behavior with a lambda@edge function attached to it to complete with “/index.html” the urls ending with a slash character.
Before this post, the Cloudformation Stack with the lambda and the one with the cloudfront distribution were separated. The only link between the two was the output value exported by the former and read by the latter.
Here’s what the AWS web UI lists:

The Cloudfront distribution can’t live without the lambda, so the deployment of the lambda should be done within the distribution one. The risk of having the two stacks completely separated is that an updated version of the lambda is not immediately referenced in the Cloudfront distribution (which is exactly what happened in the previous deploys of the website)

Taken approach and what’s needed

AWS Cloudformation Nested Stacks can be useful. One child stack is referenced in a parent stack and, when the parent is deployed, the resources of the child stack are deployed first.

Some resources should be prepared because they can’t be referenced in the stack itself, or it will end in a loophole

  • The s3 bucket that will contain the artifact (a zipfile in this case) with the lambda code
  • The s3 bucket that will contain the deployed cloudformation template, that is, with the path to the nested stack yaml file resolved

Folder structure

So the folder structure will be:

/
|rootstack.yaml 
|----/lambda-edge
|----|index.mjs
|----|lambdastack.yaml

Preparing the lambda

So, starting from being in the root folder of the project, first thing is preparing the lambda. Compress the source javascript, create the s3 bucket and then upload the compressed source

cd lambda-edge
# (beg my pardon, this is for Windows)
Compress-Archive .\index.mjs .\lambda-edge.zip
# if the bucket is still not ready
aws s3 mb s3://lambda-artifacts-bucket
aws s3 cp lambda-edge.zip s3://lambda-artifacts-bucket

Now the lambda Cloudformation stack can be deployed as a nested stack the lambda is ready in the referenced path

Stack configurations

You can view the full child and parent stacks in the Gist section here

Child stack template output section

The child stack contains the outputs that will be referenced in the parent stack

Outputs:
  LambdaFunctionVersion:
    Description: LambdaFunctionVersion Arn
    Value:
      Ref: LambdaFunctionVersion
    Export:
      Name: LambdaFunctionVersion-Arn

Reference the child stack in the parent stack

Declaring the nested stack

The child stack contained in the parent stack is simply a resource of the type AWS::Cloudformation::Stack itself:

"LambdaEdgeCloudFrontStack":{
    "Type" : "AWS::CloudFormation::Stack",
    "Properties": {
        "TemplateURL":"lambda-edge/lambda-edge.yaml"
    }
}

Reference the output values

Here’s how the child stack values are referenced in the parent stack. This happens in the cache behaviors of the Cloudfront distribution. The ARN of the lambda to be referenced in the distribution points to the output of the child stack:
(Here’s a reference to an example: Cloudformation Nested Stack example)

"LambdaFunctionAssociations": [
    {
        "EventType": "viewer-request",
        "LambdaFunctionARN": {
            "Fn::GetAtt": [
              "LambdaEdgeCloudFrontStack",
              "Outputs.LambdaFunctionVersion"
            ]
        }
    }
]

Package the stack

Once that all the resources are ready, the packaging of stack can be issued with:

aws cloudformation package\
 --template-file parent-stack.json\
 --s3-bucket bucket-for-templates\
 --output-template-file target\packaged-template.json

And here’s the nested stack reference that points to the actual s3 location that can be found in packaged-template.json:

LambdaEdgeCloudFrontStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/cf-templates-***-us-east-1/***.template

Updating the stack

Once that the stack is packaged, a deploy can be issued to update the stack. This will remove the reading of the exported values and inserting the creation of the Lambda@edge function via the nested stack (paramters are not needed, aws will reuse the same values present in the existing stack).

aws cloudformation deploy\
 --template-file target\packaged-template.json
 --stack-name parent-stack-name\
 --capabilities CAPABILITY_NAMED_IAM

All done? Not yet, this command will lead to errors, as the subsequent section will tell.

The unexpected stuff

Having the configuration as it is, two clashes will happen:

Exported output values can’t be duplicated

The lambda standalone stack and the new lambda nested stack have the same output export name, which will produce this error

That is:

Embedded stack B was not successfully created:
Export with name XYZ is already exported by stack A 

Remediation

The output in the child stack can be left without the exports, since the reference is only between parent and child stacks.
New output section for nested stack is:

Outputs:
  LambdaFunctionVersion:
    Description: LambdaFunctionVersion Arn
    Value:
      Ref: LambdaFunctionVersion

Lambda role name can’t be duplicated

Using nested stacks will lead to having to identical lambda functions: one for dev pipeline and one for the prod stacks. Both lambda functions will have their instance of the role, and those can’t have the same role name: before correcting it, this error would prevent the stack update:

That is:

Embedded stack B was not successfully created:
The following resource(s) failed to create:
[LambdaRoleForCF]. 

Remediation

This one is a little trickier. It involved adding a parameter for the parent stack in order to distinguish between dev and prod stacks, and passing it to their respective child stack, in order to differentiate the role name with a suffix containing “dev” or “prod”.
This is the stage paramter in the parameters section in the parent stack:

"Stage": {
  "Description": "Distribution stage",
  "Type": "String",
  "Default" : "dev",
  "AllowedValues" : ["dev", "prod"]
}

And this is the stage parameter in parameter section in the child stack:

  Stage:
    Type: String
    AllowedValues:
        - dev
        - prod

And this is the usage of the Stage parameter in the child stack, in order to change the role name according to the stage:

RoleName: !Join [ "-", [ "LambdaRoleForCF", !Ref Stage ] ]

Updating the stacks

Now that all the issues have been fixed, the final deploy can we issued with the same command as before. The only difference is on using parameter-override, so that dev and prod parent stacks can be differentiated

aws cloudformation deploy --template-file target\packaged-template.json\
  --stack-name website-prod --capabilities CAPABILITY_NAMED_IAM\
  --parameter-overrides Stage=prod

Here we have the AWS web ui showing the updated dev stack

the updated prod stack

and their respective nested stacks

Cleaning

Now the standalone lambda containing the edge function can be removed (only the prod one is left), since its output is not referenced anymore by the parent stack. The deletion can be done via the ui, eliminating the stack and its resources.

Gist references

Child stack

Child stack containing the lambda

Parent stack

Parent stack containing the distribution

Next actions

The activities discussed in this post have fixed the drifts of the stacks that were present due to the lambda being updated in different times. Also the point “automate on cloudformation the deploy of the lambda “ can be marked as done. One step that can be added is having a cloudformation template that contains only the precondition buckets (for cloudformation templates and lambda artifacts) and a codebuild pipeline that automates the steps described above (e.g. packaging and deploying)

Unforgettable deploy: keep resources coupled with Cloudformation Nested Stacks

https://marcoaguzzi.it/2023/11/18/nested-stack/

Author

Marco Aguzzi

Posted on

2023-11-18

Updated on

2024-11-03

Licensed under