Rafał Wrzeszcz - Wrzasq.pl

Multiple Amazon API Gateway stages

Sunday, 03 December 2017, 23:05

Short time ago I described CI/CD pipeline design, that allows for handling multiple environment deployments replicated from same CloudFormation template. Process is usually quite easy if you have some EC2-based deployment, per-environment ECS cluster or Lambda functions that you can deploy freely however you like. Things get complicated when you integrate API Gateway. The easiest way would be to deploy separate APIs for each of your environments. But it seems so wrong, isn't it? API Gateway has a feature of stages, that seems to be perfect for such cases.

It's all feasible with stage variables - you can use them as placeholders for method integrations and even authorizer URIs. So in theory it's quite easy: define one API gateway shared for all of the environments and define an API stage in each environment with stage variables pointing proper target URIs. And it is, as long as you know about few details. Let's takes this CloudFormation template:

Resources:
    ApiGatewayV1:
        Type: "AWS::ApiGateway::RestApi"
        Properties:
            Name: !Sub "my-project:v1"

    ApiAuthorizerV1:
        Type: "AWS::ApiGateway::Authorizer"
        Properties:
            RestApiId: !Ref "ApiGatewayV1"
            Name: "my-custom"
            Type: "TOKEN"
            # (1) - authorizer Lambda URI
            AuthorizerUri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${!stageVariables.AuthorizerLambdaName}/invocations"
            IdentitySource: "method.request.header.Authorization"

    ApiGatewayRole:
        Type: "AWS::IAM::Role"
        Properties:
            RoleName: !Sub "my-project-apigateway"
            AssumeRolePolicyDocument:
                Statement:
                    -
                        Action:
                            - "sts:AssumeRole"
                        Effect: "Allow"
                        Principal:
                            Service:
                                - "apigateway.amazonaws.com"

    ApiResource:
        Type: "AWS::ApiGateway::Resource"
        Properties:
            RestApiId: !Ref "ApiGatewayV1"
            ParentId: !GetAtt "ApiGatewayV1.RootResourceId"
            PathPart: "content"

    ApiMethodGet:
        Type: "AWS::ApiGateway::Method"
        Properties:
            RestApiId: !Ref "ApiGatewayV1"
            ResourceId: !Ref "ApiResource"
            HttpMethod: "GET"
            AuthorizationType: "CUSTOM"
            AuthorizerId: !Ref "ApiAuthorizerV1"
            RequestParameters:
                method.request.header.Authorization: true
            MethodResponses:
                -
                    StatusCode: 200
            Integration:
                Type: "AWS"
                IntegrationHttpMethod: "POST"
                # (2) - backend Lambda URI
                Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${!stageVariables.GetContentLambdaArn}"
                Credentials: !GetAtt "ApiGatewayRole.Arn"
                IntegrationResponses:
                    -
                        StatusCode: 200

We have here two stage variables - one for authorizer Lambda and one for backend integration Lambda.

Authorizer Lambda URI

As you can see, authorizer Lambda variable replaces just a function name. Nothing else. This is the only possible way. You can't put full ARN there - it won't work. You can't include /invocations part in the variable (actually that would be not clever at all, but read further), as it won't be recognized.

But that's not the end - you need to use ARN with a specified region, otherwise… authorizer creation will fail with a fail Internal failure. (good luck debugging).

So, for authorizer we just store name/alias of the function.

Backend Lambda URI

All of the above is not quite true for backend integrations (I just write about Lambdas, didn't try other backend types).

First of all the variable can only be used at the end of URI, which means it must contain /invocations part in it.

But it's still not the end - when using CloudFormation, you must put entire Lambda ARN into variable, otherwise the URI get's too long (for CloudFormation? no idea).

Tags: CloudFormation, Lambda, AWS, Cloud, API Gateway