AWS Cloud Operations Blog
Introducing TypeScript support for building AWS CloudFormation resource types
If you’ve authored private resource types to extend the AWS CloudFormation registry, you might have used Java, Python, or Go, which, until now, were our officially supported languages.
In this blog post, we will show you how to create a private resource type using TypeScript, the latest addition to our growing list of officially supported languages. The example resource type used in this post, a New Relic monitor that checks the state of your web application is available on GitHub, so you can download the code and try it right away.
We will be using the AWS CloudFormation CLI, an open source tool that helps author private resource types by providing scaffolding code, a testing framework, and a packaging and registration tool. The CloudFormation CLI uses a language plugin system to support multiple languages. When we first released the CloudFormation CLI, we provided a plugin for Java. That was followed by support for Go and Python, which were written by internal teams. The TypeScript plugin was written by Eduardo de Moura Rodrigues, a member of the open source community. It is now officially supported by the CloudFormation team. Thank you, Eduardo!
Prerequisites
To use the TypeScript plugin with the example used in this post, you will need the following:
- An AWS Account
- Python version 3.6 or later
- AWS CLI
- AWS CloudFormation CLI version 0.23 or later
- A Git command line client
- AWS SAM CLI
- Docker
- Node.js version 12 or later
- A New Relic Account (required only to support the example used in this post)
After you create the account, follow these instructions to generate a personal API key.
Installation
- Use pip to add TypeScript support for the CloudFormation CLI.
- Run the
cfn init
command to validate that the TypeScript plugin is recognized by the CloudFormation CLI. - The wizard prompts you to select a language plugin. Choose TypeScript:
Now that you have confirmed the TypeScript plugin installation is properly installed, use CTRL+C to cancel the project initialization wizard.
Walkthrough of a sample TypeScript project
In this walkthrough, you create a resource type that provisions a New Relic ping monitor that is used to verify that a web application is online.
Create a directory and clone the repository that contains this example.
Add dependencies
Although you won’t be making AWS API calls in this example, it’s a good practice to have a development dependency on the AWS SDK for JavaScript.
- In your IDE, open the project’s
package.json
file. - (Optional) Edit the description field.
- In the development dependencies section, change the AWS SDK to your preferred semantic version.
- You are now ready to install the dependencies and generate the lock file.
npm install --optional
Note: You can reduce the package size by not including the AWS SDK, because it’s already installed in the resource provider runtime. If you choose this option, usenpm install --no-optional
to install the dependencies instead. For information about how to add dependencies, see the NPM documentation.
Resource model
Now that you’ve cloned the project, let’s take a look at the model to get familiar with the resource type.
- In your IDE, open
example-monitoring-website.json
. - This schema defines a resource,
Example::Monitoring::Website
, that provisions a monitoring solution using New Relic to ping a website and check if it’s available. The resource contains ten properties, five of which can be set by users:ApiKey
,EndpointRegion
,Name
,Uri
, andFrequency
. The other properties are read-only (meaning users can’t set them) and are assigned during resource creation. TheName
property is listed in thecreateOnlyProperties
section, because, if changed, it will cause a new resource to be created. TheId
property serves as the primary identifier for the resource when it is provisioned. - Use
cfn generate
to validate the schema and update the auto-generated files in the resource type package to reflect any change you made to the resource type schema.
Resource handlers
Now that you have defined the resource type schema, you can start writing the code for the event handlers.
In our example resource, we implement the create, update, and delete operation handlers and leave the read handler to return static data only. To simplify the development, all event handler code is in a single file, handlers.ts
, located in the src/
folder. CloudFormation events are routed using a @handlerEvent
decorator.
Implement the create handler
CloudFormation invokes this handler when the resource is initially created during stack create operations.
- In your IDE, open the
handlers.ts
file, located in thesrc/
folder. - Find the
create
method within theResource
class. It is important to create a new instance of the model, because the desired state is immutable.Because
Id
is a read-only property, it can’t be set during create or update operations.Set or fall back to the default values for each property.
Use the address from the location header to retrieve the ID of the newly created monitor and store it in your resource model.
By setting progress.status to success, you signal to CloudFormation that the operation is complete.
Implement the read handler
CloudFormation invokes this handler as part of a stack update operation when detailed information about the resource’s current state is required.
- In your IDE, open the
handlers.ts
file located in thesrc/
folder. - Find the
read
method in the Resource class. The handler code returns the static values and the resource’s unique identifier.
Implement the update handler
CloudFormation invokes this handler when the resource is updated as part of a stack update operation.
- In your IDE, open the
handlers.ts
file located in thesrc/
folder. - Find the
update
method in theResource
class.
Name
is a create only property, which means that it shouldn’t be updated. If the Name property does not match the name stored in your model, the exceptions
object throws an exception.
The exceptions object returns a ProgressEvent
to CloudFormation, with an OperationsStatus
of FAILED and a HandlerErrorCode
. In this case, an error code of NotUpdatable
is returned, which causes CloudFormation to roll back the stack and advise the user of this error condition.
After the Name
property is validated, the synthetics monitor is updated by calling the endpoint using the Id
.
For convenience, you can use the success
method to create a successful ProgressEvent
object, which signals to CloudFormation that your update operation was completed successfully.
Implement the delete handler
CloudFormation invokes this handler when the resource is deleted, either when the resource is deleted from the stack as part of a stack update operation or when the stack itself is deleted.
- In your IDE, open the
handlers.ts
file, located in thesrc/
folder. - Find the delete method within the Resource class.
The Id
property is the primary identifier, so it cannot be left empty. If left empty, our code throws an exception using the NotFound
handler error code.
If you have an Id
value in your model, our code deletes the monitor using the New Relic API.
Test the resource type
Use the AWS SAM CLI to test that your resource will work as expected after you submit it to the CloudFormation registry. To do this, define tests for AWS SAM to run against your create, update, delete, and read handlers. Testing with AWS SAM requires Docker, so be sure that it’s running on your computer.
Use npm to build your code.
npm run build
This command invokes the TypeScript compiler with the configuration specified in tsconfig.json
.
You need your New Relic account and API key for the following steps.
Test the create handler
- Open the
create.json
file located in thesam-tests/
folder and replace the placeholders with your API key and endpoint region (either US or EU): - Invoke the SAM function (from the resource package root directory):
You might be wondering what the
TestEntryPoint
is. The TypeScript plugin has two entry points: production and test. The production entry point is used when CloudFormation invokes the handler code. The test entry point has less overhead and is better suited for local testing. Because we’re running local tests, we specify theTestEntryPoint
. After the resource provisioning is complete, the test returns a response with a status ofSUCCESS
. For example:
Make a note of the Id
that is returned. You will need it to test the create, read, and delete handlers.
Test the read handler
- Open the
read.json
file located in thesam-tests/
and replace the placeholder with theId
for your resource: - Use the following command to invoke the SAM function from the resource package root directory:
After the resource data has been retrieved, the test returns a response with a status of
SUCCESS
.
Test the update handler
- Open the
update.json
file located in thesam-tests/
folder and replace the placeholders with the Id, API key, and endpoint region (either US or EU) of your resource: - Use the following command to invoke the SAM function from the resource package root directory:
After the resource has been updated, the test returns a response with a status of SUCCESS.
Test the delete handler
- Open the
delete.json
file located in thesam-tests/
folder and replace the placeholders with the Id, API key, and endpoint region (either US or EU) of your resource: - Use the following command to invoke the SAM function from the resource package root directory:
After the resource has been deleted, the test returns a response with a status of SUCCESS. For example:
About contract tests
Resource contract tests are used to validate user input before passing it to the resource handlers. These tests verify that the resource schema you’ve defined catches property values that will fail when passed to the underlying APIs called from your resource handlers. For example, in the Example::Monitoring::Website
resource type schema (in the example-monitoring-website.json
file), we specified regex patterns for the Uri
property and set the maximum length of Name
to 50 characters. Contract tests are used to stress and validate those input definitions.
Run the contract tests
Before you start the contract tests, open the overrides.json file and replace the placeholders with your API key and endpoint region (US or EU).
To run resource contract tests, you’ll need two shell sessions.
In one of those shell sessions, create a local endpoint that emulates AWS Lambda.
sam local start-lambda
From your second shell session, run cfn test to run the resource contract tests.
cfn test
The session that is running the SAM command will display information about the status of your tests. Due to the mechanism by which the API Key credentials are stored and retrieved in the read handler, it is expected that the contract_delete_read
and contract_read_without_create
tests will fail, and can be ignored.
Submit your resource type to the CloudFormation registry
To use your resource type, you must register it in the CloudFormation registry.
In a terminal, run the cfn submit
command to register the resource type in your default region and set this version as the default.
cfn submit -v --set-default
The CloudFormation CLI validates the resource type schema, packages and uploads your resource provider code, and then submits it to the CloudFormation registry. A successful invocation should look similar to the following output.
Provision the resource type in a CloudFormation template
After you’ve successfully registered the resource type, create a stack that includes resources of that type.
- Store your New Relic API Key in Secrets Manager.
- Save the following template code with the name
example-stack.yml
. ReplaceEndpointRegion
with your own values (US
orEU
). - Use the template to create a stack. Navigate to the folder in which you saved the
example-stack.yml
file, and create a stack namedexample-website-monitor
.As CloudFormation creates the stack, it will invoke your resource type create handler to provision a resource of type
Example::Monitoring::Website
and a New Relic Synthetics monitor namedMyWebsiteMonitor
will be created.
Next steps
Using the resource provider framework, you can perform the following actions:
- Interact with AWS APIs using the AWS SDK for JavaScript.
- Create handlers that have long running processes. For more information, see Progress chaining, stabilization and callback pattern in the CloudFormation Command Line Interface User Guide for Extension Development.
- Implement resource type schemas with more complex objects.
- Take API throttling into account.
Clean up resources
When you’re done experimenting with the resource type, perform these cleanup steps:
- Delete the example example-website-monitor stack.
- Delete the secret from Secrets Manager.
- Delete the execution role for the Example::Monitoring::Website resource type.
- Remove the
Example::Monitoring::Website
resource type from the AWS CloudFormation registry.
Conclusion
In this blog post, we showed you how to develop, test, and register a resource type authored in TypeScript.
The CloudFormation CLI and the CloudFormation CLI TypeScript plugin are open source projects. If you’d like to inspect the source code, raise an issue, or contribute to the project, we encourage you to visit our CloudFormation CLI or CloudFormation CLI TypeScript plugin GitHub repositories.
If you’ve read this far and would like to dive into a more complex use case using the TypeScript language plugin, we encourage you to check out the org-formation GitHub project.