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 initcommand 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.jsonfile. - (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 --optionalNote: 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-optionalto 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. TheNameproperty is listed in thecreateOnlyPropertiessection, because, if changed, it will cause a new resource to be created. TheIdproperty serves as the primary identifier for the resource when it is provisioned. - Use
cfn generateto 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.tsfile, located in thesrc/folder. - Find the
createmethod within theResourceclass. It is important to create a new instance of the model, because the desired state is immutable.Because
Idis 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.tsfile located in thesrc/folder. - Find the
readmethod 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.tsfile located in thesrc/folder. - Find the
updatemethod in theResourceclass.
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.tsfile, 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.jsonfile 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
TestEntryPointis. 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.jsonfile located in thesam-tests/and replace the placeholder with theIdfor 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.jsonfile 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.jsonfile 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. ReplaceEndpointRegionwith your own values (USorEU). - Use the template to create a stack. Navigate to the folder in which you saved the
example-stack.ymlfile, 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::Websiteand a New Relic Synthetics monitor namedMyWebsiteMonitorwill 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::Websiteresource 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.