Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AWS CloudFormation stack: API Gateway resource with nested paths?

I have an API Gateway resource manually built that looks like:

GET
  /assets/{items} - (points to S3 bucket)
  /{proxy+} - points to Lambda function

enter image description here

I would like to mimic this setup in a Cloudformation YAML template but unsure how to do so. Here is the current template I'm working with (partially reduced for brevity):

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  apiGatewayStageName:
    Type: String
    AllowedPattern: '^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$'
    Default: call
  lambdaFunctionName:
    Type: String
    AllowedPattern: '^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$'
    Default: my-function
  s3BucketName:
    Type: String
    AllowedPattern: '^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$'
Resources:
  apiGateway:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: my-api
      Description: My API
    Metadata:
      ...
  apiGatewayRootMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: POST
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub 
          - >-
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt lambdaFunction.Arn
      ResourceId: !GetAtt apiGateway.RootResourceId
      RestApiId: !Ref apiGateway
    Metadata:
      ...
  apiGatewayDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn:
      - apiGatewayRootMethod
      - apiGatewayGETMethod
    Properties:
      RestApiId: !Ref apiGateway
      StageName: !Ref apiGatewayStageName
    Metadata:
      ...
  lambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      ...
  lambdaApiGatewayInvoke:
    ...
  lambdaIAMRole:
    ...
  lambdaLogGroup:
    ...
  apiGatewayGETMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub 
          - >-
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt lambdaFunction.Arn
      ResourceId: !GetAtt apiGateway.RootResourceId
      RestApiId: !Ref apiGateway
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 1a329c4d-9d18-499e-b852-0e361af324f4
  s3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref s3BucketName
    Metadata:
      ...
Outputs:
  apiGatewayInvokeURL:
    Value: !Sub >-
      https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}
  lambdaArn:
    Value: !GetAtt lambdaFunction.Arn

That is the result of lots of tweaking and me not having any prior CloudFormation knowledge besides going over the official docs. When the stack behind that template gets created, its API Gateway resource looks like: enter image description here

The POST action is unnecessary, and only there from trial and error. The GET resource is the only important one as application returned by the Lambda function isn't doing any post requests yet.

The GET one must be created from this portion of the Stack:

apiGatewayGETMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub 
          - >-
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt lambdaFunction.Arn
      ResourceId: !GetAtt apiGateway.RootResourceId
      RestApiId: !Ref apiGateway

What must be done to make it so that GET resource has the nested /assets/{items} path pointing to an S3 bucket and the {proxy+} path pointing to a Lambda? Do I need to specify separate sibling resources for those paths like apiGatewayAssets and apiGatewayLambdaProxy then connect them to apiGatewayGETMethod somehow?

2020-05-17 Update

The current part tripping me up is this resource:

apiGatewayAssetsItemsResourceMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      ResourceId: !Ref apiGatewayAssetsItemsResource
      RestApiId: !Ref apiGateway
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        Type: AWS
        Credentials: arn:aws:iam::XXXXXX:role/AnExistingRole
        IntegrationHttpMethod: GET
        PassthroughBehavior: WHEN_NO_MATCH
        RequestParameters:
          integration.request.path.item: 'method.request.path.item'
          method.request.path.item: true
        Uri: !Sub >-
          arn:aws:apigateway:${AWS::Region}:s3:path/${s3BucketName}/{item}

That leads to a CloudFormation stack creation error with the status reason being Invalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression parameter specified: method.request.path.item] (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: XXXXXX)

However, if I attempt to create it with the exact same resource minus the RequestParameters entry, it gets created successfully. Although when viewing that API Gateway GET method in the console, it's missing the Paths: item line inside the Integration Request box. Full template I'm currently using:

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  apiGatewayStageName:
    Type: String
    AllowedPattern: '^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$'
    Default: call
  lambdaFunctionName:
    Type: String
    AllowedPattern: '^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$'
    Default: my-function
  s3BucketName:
    Type: String
    AllowedPattern: '^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$'
Resources:
  apiGateway:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: my-api
      Description: My API
  apiGatewayDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    DependsOn:
      - apiGatewayGETMethod
    Properties:
      RestApiId: !Ref apiGateway
      StageName: !Ref apiGatewayStageName
  lambdaFunction:
    ...
  lambdaApiGatewayInvoke:
    ...
  lambdaIAMRole:
    ...
  lambdaLogGroup:
    ...
  apiGatewayGETMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub 
          - >-
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt lambdaFunction.Arn
      ResourceId: !GetAtt apiGateway.RootResourceId
      RestApiId: !Ref apiGateway
  s3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref s3BucketName
  BucketPolicy:
    ...
  apiGatewayAssetsResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref apiGateway
      ParentId: !GetAtt 
        - apiGateway
        - RootResourceId
      PathPart: assets
  apiGatewayAssetsItemsResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref apiGateway
      PathPart: '{item}'
      ParentId: !Ref apiGatewayAssetsResource
  apiGatewayAssetsItemsResourceMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      ResourceId: !Ref apiGatewayAssetsItemsResource
      RestApiId: !Ref apiGateway
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        Type: AWS
        Credentials: arn:aws:iam::XXXXXX:role/AnExistingRole
        IntegrationHttpMethod: GET
        PassthroughBehavior: WHEN_NO_MATCH
        Uri: !Sub >-
          arn:aws:apigateway:${AWS::Region}:s3:path/${s3BucketName}/{item}
  apiGatewayLambdaResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref apiGateway
      PathPart: '{proxy+}'
      ParentId: !GetAtt 
        - apiGateway
        - RootResourceId
  apiGatewayLambdaResourceMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      AuthorizationType: NONE
      RestApiId: !Ref apiGateway
      ResourceId: !Ref apiGatewayLambdaResource
      HttpMethod: ANY
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: GET
        Uri: !Sub 
          - >-
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt lambdaFunction.Arn
Outputs:
  apiGatewayInvokeURL:
    Value: !Sub >-
      https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}
  lambdaArn:
    Value: !GetAtt lambdaFunction.Arn
like image 227
Pat Needham Avatar asked Oct 16 '22 03:10

Pat Needham


1 Answers

So what you need to do for this is the following:

  • Create a AWS::ApiGateway::Resource with a PathPart of assets, this will use a ParentId of the RootResourceId attr from your Rest API
  • Create a AWS::ApiGateway::Resource with a PathPart of {item}, this will use a ParentId of the assets Resource above.
  • Create a AWS::ApiGateway::Method for the ResourceId of the resource above. This will use the HTTP_PROXY and set the Uri to be the S3 bucket path, making sure to include the {{ item }} variable in the path.
  • Create a AWS::ApiGateway::Resource with a PathPart of {proxy+}, this will use a ParentId of the RootResourceId attr from your Rest API
  • Create a AWS::ApiGateway::Method for the ResourceId of the resource above. This will use the AWS_PROXY and set the uri to reference the Lambda function.

Hope this helps

like image 198
Chris Williams Avatar answered Oct 20 '22 14:10

Chris Williams