AWS CloudFormation Stack Policy Conditions: Don't Replace or Delete My DB Instances on Stack Updates

AWS CloudFormation Stack Policy Conditions: Never Replace or Delete My DB Instances on Stack Updates

Stack policies are among the most helpful features of AWS CloudFormation for protecting your stacks from unintended updates. Let’s say that you have an Amazon RDS instance that you manage with AWS CloudFormation. After a while, you change one of its attributes and update your stack. Guess what! The update you considered innocent was not so after all. It replaces your database instance with all the data inside. It’s a nightmare, isn’t it? So, you wish there were a CloudFormation feature preventing this from happening.

Luckily, CloudFormation has stack policies to help you in situations like this. In this post, I will talk about stack policies and how to use them as a supplementary protection mechanism in your stack updates.

What are CloudFormation stack policies?

So, let’s start with what stack policies are. Most people confuse stack policies with resource policy attributes like UpdatePolicy. But those resource attributes tell CloudFormation how to perform a particular operation, such as taking a backup before deletion. On the other hand, stack policies establish guardrails on which updates you can perform on your resources during a stack update. You can avoid any updates on your all stack resources, specific resources, or members of specific resource types.

You can also allow only a specific type of update for a resource. There are three types of resource updates in a stack update. The change you make can be a simple one, and it may not require a replacement. Or, your change can only be performed by recreating that resource, such as changing the subnet of an EC2 instance. CloudFormation needs to terminate the existing one before launching your EC2 instance in the new subnet. If you have taken my AWS CloudFormation Step by Step: Beginner to Intermediate course, you will recall the lecture we talk about stack updates that require replacements.

The third update type is the one that deletes the resource. Please do not confuse it with stack deletions. This update is only the case where you remove a resource from your template and update your stack. So, CloudFormation deletes the resource but keeps the stack with other resources.

Stack policies are similar to IAM policies in structure. So, you need to type them in JSON. Before showing you an example, let’s discuss the rules you need to know before creating a stack policy.

Rules of stack policies

These are the rules of stack policies:

  • By default, your stack does not have any stack policy unless you add one while creating it. Besides, having no stack policies means all resource updates are allowed. It’s why you could update your resources on your stack updates without knowing anything about stack policies until now.
  • An empty stack policy is the opposite of having no stack policies. It denies any updates on your resources during your stack updates. By the empty stack policy, I mean only the one with an empty JSON object, {}.
  • If you have conflicting explicit Deny and Allow statements for an update type on a resource in your stack policy, the explicit Deny policy always wins. So, for the update to be allowed, not only do you need an Allow statement covering that resource, but you also need to be sure that there is no Deny policy that includes it.
  • Once you create a stack policy, you cannot remove it from your stack. You can only update it.
  • You can also provide a temporary stack policy while updating your stack, and it will be valid only for that stack update. After the update, your stack policy returns to its original version.
  • Stack policies do not prevent resource deletions if you delete your stacks. They are only valid in stack updates.

You can add a statement in your stack policy covering specific resources using logical IDs or all stack resources with a * wildcard. We also talk about these in the stack policies section of my AWS CloudFormation Step by Step: Intermediate to Advanced course in detail. I recommend joining it if you need to learn more.

Now let’s continue with the primary example of this post, using conditions in your stack policies.

Conditions in stack policies

What if you have many resources of the same type, such as RDS instances, that you never want to be replaced or deleted during a stack update? Besides, you may want to reuse the same stack policy without the need to define resources’ logical IDs each time. It is where conditions come into effect and make your life easier, and you can define conditions in your stack policies that span specific resource types.

Let’s start with a sample template and proceed step by step.

Sample template

We have two RDS instance resources in our sample template, DBInstance1, and DBInstance2. As you see, we also have a DB subnet group resource, DBSubnetGroup, and a sample SNS Topic named SampleTopic.

...
Resources:
  DBInstance1:
    Type: AWS::RDS::DBInstance
    Properties:
      ...
  DBInstance2:
    Type: AWS::RDS::DBInstance
    Properties:
      ...
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      ...
  SampleTopic:
    Type: AWS::SNS::Topic
    Properties:
      ...
...

As I discussed before, by default, stacks we created from this template will not have a stack policy, and all updates will be allowed. So, let’s start with a stack policy doing the same thing, allowing all updates.

A stack policy that allows all updates

A stack policy is a JSON object with properties similar to IAM policies. If your stack policy is an empty JSON object like below, it allows no updates.

{}

So, to allow all updates, we need to provide an Allow statement spanning all resources and update types.

{
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "Update:*",
            "Principal": "*",
            "Resource": "*"
        }
    ]
}

In a stack policy, Effect can be Allow or Deny. The only valid choice for the Principal is *, which means all. The * for the Resource property also makes this statement valid for all resources. You would recall these attributes from IAM policies.

As in IAM policies, we also have an Action attribute. Here, the Update:* value makes the statement effective for all update types. In stack policies, all actions start with Update:.

So, let’s deny any replacements or deletions on our stack resources as the next step.

Deny replacements or deletions, but allow other modifications

As I mentioned in the rules section, an explicit Deny policy overrides an explicit Allow. So, to deny only specific update types and allow others, we will keep our allow-all statement and add a new one denying them.

{
    "Statement": [
        {
            "Effect": "Deny",
            "Action": ["Update:Replace", "Update:Delete"]
            "Principal": "*",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "Update:*",
            "Principal": "*",
            "Resource": "*"
        }
    ]
}

In its first statement, our stack policy denies Update:Replace and Update:Delete actions on all resources. But in the last statement, it allows everything. So, the intersection of the two statements will be denied, and the rest will be allowed. Here, only the third update option is not included in the deny statement, Update:Modify. So, this stack policy will allow modifications without replacements. But it will deny any replacements or deletions during a stack update.

Next, let’s limit the scope of this policy to only our DB instance resources. The first option is to define their logical IDs in the Resource property one by one.

Deny replacements or deletions of specific resources

To limit a statement to only specific resources, you can include their logical resource IDs in your CloudFormation template in the Resource property of your stack policy. So, as an example, let’s make our Deny statement only valid for our DBInstance1 and DBInstance2 resources as below.

{
    "Statement": [
        {
            "Effect": "Deny",
            "Action": ["Update:Replace", "Update:Delete"]
            "Principal": "*",
            "Resource": ["LogicalResourceId/DBInstance1", "LogicalResourceId/DBInstance1"]
        },
        {
            "Effect": "Allow",
            "Action": "Update:*",
            "Principal": "*",
            "Resource": "*"
        }
    ]
}

As you see, we provided the resources as a JSON array. LogicalResourceId/ is a mandatory prefix for all resources specified this way. The DBInstance1 and DBInstance2 are exactly the same as we defined in our CloudFormation template.

Now our stack policy denies all replacements and deletions of our DB instance resources in a stack update. But what if we add a new DB instance in the future? Then, we need to add its logical resource ID to the resource property as before. Wouldn’t it be more efficient if our stack policy spanned all future DB instances? Let’s do this as the final step.

Using conditions to deny replacements or deletions of all RDS DB instances in a stack policy

You can also define a Condition attribute in your statement in addition to the standard fields. At this time, you can only use resource types in your conditions and the Condition attribute works with two operators for this, StringLike and StringEquals.

StringEquals is the one we need in our case. All we need to do is defining the resource types that the statement will be valid in its value as below. But we also return the Resource to its previous version, spanning all resources.

{
    "Statement": [
        {
            "Effect": "Deny",
            "Action": ["Update:Replace", "Update:Delete"]
            "Principal": "*",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "ResourceType": [ "AWS::RDS::DbInstance" ]
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": "Update:*",
            "Principal": "*",
            "Resource": "*"
        }
    ]
}

So, in the Condition, it compares the ResourceTypes of our stack resources with the AWS::RDS::DbInstace resource type using the equality operator, StringEquals. DbInstance1 and DbInstance2 are the only ones matching this condition. So the first statement will be valid only for them.

The second statement in our CloudFormation stack policy allows all updates on all resources. So, all update types on SampleTopic and DBSubnetGroup resources will be allowed because there is no Deny for them. But because of the first statement, our DbInstances will only be allowed Update:Modify as this action is not included.

By the way, if you noticed, the ResourceType value is a JSON array. So, you can include multiple resource types in your conditions if you need them.

Before finishing, let me give you an example of using the * wildcard in your statements with the StringLike operator.

StringLike for denying all resource types under AWS::RDS

StringLike works as a like operator in an SQL statement. But we can only work with resource types in our stack policy conditions here. If we convert the StringEquals to StringLike below, our Deny statement will also span the DBSubnetGroup resource in addition to DbInstance1 and DbInstance2 resources. It is because all these three resources’ types start with AWS::RDS::.

{
    "Statement": [
        {
            "Effect": "Deny",
            "Action": ["Update:Replace", "Update:Delete"]
            "Principal": "*",
            "Resource": "*",
            "Condition": {
                "StringLike": {
                    "ResourceType": [ "AWS::RDS::*" ]
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": "Update:*",
            "Principal": "*",
            "Resource": "*"
        }
    ]
}

Now our stack policy allows all updates on our SampleTopic resource, but only modifications without replacements on the rest.

Would you like to learn CloudFormation stack policies hands-on?

If you are looking for an online course to learn AWS CloudFormation stack policies, please check out my AWS CloudFormation Step by Step: Intermediate to Advanced course on Udemy. We make examples of setting and updating stack policies with hands-on examples. You can also learn other advanced level CloudFormation topics like nested stacks, detecting stack drifts, custom resources, etc.

But if you are new to CloudFormation, I recommend finishing my AWS CloudFormation Step by Step: Beginner to Intermediate course first. Knowledge of its topics is a prerequisite for the advanced course.

Conclusion

Setting AWS CloudFormation stack policies is an efficient way to protect your stack resources from unintended updates. You can allow only specific update actions or only to particular resources in your CloudFormation stack updates by defining stack policies. In this post, I explained using conditions in your stack policies with StringEquals and StringLike operators. We built our sample stack policy step by step.

I hope this post assists you in managing your CloudFormation stack resources.

Thanks for reading and see you in my courses on Udemy!

References

AWS CloudFormation Stack Policy Conditions: Never Replace or Delete My DB Instances on Stack Updates
Emre Yilmaz

AWS Consultant • Instructor • Founder @ Shikisoft

Follow