Search code examples
amazon-web-servicesaws-lambdaaws-cloudformationaws-api-gatewayintegration

How do I give API Gateway permission to invoke Lambda function in CloudFormation template?


I have an AWS CloudFormation template written in yaml that successfully creates all the resources I need and sets up the API Gateway to call the lambda file successfully except that the API Gateway doesn't have permission to call the lambda function automatically.

Currently, I have to go into the API Gateway and give the permission manually. Before I do that, I get an error with code 500, and afterwards I get a code of 200 and the result I want.

Add Permission to Lambda Function

At the bottom of my template I tried making a permission based on a few other Stack Overflow posts I saw on this topic, but I couldn't get them to work after hours of trying different answers and formats. I put the full template below.

AWSTemplateFormatVersion: 2010-09-09

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: '2012-10-17'
        Statement: 
        - Effect: 'Allow'
          Principal: 
            Service: 
              - 'lambda.amazonaws.com'
              - 'comprehend.amazonaws.com'
              - 'apigateway.amazonaws.com'
          Action:
            - 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ComprehendFullAccess'
        - 'arn:aws:iam::aws:policy/service-role/ComprehendDataAccessRolePolicy'
        - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
      RoleName: LambdaRole

  SentimentFunction:
    Type: AWS::Lambda::Function
    DependsOn:
      - LambdaRole
    Properties: 
      Code:
        ZipFile: |
          import boto3
  
          client = boto3.client('comprehend')
          
          def lambda_handler(event, context):
            result = {
              'Sentiment': client.detect_sentiment(Text = event['inputTranscript'], LanguageCode='en')['Sentiment'],
              'SentimentScore': client.detect_sentiment(Text = event['inputTranscript'], LanguageCode='en')['SentimentScore']
            }
            return result
      FunctionName: 'SentimentFunction'
      Handler: index.lambda_handler
      Role: !GetAtt LambdaRole.Arn
      Runtime: 'python3.9'

  SentimentSchedule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Schedule to trigger SentimentFunction every minute"
      ScheduleExpression: "cron(* * * * ? *)"
      Targets:
        - Arn: !GetAtt SentimentFunction.Arn
          Id: "SentimentFunction"

  SentimentAPI:
    Type: AWS::ApiGateway::RestApi
    Properties:
      EndpointConfiguration:
        Types:
        - REGIONAL
      Name: SentimentAPI

  RestApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - SentimentAPI
      - SentimentAnalysis
    Properties:
      RestApiId: !Ref SentimentAPI
      StageName: dev

  SentimentGatewayResource:
    Type: AWS::ApiGateway::Resource
    DependsOn:
      - SentimentAPI
    Properties:
      ParentId: !GetAtt SentimentAPI.RootResourceId
      PathPart: analysis
      RestApiId: !Ref SentimentAPI

  SentimentAnalysis:
    Type: AWS::ApiGateway::Method
    DependsOn:
      - SentimentGatewayResource
      - SentimentAPI
      - SentimentFunction
    Properties:
      ApiKeyRequired: false
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        IntegrationHttpMethod: GET
        IntegrationResponses:
          - StatusCode: '200'
        PassthroughBehavior: WHEN_NO_TEMPLATES
        RequestTemplates:
            application/json : '{"inputTranscript": "I want something"}'
        Type: AWS
        Uri:
          !Join
          - ''
          - - 'arn:'
            - !Ref AWS::Partition
            - ':apigateway:'
            - !Ref AWS::Region
            - :lambda:path/2015-03-31/functions/
            - !GetAtt SentimentFunction.Arn
            - /invocations
      MethodResponses:
        - ResponseModels:
            application/json : Empty
          StatusCode: '200'
      ResourceId: !Ref SentimentGatewayResource
      RestApiId: !Ref SentimentAPI

  # vvvvvvvv This doesn't work vvvvvvvv
  GWAuth:
    Type: AWS::ApiGateway::Authorizer
    Properties: 
      AuthorizerUri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SentimentFunction.Arn}/invocations"
      RestApiId: !Ref SentimentAPI
      Type: "REQUEST"
      IdentitySource: method.request.header.authorization
      Name: custom_auth

  GWAuthPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt SentimentFunction.Arn
      Principal: "apigateway.amazonaws.com"
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SentimentAPI}/authorizers/${GWAuth}"

I'm very new to AWS, so I'm sure my template is badly formed to begin with. This error has been holding me back from making more progress. Any help is very much appreciated!

Lambda call failing before manual permissions granted

Lambda call working after manual permissions granted

The stack is created completely


Solution

  • I have an API to lambda setup which uses events property in the Lambda:

    RetrieverLambda:
        Type: AWS::Serverless::Function
        Properties:
          Environment:
            Variables:
              STAGE:
                Ref: Stage
              ACCOUNT:
                Ref: AWS::AccountId
              REGION:
                Ref: AWS::Region
          CodeUri:
            Bucket: {'Fn::If': ['UseBatsKey', 'BATS::SAM::CodeS3Bucket', {"Fn::ImportValue": {Ref: 'DeploymentBucketImportName'}}]}
            Key: BATS::SAM::CodeS3Key
          Events:
            APIG:
              Properties:
                Method: ANY
                Path: /
                RestApiId: {Ref: LambdaAPIDefinition}
              Type: Api
            proxy:
              Type: Api
              Properties:
                Path: /{proxy+}
                Method: ANY
                RestApiId: {Ref: LambdaAPIDefinition}
          Handler: ...
          ..... 
    

    And the Api Gateway is defined similar to how you have done:

    LambdaAPIDefinition:
        Type: 'AWS::Serverless::Api'
        Properties:
          StageName: {Ref: Stage}
          EndpointConfiguration: REGIONAL
          Auth:
            DefaultAuthorizer: AWS_IAM
            InvokeRole: NONE     # removal of this will cause deployment failure with the error
                                 #'Caller provided credentials not allowed when resource policy is set'
            ResourcePolicy:
              CustomStatements:
                - Effect: 'Allow'
                  Action: 'execute-api:Invoke'
                  Resource: ['execute-api:/*/*/*']
                  Principal:
                    AWS:
                      - !Sub "arn:aws:iam::${AWS::AccountId}:user/ApiUser"
    

    In the above, I am using an IAM user instead of a role. But I dont think that matters here.

    I would think, adding the event trigger to your Lambda construct should fix it.

    https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html