Thursday, December 27, 2018

Mastering CloudFormation for API Gateway Deployments

I recently spent a fair amount of time trying to write a CloudFormation template for API Gateway to do precisely what I wanted to do. I struggled to make this happen, and failed to find good community resources to supplement the official documentation. One forum thread seemed to describe exactly what I needed, but the conclusion in the thread seemed to indicate that it wasn’t possible. Nonetheless, it was helpful in understanding some of the problems I was facing. Here is what I wanted CloudFormation to do with API Gateway:
  • Create an API instance 
  • Configure a stage 
  • Deploy API changes from swagger 
Through trial and error and discovering basic concepts in CloudFormation, I was able to get exactly what I wanted. I hope this post helps others write a perfect template for their needs.

Problems I Ran Into

As I attempted different appraoches, I experienced a few common problems:
  • Deployment of revised swagger would not occur 
  • Updating the stack with revised swagger would error with “stage already exists” 
  • No deployment history

CloudFormation Basics

Being new to CloudFormation, I didn’t fully grok some key CloudFormation concepts which was a barrier to getting my template right. The primary concept is that CloudFormation templates dictate desired state, not a set of operations to perform. If a resource is defined in your template, it will be created. If a resource already exists, it will not be created, but can be updated if its properties change. If a resource is removed from the template it will be deleted.

API Gateway Basics

  • Swagger is used to define a REST API. 
  • That REST API is deployed. 
  • A stage references a deployment to make the API available via an endpoint.

The Template

---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Setup our API Gateway instances

Parameters:
  StageName:
    Type: String
    Default: 'example_stage'
    Description: 'The name of the stage to be created and managed within our API Gateway instance.'
    
Resources:

  Api:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: ExampleApi
      EndpointConfiguration: 
        Types: 
        - REGIONAL

      # The body should contain the actual swagger
      Body: $SWAGGER_DEFINITION$

  # Timestamp is added so that each deployment is unique. Without a new timestamp, the deployment will not actually occur
  ApiDeployment$TIMESTAMP$:
    Type: AWS::ApiGateway::Deployment
    DependsOn: [ Api ]
    # we want to retain our deployment history
    DeletionPolicy: Retain    
    Properties:
      RestApiId:
        Ref: Api
      
  ApiStage:
    Type: AWS::ApiGateway::Stage
    DependsOn: [ApiDeployment$TIMESTAMP$]
    Properties:
      RestApiId:
        Ref: Api
      DeploymentId:
        Ref: ApiDeployment$TIMESTAMP$
      StageName: {Ref: StageName}
      MethodSettings:
        - ResourcePath: "/*"
          HttpMethod: "*"
          LoggingLevel: INFO
          MetricsEnabled: true
          DataTraceEnabled: true
          
Outputs:
  Endpoint:
    Description: Endpoint url
    Value:
      Fn::Sub: 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com'          


Key Points

The deployment resource can specify a StageName and StageDescription. If this is done the deployment will implicitly create a stage that is not managed by CloudFormation. If we attempt to create a stage with the same name we’ll get an error that it already exists. Instead we’re better off creating a deployment that doesn’t prescribe anything about the stage and explicitly defining a stage resource.

Each deployment needs a unique id. Otherwise, CloudFormation will determine the deployment already exists and will not create a deployment. Without a new deployment, changes to our REST API will not be visible. Furthermore, we set a deletion policy on our deployment to retain the deployment. Otherwise, when the timestamp on our deployment changes, CloudFormation will want to delete the old deployment. If this deletion occurs, the deployment is removed from our stage deployment history. With this policy, the deployment is removed from the stack but not deleted from API Gateway.

EDIT:
A script must be written to preprocess the the template and replace the $TIMESTAMP$ token with the timestamp WITHOUT any separators e.g. 05012019355 Thanks to somebody for pointing this out in the comments.


Disclaimer: I wrote this post as an enthusiast. The content is not the official position of Amazon or AWS.

12 comments:

  1. This is a very good write up, can I suggest that you add a summary of it + link to https://stackoverflow.com/questions/41423439/cloudformation-doesnt-deploy-to-api-gateway-stages-on-update ?

    ReplyDelete
  2. This does not work :

    Resource name ApiDeployment$TIMESTAMP$ is non alphanumeric.

    ReplyDelete
  3. All good, but where from do you have that $TIMESTAMP$ value?

    ReplyDelete
  4. I'm guessing they have something else inserting the timestamp at the time the template is generated. This does not work for me either as explicitly written out, but if you can manually update your template's stage/deployment/base path mapping resources and insert your own unique timestamp, it will properly replace the deployment resource with the new one. Here's my functional example:

    rDeployment05012019355:
    Type: AWS::ApiGateway::Deployment
    DependsOn: rApiGetMethod
    Properties:
    RestApiId:
    Fn::ImportValue:
    !Sub '${pApiCoreStackName}-RestApi'
    StageName: !Ref pStageName

    rCustomDomainPath:
    Type: AWS::ApiGateway::BasePathMapping
    DependsOn: [rDeployment05012019355] # rDeployment
    Properties:
    BasePath: !Ref pPathPart
    Stage: !Ref pStageName
    DomainName:
    Fn::ImportValue:
    !Sub '${pApiCoreStackName}-CustomDomainName'
    RestApiId:
    Fn::ImportValue:
    !Sub '${pApiCoreStackName}-RestApi'

    ReplyDelete
    Replies
    1. I don't understand how did you this seems sort of id '05012019355' into the resource name, are you hard-coding it?

      Delete
    2. There is a script that preprocesses the template. This could be a shell script python, anything that can replace the $TIMESTAMP$ token with a timestamp without separators. Separators are undesirable as CF will tolerate them in a resource name.

      Delete
  5. You are correct, we do have a script inserting the timestamp. Also, I apparently did not have notifications of comments set up.

    ReplyDelete
    Replies
    1. how do I do that? do you have any example?
      Thannks

      Delete
    2. Hi, as asked above. Do you have any example of appending the timestamp to logical ID of api gateway deployment resource name?

      Delete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Great article! Here is a gist I wrote to accomplish this. It should be easy to adapt this to your purposes.
    https://gist.github.com/officialhopsof/faafdde667e10cc4f9de24df92c30b0b

    ReplyDelete