Deploying services with AWS CloudFormation
In our previous post, we implemented the necessary resources on AWS to build a serverless backend for our contact form. We carried out the entire process of deploying services manually from the AWS web console. Now, to continue following best practices, we will reproduce the service deployment using CloudFormation to deploy infrastructure as code.
Infrastructure as Code
Infrastructure as Code (IaC) refers to the practice of defining and deploying IT infrastructure through machine-readable configuration files instead of manual processes. With IaC, infrastructure can be treated like code, meaning it can be versioned, tested, and deployed just like software.
One of the main advantages of using IaC is that it enables teams to deploy and manage infrastructure more efficiently and consistently. Because IaC uses version control systems, teams can track and manage changes to infrastructure over time. It also allows for automated testing and deployment, which reduces the risk of human error and increases the speed and reliability of deployments. Additionally, IaC promotes infrastructure transparency, as it provides a clear view of what infrastructure is in use and how it is configured.
AWS CloudFormation
AWS CloudFormation is a service provided by AWS that allows users to define their infrastructure as code, using templates written in YAML or JSON. The templates contain a description of the resources to be created, such as EC2 instances, load balancers, and databases, as well as any necessary configurations and dependencies between them.
When a CloudFormation stack is created, updated, or deleted, CloudFormation reads the template and uses it to provision and configure the resources accordingly. CloudFormation handles all the necessary actions, such as creating, updating, or deleting resources, in the correct order and ensures that they are properly configured and connected. CloudFormation provides features such as rollback protection, strongresource tagging and drift detection, which further simplify the management of infrastructure resources.
JSON or YAML
When defining a CloudFormation template, both JSON and YAML can be used. However, in my opinion, it's better to use YAML since it's easier to read and write than JSON. YAML uses fewer characters and doesn't use braces and quotes, making it visually less noisy. It also has a more flexible and less strict structure than JSON, making it easier to write and more intuitive to understand. Additionally, YAML is compatible with comments, making it easier to document and maintain. For these reasons, YAML will be used to define my CloudFormation template.
MyStacks
Among the services that I have deployed, some are general for MyWebsite, while others are specific to the ContactForm service. When defining infrastructure as code, it is important to structure and separate our infrastructure stacks properly so that they can interact with each other, and we can define infrastructure separately for different projects.
In this case, we have the MyWebsite project and an additional service of ContactForm. MyWebsite may have other services independent of ContactForm in the future, which is why we will define an independent Stack for MyWebsite's resources that will be shared by all subsequently deployed services. We will create a second Stack that receives the ApiId provided by the first Stack as an input so that we can reference it from our second Stack.
MyWebsite Stack
In this stack, all the necessary infrastructure for MyWebsite will be deployed. Currently, it will only consist of the API Gateway service, but in the future, this stack will be used to deploy more services such as the infrastructure needed for hosting.
The first step is to define the HTTP API. The AWS::ApiGatewayV2::Api resource type will be used to define the necessary configurations such as CORS or protocol. Additionally, the default input URL to the API will be disabled since a custom domain will be used.
Api:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: jaimeelso
Description: API that manages all the backend calls from MyWebsite
ProtocolType: HTTP
RouteSelectionExpression: $request.method $request.path
DisableExecuteApiEndpoint: true
CorsConfiguration:
AllowCredentials: false
AllowHeaders:
- content-type
AllowMethods:
- POST
- OPTIONS
AllowOrigins:
- https://jaimeelso.com
MaxAge: 0
Now that the API Gateway has been set up, a Stage needs to be created. AWS::ApiGatewayV2::Stage will be used to create the default stage and configure it for automatic deployment. In addition, request handling will be limited to only 1 request per second and 1 concurrent request. This is sufficient for the current traffic on the website.
ApiStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref Api
StageName: default
Description: Default stage
AutoDeploy: true
DefaultRouteSettings:
DetailedMetricsEnabled: false
ThrottlingBurstLimit: 1
ThrottlingRateLimit: 1.0
Once the primary stack is ready, the API ID value needs to be exported. This is necessary because the ContactForm stack will need to define services that require references to this value for certain configurations. The Outputs section of our stack will be used to export the ID. These variables that are exported must be unique in each region, so it is good practice to use NameStack-VariableName as the variable name. The AWS::StackName pseudoparameter will be used to obtain the stack name within our template.
Outputs:
ApiId:
Value: !Ref Api
Description: The ID of the AWS API Gateway that manages the backend on the web jaimeelso.com.
Export:
Name: !Sub "${AWS::StackName}-ApiId"
ContactForm Stack
To make the stack as flexible as possible and adapt to future deployments, the Parameters section is used to include input parameters in our stack that can be used by our template. We will include the email address to subscribe to our SNS topic, the private key of the reCaptcha API, the S3 Bucket, the key of the object where our lambda function code is located, and finally, we will indicate the name of the primary stack in which we have defined the API Gateway as parameters.
Parameters:
SubscriptionEndpoint:
Type: String
Description: The email address to which contact form messages will be sent
RecaptchaSecretKey:
Type: String
Description: The secret key to call de reCaptcha API from the backend
LambdaS3Bucket:
Type: String
Description: Bucket name where Lambda code is place
LambdaS3Key:
Type: String
Description: Bucket key where Lambda code is place
ApiStackName:
Type: String
Description: API Stack name
From here, these parameters can be used to define resources in this Stack. In this Stack, the SNS topic and subscription, lambda function, SecretsManager secrets, and the path and implementation for the API Gateway that redirects requests to the lambda function will be defined. Additionally, the execution role of lambda and the associated permission policy will be defined.
Since the template contains a lot of code, the complete template file is available on Github for interested readers.
Conclusion
By defining my infrastructure as code in CloudFormation templates, I have greater control over the infrastructure deployed in AWS, making maintenance and future updates simpler. It also allows for easy migration of my environment to another AWS region or account if needed in the future.
Currently, my templates are stored in a private S3 bucket and deployments are done manually from the CloudFormation console. The implementation of a pipeline to automate the deployment and updates of these stacks will be done later. I will write a dedicated post on implementing CI/CD for all AWS services that require automated deployments.
Thanks for reading! Catch you in the next one.