Journey of creating a new AWS CloudFormation resource
Tags: aws
Published: 2023-06-09 06:56:24

## TL;DR
I created a new CloudFormation resource in the `AwsCommunity` organization with Python, created a new [Troposphere](https://github.com/cloudtools/troposphere) resource for it, and now can use it in production.

## The why

AWS CloudFormation (CFN) is my favorite Infrastructure as Code (IaC) service to use with AWS. However, some resources might not have a CloudFormation equivalent created by the respective product team, for various possible reasons.

So then, you have to either use another IaC tool (i.e. Ansible), create a CustomResource (lambda function invoked by CFN), or you could create your own AWS Resource and publish it for everyone else on AWS to use, with the [AWS CloudFormation Registry](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry.html)

I wanted the ability to create [ScheduledActions](https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-scheduled-scaling.html), resources managed by the [ApplicationAutoscaling](https://docs.aws.amazon.com/autoscaling/application/userguide/what-is-application-auto-scaling.html) service, but there wasn't a resource for that. So I created it.

# The how
The CFN team has created a set of tools and libraries in different languages to allow people to author their own resources.

I created a few myself before, and published these out of my own AWS Accounts. But this time instead of doing this all on my own, I had the opportunity to contribute to the [Open Source community driven repository](https://github.com/aws-cloudformation/community-registry-extensions) on GitHub. Great place to see what already existed, and have other people actually help and review my code.

If you are used the workflows of CloudFormation resources provisioning and the different stages it goes through, this is a very painless process. And to make the whole process even less so painful, you can (and definitely should) run contract testing locally using the `sam cli` which will validate that your code passes all the different use-cases it should.

## Publishing your resource
As mentioned, being a seasoned CloudFormation user, creating a CFN template to publish my resource and create a StackSet to publish it in all AWS regions isn't difficult, but it can be very daunting.

Joining the AWS Community driven repository will remove that concern away from you as the friendly & helpful team at AWS have automated the whole process for you, and will be publishing your resource as part of the `AwsCommunity` "Organization".

## Using the resource

Once the resource is published on AWS, you need only to activate it in your account. This will require you to create an IAM role to allow the resource to perform actions on your behalf. You can also, optionally, have logging output sent to your AWS Account, which can help with your audits and security posture.

After you have activated the resource, you can start using it in your CloudFormation templates, like any other resource. The resource will return attributes, so you can use functions like Fn::GetAtt, Fn::Ref or Fn::Sub and so on.

Here is a snippet of how the resource would be used in a CloudFormation template

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Template to test AwsCommunity::ApplicationAutoscaling::ScheduledAction

Parameters:
  ScheduledActionName:
    Type: String
    Default: cfn-testing-resource

  ServiceNamespace:
    Type: String
    AllowedValues: [
      "ecs",
      "elasticmapreduce",
      "ec2",
      "appstream",
      "dynamodb",
      "rds",
      "sagemaker",
      "custom-resource",
      "comprehend",
      "lambda",
      "cassandra",
      "kafka",
      "elasticache",
      "neptune"
    ]

  ScalableDimension:
    Type: String
    AllowedValues: [
      "ecs:service:DesiredCount",
      "ec2:spot-fleet-request:TargetCapacity",
      "elasticmapreduce:instancegroup:InstanceCount",
      "appstream:fleet:DesiredCapacity",
      "dynamodb:table:ReadCapacityUnits",
      "dynamodb:table:WriteCapacityUnits",
      "dynamodb:index:ReadCapacityUnits",
      "dynamodb:index:WriteCapacityUnits",
      "rds:cluster:ReadReplicaCount",
      "sagemaker:variant:DesiredInstanceCount",
      "custom-resource:ResourceType:Property",
      "comprehend:document-classifier-endpoint:DesiredInferenceUnits",
      "comprehend:entity-recognizer-endpoint:DesiredInferenceUnits",
      "lambda:function:ProvisionedConcurrency",
      "cassandra:table:ReadCapacityUnits",
      "cassandra:table:WriteCapacityUnits",
      "kafka:broker-storage:VolumeSize",
      "elasticache:replication-group:NodeGroups",
      "elasticache:replication-group:Replicas",
      "neptune:cluster:ReadReplicaCount"
    ]
  MinCapacity:
    Type: Number
    MinValue: 0

  MaxCapacity:
    Type: Number
    MinValue: 0

  ScheduleExpression:
    Type: String
    Description: the cron(), rate() or at() expression.

  EndTime:
    Type: String
    Default: none
    Description: When using cron() or rate(), timestamp of when the rule expires.

  StartTime:
    Type: String
    Default: none
    Description: When using cron() or rate(), timestamp of when the rule starts.

  TimeZone:
    Type: String
    Default: "UTC"

  ResourceId:
    Type: String
    Description: The Scalable Target ID.

Conditions:
  NoEndTime:
    !Equals [ !Ref EndTime, "none" ]
  NoStartTime:
    !Equals [ !Ref StartTime, "none" ]

Resources:
  ScheduledActionForScalableTarget:
    Type: AwsCommunity::ApplicationAutoscaling::ScheduledAction
    Properties:
      ScheduledActionName: !Ref ScheduledActionName
      ServiceNamespace: !Ref ServiceNamespace
      ScalableDimension: !Ref ScalableDimension
      Schedule: !Ref ScheduleExpression
      ScalableTargetAction:
        MinCapacity: !Ref MinCapacity
        MaxCapacity: !Ref MaxCapacity
      StartTime: !If
        - NoStartTime
        - !Ref AWS::NoValue
        - !Ref StartTime
      EndTime: !If
        - NoEndTime
        - !Ref AWS::NoValue
        - !Ref StartTime
      Timezone: !Ref TimeZone
      ResourceId: !Ref ResourceId

```


## Creating a troposphere resource and integration to ECS Compose-X

If you have read my earlier posts or heard me talk with [Corey](https://twitter.com/QuinnyPig) on [Screaming in the Cloud](https://www.lastweekinaws.com/podcast/screaming-in-the-cloud/clouddev-for-retail-companies-with-john-mille/), I am the author of a tool called [ECS Compose-X](https://github.com/compose-x/ecs_composex). It's designed to support all the docker compose features, and allow for extensions that make it easier to deploy to AWS.

Because ECS Compose-X uses [Troposphere](https://github.com/cloudtools/troposphere), I was able to create a very light and simple python library(https://github.com/JohnPreston/troposphere-awscommunity-applicationautoscaling-scheduledaction) to distribute the resource for other Troposphere users to re-use.

Naturally, as autoscaling was already implemented in the project, this was a natural addition to the [x-scaling](https://docs.compose-x.io/syntax/compose_x/ecs.details/scaling.html) services feature. This particular resource allows to plan for scaling your ECS service when you need to, and scale back down accordingly.

## Conclusion
Creating AWS Resource for CloudFormation might seem like something like you shouldn't have to do, but I see it as a great way to contribute back to the community and to a service I love. And doing so, I hope, will help people who might trying to achieve the same thing.

But it is not limited to AWS resources: MongoDB, NewRelic, DataDog and other AWS partners have taken advantage of this capability to publish their own resources, allowing AWS customers to rely on AWS CloudFormation to provision their resources.

I am not a CDK user, but one could also now create a CDK extension to provision that resource in their code.

Big shoutout to the AWS CloudFormation team (@ericzbeard, @kddejong) & contributors for having helped me out and look forward to seeing more exciting resources and projects coming up!