I used this template to create a cloudformation stack
Description: App Store Env
Parameters:
SourceAccountId:
Description: source S3 bucket owner account ID
Type: String
SourceS3Bucket:
Description: source S3 bucket
Type: String
AipEnv:
Description: AIP environment
Type: String
Default: preprod
ServiceName:
Description: Service Name
Type: String
ServiceVersion:
Description: Service Version
Type: String
DescriptionTag:
Description: Service description
Type: String
Default: ""
LambdaRoleName:
Description: Lambda role name
Type: String
Default: Nucleus-Renegade-Lambda
Mappings:
EnvironmentMap:
dev:
ApplicationId: {some_value}
BillingTag: AppStore-Services
CostCenterTag:
TeamBucket: {some_value}
TeamDataBucket: {some_value}
AccountId: {some_value}
SecurityGroups:
- {some_value}
- {some_value}
Subnets:
- {some_value}
- {some_value}
preprod:
ApplicationId: {some_value}
BillingTag: AppStore-Services
CostCenterTag: {some_value}
TeamBucket: {some_value}
TeamDataBucket: {some_value}
AccountId: {some_value}
SecurityGroups:
- {some_value}
Subnets:
- {some_value}
- {some_value}
prod:
ApplicationId: {some_value}
BillingTag: AppStore-Services
CostCenterTag: {some_value}
TeamBucket: {some_value}
TeamDataBucket: {some_value}
AccountId: {some_value}
Resources:
S3SyncLambdaFunction:
Type: 'AWS::Lambda::Function'
Properties:
Handler: app.lambda_handler
MemorySize: 512
Role: !Sub "arn:aws:iam::${AWS::AccountId}:role/${LambdaRoleName}"
Code:
S3Bucket: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- TeamBucket
S3Key: !Sub >-
${ServiceName}/lambda/s3sync/${ServiceVersion}/lambda-function.zip
FunctionName: !Sub >-
aip-s3sync-${AipEnv}-${ServiceName}-${ServiceVersion}
Runtime: python3.9
Timeout: 300
Environment:
Variables:
TEAM_BUCKET: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- TeamBucket
Tags:
- Key: Billing
Value: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- BillingTag
- Key: COST_CENTER
Value: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- CostCenterTag
- Key: APP_ID
Value: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- ApplicationId
- Key: Description
Value: !Ref DescriptionTag
AdtTopic:
Type: 'AWS::SNS::Topic'
Properties:
TopicName: !Sub "aip-${AipEnv}-${ServiceName}-NewAdtSNS"
KmsMasterKeyId: alias/SNS
Tags:
- Key: Billing
Value: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- BillingTag
- Key: COST_CENTER
Value: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- CostCenterTag
- Key: APP_ID
Value: !FindInMap
- EnvironmentMap
- !Ref AipEnv
- ApplicationId
- Key: Description
Value: !Ref DescriptionTag
AdtTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
Id: AdtTopicPolicy
Version: '2012-10-17'
Statement:
- Sid: Allow-S3-id
Effect: Allow
Principal:
Service: s3.amazonaws.com
Action: sns:Publish
Resource: !Ref AdtTopic
Condition:
StringEquals:
aws:SourceAccount: !Ref SourceAccountId
ArnLike:
aws:SourceArn: !Sub "arn:aws:s3:*:*:${SourceS3Bucket}"
Topics:
- !Ref AdtTopic
InvokeLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref S3SyncLambdaFunction
Principal: sns.amazonaws.com
SourceArn: !Ref AdtTopic
However, after the stack is created, I checked lambda function trigger and found:
SNS:
A subscription for {lambda_function_arn} on the topic {sns_topic} could not be found.
Do not know which part in template is wrong. I use InvokeLambdaPermission to configure sns trigger to lambda function. Can anyone help me on this issue?
Regards,
Arthur
I think you should use this resource type in order to set sns topic to trigger your lambda:
Type: AWS::Lambda::EventSourceMapping
On your case add something like this:
LambdaFunctionEventSourceMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
EventSourceArn: !GetAtt AdtTopic.Arn
FunctionName: !GetAtt LambdaFunction.Arn
You can read more about the resource type here:
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html
Also in this article:
https://aws.amazon.com/premiumsupport/knowledge-center/lambda-subscribe-push-cloudformation/
it says:
"
The AWS::Lambda::EventSourceMapping resource is designed for pull-based event sources, such as Amazon DynamoDB event streams and Amazon Kinesis. With push-based event sources, such as Amazon S3 event notifications or Amazon SNS messages, the event source is responsible for invoking the Lambda function. For a push event source to invoke a Lambda function, the function’s resource policy must authorize a supported event source.
"
Good luck!
You have to define AWS::SNS::Subscription which is going to subscribe your lambda to a given sns topic.
This is my cloudformation template, which deploys successfully:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Deploy Da Blog
Resources:
ArticleTable:
Type: AWS::DynamoDB::Table
Properties:
KeySchema:
- AttributeName: id
KeyType: HASH
AttributeDefinitions:
- AttributeName: id
AttributeType: S
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: !Sub ${AWS::StackName}-Article
UpdateReplacePolicy: Retain
DeletionPolicy: Retain
############################### BLOG API ############################
BlogRestApiLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/apigateway/${AWS::StackName}'
BlogRestApiLoggingRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'BlogRestApiLoggingRole-${AWS::StackName}'
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: apigateway.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs
## specifies the IAM role that Amazon API Gateway uses to write API logs to Amazon CloudWatch Logs
BlogRestApiAccount:
Type: AWS::ApiGateway::Account
Properties:
CloudWatchRoleArn: !GetAtt [ BlogRestApiLoggingRole, Arn ]
BlogRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: !Sub '${AWS::StackName}'
Description: Blog API
EndpointConfiguration:
Types:
- EDGE
BlogRestApiDeployment:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref BlogRestApi
Description: Automatically created by the RestApi construct
DependsOn:
- BlogRestApi
- BlogApiResource
- BlogApiIdResource
- ListArticlesMethod
- PostArticleMethod
- GetArticleMethod
BlogRestApiStageProd:
Type: AWS::ApiGateway::Stage
DependsOn:
- BlogRestApiDeployment
Properties:
RestApiId: !Ref BlogRestApi
DeploymentId: !Ref BlogRestApiDeployment
StageName: prod
MethodSettings:
- LoggingLevel: INFO
ResourcePath: '/*'
HttpMethod: '*'
MetricsEnabled: true
BlogApiRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'BlogApiRole-${AWS::StackName}'
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: apigateway.amazonaws.com
Action:
- sts:AssumeRole
BlogApiPolicy:
Type: AWS::IAM::Policy
DependsOn:
- BlogApiRole
- ListArticlesFunction
- PostArticleFunction
- GetArticleFunction
Properties:
PolicyName: !Sub 'BlogApiPolicy-${AWS::StackName}'
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: 'Allow'
Action: 'lambda:InvokeFunction'
Resource:
- !GetAtt ListArticlesFunction.Arn
- !GetAtt PostArticleFunction.Arn
- !GetAtt GetArticleFunction.Arn
Roles:
- !Ref BlogApiRole
############################### END OF Blog API ############################
############################### START OF Blog Functions ####################
BlogApiResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt [ BlogRestApi, RootResourceId ]
PathPart: article
RestApiId: !Ref BlogRestApi
BlogApiIdResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !Ref BlogApiResource
PathPart: "{id}"
RestApiId: !Ref BlogRestApi
BlogApiOptionsMethod:
Type: AWS::ApiGateway::Method
DependsOn:
- BlogApiResource
Properties:
ApiKeyRequired: true
AuthorizationType: NONE
HttpMethod: OPTIONS
Integration:
Type: MOCK
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestTemplates:
'application/json': '{"statusCode": 200}'
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Methods: "'GET,POST,PATCH,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
ResponseTemplates:
'application/json': '{}'
MethodResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: false
method.response.header.Access-Control-Allow-Methods: false
method.response.header.Access-Control-Allow-Origin: false
ResourceId: !Ref BlogApiResource
RestApiId: !Ref BlogRestApi
ListArticlesFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-list-articles-function'
Handler: com.zenithwebfoundry.blog.api.ListArticlesHandler
CodeUri:
Key: !Ref ParamCodePackage
Bucket: !Ref ParamCodeBucket
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:Query
Resource: !GetAtt [ ArticleTable, Arn ]
Runtime: java11
Timeout: 10
MemorySize: 256
Environment:
Variables:
TABLE_NAME: !Ref ArticleTable
PRIMARY_KEY: id
DependsOn:
- ArticleTable
ListArticlesMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: GET
ResourceId: !Ref BlogApiResource
RestApiId: !Ref BlogRestApi
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ListArticlesFunction.Arn}/invocations'
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestTemplates:
'application/json': !Ref ParamAPIGatewayRequestEventMappingTemplate
IntegrationResponses:
- StatusCode: 200
ResponseTemplates:
'application/json': !Ref ParamAPIGatewayResponseEventMappingTemplate
MethodResponses:
- StatusCode: 200
OperationName: ListArticles
PostArticleFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-post-article-function'
Handler: com.zenithwebfoundry.blog.api.SaveArticleHandler
CodeUri:
Key: !Ref ParamCodePackage
Bucket: !Ref ParamCodeBucket
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource: !GetAtt [ ArticleTable, Arn ]
Runtime: java11
Timeout: 10
MemorySize: 256
Environment:
Variables:
TABLE_NAME: !Ref ArticleTable
PRIMARY_KEY: id
DependsOn:
- ArticleTable
PostArticleMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: POST
ResourceId: !Ref BlogApiResource
RestApiId: !Ref BlogRestApi
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostArticleFunction.Arn}/invocations'
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestTemplates:
'application/json': !Ref ParamAPIGatewayRequestEventMappingTemplate
IntegrationResponses:
- StatusCode: 200
ResponseTemplates:
'application/json': !Ref ParamAPIGatewayResponseEventMappingTemplate
MethodResponses:
- StatusCode: 200
OperationName: PostArticle
GetArticleFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-get-article-function'
Handler: com.zenithwebfoundry.blog.api.GetArticleHandler
CodeUri:
Key: !Ref ParamCodePackage
Bucket: !Ref ParamCodeBucket
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
Resource: !GetAtt [ ArticleTable, Arn ]
Runtime: java11
Timeout: 10
MemorySize: 256
Environment:
Variables:
TABLE_NAME: !Ref ArticleTable
PRIMARY_KEY: id
DependsOn:
- ArticleTable
GetArticleMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: GET
ResourceId: !Ref BlogApiIdResource
RestApiId: !Ref BlogRestApi
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetArticleFunction.Arn}/invocations'
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestTemplates:
'application/json': !Ref ParamAPIGatewayRequestEventMappingTemplate
IntegrationResponses:
- StatusCode: 200
ResponseTemplates:
'application/json': !Ref ParamAPIGatewayResponseEventMappingTemplate
MethodResponses:
- StatusCode: 200
OperationName: GetArticle
############################### END OF Blog Functions ######################
BlogAPIDomainName:
Type: AWS::ApiGateway::DomainName
Properties:
DomainName: !Join [ ".", ['blogapi', !Ref DomainName]]
EndpointConfiguration:
Types:
- EDGE
CertificateArn: !Ref DomainCert
SecurityPolicy: TLS_1_0
BlogAPIHostedZone:
Type: AWS::Route53::HostedZone
Properties:
Name: !Ref BlogAPIDomainName
BlogAPIBasePathMapping:
Type: AWS::ApiGateway::BasePathMapping
Properties:
DomainName: !Ref BlogAPIDomainName
RestApiId: !Ref BlogRestApi
Stage: 'prod'
Route53RecordSetGroup:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref DomainHostedZoneId
RecordSets:
- Name: !Join [ "", ['blog', '.', !Ref DomainName, '.']]
Type: A
TTL: '300'
ResourceRecords:
- 52.64.238.177
- Name: !Join [ ".", ['blogapi', !Ref DomainName]]
Type: A
AliasTarget:
# HostedZoneId: !Ref BlogAPIHostedZone # distributionHostedZoneId - alias target name does not lie in the target zone
HostedZoneId: Z2FDTNDATAQYW2
# https://j97h8bvml9.execute-api.ap-southeast-2.amazonaws.com/prod/articles
DNSName: !GetAtt [BlogAPIDomainName, DistributionDomainName]
AssetsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Join [ ".", [ !Ref 'AWS::StackName', 'assets' ] ]
CorsConfiguration:
CorsRules:
- AllowedHeaders: ['*']
AllowedMethods: [GET,PUT,POST,DELETE,HEAD]
AllowedOrigins: ['http://localhost*']
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
WebBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Join [ ".", [ !Ref 'AWS::StackName', 'web' ] ]
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
WebsiteConfiguration:
IndexDocument: 'index.html'
ErrorDocument: 'index.html'
Parameters:
ParamCodePackage:
Type: String
ParamCodeBucket:
Type: String
DomainName:
Description: "Public DNS Zone Name"
Type: String
DomainHostedZoneId:
Type: String
Description: 'The AWS HostedZoneId of the above domain name'
DomainCert:
Type: String
Description: 'The arn reference to the certificate used with the domain.'
ParamRequestMappingTemplate:
Type: String
Description: 'Read from resources/templates'
SaveArticleHandler:
Type: String
Default: 'com.zenithwebfoundry.blog.api.SaveArticleHandler'
GetArticleHandler:
Type: String
Default: 'com.zenithwebfoundry.blog.api.GetArticleHandler'
ListArticlesHandler:
Type: String
Default: 'com.zenithwebfoundry.blog.api.ListArticlesHandler'
ParamAPIGatewayRequestEventMappingTemplate:
Type: String
Default: '{
"resource" : "$context.resourceId",
"path" : "$context.path",
"httpMethod" : "$context.httpMethod",
"headers": {
#foreach($header in $input.params().header.keySet())
"$header": "$util.escapeJavaScript($input.params().header.get($header))" #if($foreach.hasNext),#end
#end
},
"method": "$context.httpMethod",
"pathParameters": {
#foreach($param in $input.params().path.keySet())
"$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end
#end
},
"queryStringParameters": {
#foreach($queryParam in $input.params().querystring.keySet())
"$queryParam": "$util.escapeJavaScript($input.params().querystring.get($queryParam))" #if($foreach.hasNext),#end
#end
},
"body" : $input.json("$"),
"isBase64Encoded": false
}'
ParamAPIGatewayResponseEventMappingTemplate:
Type: String
Default: '#set($statusCode = $input.path("$.statusCode"))
#set($context.responseOverride.status = $statusCode)
#set($headers = $input.path("$.headers"))
#foreach($key in $headers.keySet())
#set($context.responseOverride.header[$key] = $headers[$key])
#end
#set($context.responseOverride.header.Access-Control-Allow-Headers = "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token")
#set($context.responseOverride.header.Access-Control-Allow-Methods = "*")
#set($context.responseOverride.header.Access-Control-Allow-Origin = "*")
{
"body": $input.json("$.body")
}
'
Outputs:
ArticleEndpoint:
Value: !Join ["", ['https://', !Ref BlogRestApi, '.execute-api.ap-southeast-2.', !Ref 'AWS::URLSuffix', '/', !Ref BlogRestApiStageProd, '/'] ]
The problem is that when I try to call the ListArticlesFunction using the following curl expression:
curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET https://blogapi.zenithwebfoundry.com/article
I get the following:
HTTP/2 500
content-type: application/json
content-length: 36
date: Sun, 30 Aug 2020 09:09:50 GMT
x-amzn-requestid: c1948ed5-18c5-4807-8904-3c1c2af06c25
x-amzn-errortype: InternalServerErrorException
x-amz-apigw-id: SE3y1G71ywMF5fw=
x-cache: Error from cloudfront
via: 1.1 647846f53eba457a8e4ba1d1d42a6336.cloudfront.net (CloudFront)
x-amz-cf-pop: SYD1-C1
x-amz-cf-id: 6bnMhNRUdf1znTmD0vQn86UZcMF_j9JCzFb-JvhwwT9j6ch4P8t20g==
{"message": "Internal server error"}
Checking cloudwatch, I see the slightly cryptic error:
Execution failed due to configuration error: Invalid permissions on Lambda function
The same thing happens if I go into the APIGateway service console and go to the article GET resource. So at least its consistent.
I have tested the handler in isolation and there's no compilation problems anywhere, so I'm pretty sure its stack-related.
I gather that the problem must either be in the BlogApiPolicy Policy or the Lambda Policy, but nothing I set seems to work. Does anyone know what cloudformation shenanigans is needed to get this right?
The answer is that I needed the role added as a Credentials: entry in the method of each Lambda, for example:
ListArticlesMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: GET
ResourceId: !Ref BlogApiResource
RestApiId: !Ref BlogRestApi
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ListArticlesFunction.Arn}/invocations'
Credentials: !GetAtt [BlogApiRole, Arn]
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestTemplates:
'application/json': !Ref ParamAPIGatewayRequestEventMappingTemplate
IntegrationResponses:
- StatusCode: 200
ResponseTemplates:
'application/json': !Ref ParamAPIGatewayResponseEventMappingTemplate
MethodResponses:
- StatusCode: 200
OperationName: ListArticles
Why this is suddenly needed, when this was never needed before, is quite beyond me, but I must thank #Marcin for his/her patient assistance and convincing my doubting mind.
Based on the comments.
The issue was that BlogApiRole, although being created, it was not used in any of the API Gateway methods. This is required, because API Gateway needs to have explicit permissions to invoke a lambda functions. To enable this, Credentials in AWS::ApiGateway::Method Integration should be set.
The alternative is to use AWS::Lambda::Permission. The use of the AWS::Lambda::Permission is especially useful when there are many methods in the API gateway which require invoking the lambda. The reason is that you can create one such AWS::Lambda::Permission for the lambda function which can allow API gateway access to the lambda as a whole. This saves us from defining Credentials for each method independently.
I have a small web app deployed as an aws lambda that correctly operates when tested via the lambda management console. However running a test via the apig console yeilds this error:
Sun Jan 26 21:43:30 UTC 2020 : Execution failed due to configuration error: Invalid permissions on Lambda function
However the apig is provisioned with the execute permission:
apiGatewayRole:
DependsOn:
- squashApp
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- "apigateway.amazonaws.com"
Action: "sts:AssumeRole"
Path: "/"
ManagedPolicyArns:
- !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Policies:
- PolicyName: "API_Service_Role_Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action: "lambda:InvokeFunction"
Resource: !GetAtt "squashApp.Arn"
Effect: "Allow"
lambdaApiGatewayInvoke:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt "squashApp.Arn"
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/*/"
Previously the permission was sufficient (The IAM policy is recently added) to permit the lambda invocation. The error started occuring after I moved the lambda inside a VPC.
squashApp:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
GO_ENV: production
DB_HOST: !GetAtt "squashRDS.Endpoint.Address"
DB_PORT: !GetAtt "squashRDS.Endpoint.Port"
DB_USER_NAME: !Sub "${rdsUser}"
DB_USER_PASS: !Sub "${rdsPassword}"
Code:
S3Bucket: '${lambdaHandoffBucket}'
S3Key: !Sub 'functions/${lambdaVersion}/${lambdaHash}' # TODO make parameter
Handler: 'app'
Role: !GetAtt "squashLambdaRole.Arn"
Runtime: 'go1.x'
Timeout: 2
FunctionName: !Ref "lambdaFunctionName"
VpcConfig:
SecurityGroupIds:
- !Ref "lambdaSecurtiyGroup"
SubnetIds:
- !Ref "privateSubnet1"
- !Ref "privateSubnet2"
vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.192.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
privateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref vpc
CidrBlock: 10.192.21.0/24
MapPublicIpOnLaunch: false
AvailabilityZone: !Ref "PrimaryAZ"
Tags:
- Key: Name
Value: !Sub ${apiGatewayStageName} Private Subnet (AZ1)
privateSubnet2:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref vpc
CidrBlock: 10.192.30.0/24
MapPublicIpOnLaunch: false
AvailabilityZone: !Ref "SecondaryAZ"
privateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
privateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref privateRouteTable1
SubnetId: !Ref privateSubnet1
privateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
privateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref privateRouteTable1
SubnetId: !Ref privateSubnet1
noIngressSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: "no-ingress-sg"
GroupDescription: "Security group with no ingress rule"
VpcId: !Ref vpc
The full cloud formation resource section:
Resources:
apiGateway:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: "SquashAPI"
Description: "Squash Box Ladder"
apiGatewayRootMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: "NONE"
# This can be ANY | GET| POST | etc and references the api http method as seen by a client
HttpMethod: "ANY"
Integration:
# This must be post or the integration between gateway and lambda fails
IntegrationHttpMethod: "POST"
Type: "AWS_PROXY"
Uri: !Sub
- "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations"
- lambdaArn: !GetAtt "squashApp.Arn"
ResourceId: !GetAtt "apiGateway.RootResourceId"
RestApiId: !Ref "apiGateway"
apiGatewayCatchAllProxy:
Type: AWS::ApiGateway::Resource
Properties:
PathPart: "{proxy+}"
RestApiId: !Ref "apiGateway"
ParentId: !GetAtt "apiGateway.RootResourceId"
apiGatewayCatchAllMethod:
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: "NONE"
HttpMethod: "ANY"
Integration:
# This must be post or the integration between gateway and lambda fails
IntegrationHttpMethod: "POST"
Type: "AWS_PROXY"
Uri: !Sub
- "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations"
- lambdaArn: !GetAtt "squashApp.Arn"
ResourceId: !Ref "apiGatewayCatchAllProxy"
RestApiId: !Ref "apiGateway"
apiGatewayDeployment:
Type: "AWS::ApiGateway::Deployment"
DependsOn:
- "apiGatewayRootMethod"
- "apiGatewayCatchAllMethod"
Properties:
RestApiId: !Ref "apiGateway"
StageName: !Ref "apiGatewayStageName"
squashApp:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
GO_ENV: production
DB_HOST: !GetAtt "squashRDS.Endpoint.Address"
DB_PORT: !GetAtt "squashRDS.Endpoint.Port"
DB_USER_NAME: !Sub "${rdsUser}"
DB_USER_PASS: !Sub "${rdsPassword}"
Code:
S3Bucket: '${lambdaHandoffBucket}'
S3Key: !Sub 'functions/${lambdaVersion}/${lambdaHash}' # TODO make parameter
Handler: 'app'
Role: !GetAtt "squashLambdaRole.Arn"
Runtime: 'go1.x'
Timeout: 2
FunctionName: !Ref "lambdaFunctionName"
VpcConfig:
SecurityGroupIds:
- !Ref "lambdaSecurtiyGroup"
SubnetIds:
- !Ref "privateSubnet1"
- !Ref "privateSubnet2"
lambdaSecurtiyGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: For Lambda Application
VpcId: !Ref vpc
lambdaEgres:
Type: AWS::EC2::SecurityGroupEgress
Properties:
GroupId: !Ref lambdaSecurtiyGroup
IpProtocol: tcp
FromPort: !GetAtt "squashRDS.Endpoint.Port"
ToPort: !GetAtt "squashRDS.Endpoint.Port"
CidrIp: !GetAtt 'vpc.CidrBlock'
lambdaApiGatewayInvoke:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt "squashApp.Arn"
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/*/"
squashLambdaRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "sts:AssumeRole"
Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Effect: "Allow"
Resource:
- !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${lambdaFunctionName}:*"
PolicyName: "lambda"
lambdaLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/aws/lambda/${lambdaFunctionName}"
RetentionInDays: 14
apiGatewayRole:
DependsOn:
- squashApp
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- "apigateway.amazonaws.com"
Action: "sts:AssumeRole"
Path: "/"
ManagedPolicyArns:
- !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
Policies:
- PolicyName: "API_Service_Role_Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action: "lambda:InvokeFunction"
Resource: !GetAtt "squashApp.Arn"
Effect: "Allow"
apiGwAccountConfig:
Type: "AWS::ApiGateway::Account"
Properties:
CloudWatchRoleArn: !GetAtt "apiGatewayRole.Arn"
squashRDS:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: db.t2.micro
AllocatedStorage: 20
StorageType: "gp2"
Engine: "postgres"
MasterUsername: !Sub "${rdsUser}"
MasterUserPassword: !Sub "${rdsPassword}"
DBSubnetGroupName: !Ref "rdsSubnetGroup"
VPCSecurityGroups:
- !Ref rdsSecurityGroup
DeletionPolicy: Snapshot
rdsSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: For RDS Instance
VpcId: !Ref vpc
Tags:
- Key: Name
Value: RDS-SecurityGroup
rdsSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupName: "RDS Subnet Group"
DBSubnetGroupDescription: Subnet including the RDS into the VPC
SubnetIds:
- !Ref "privateSubnet1"
- !Ref "privateSubnet2"
rdsIngres:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref rdsSecurityGroup
IpProtocol: tcp
FromPort: !GetAtt "squashRDS.Endpoint.Port"
ToPort: !GetAtt "squashRDS.Endpoint.Port"
CidrIp: !GetAtt 'vpc.CidrBlock'
vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.192.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
privateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref vpc
CidrBlock: 10.192.21.0/24
MapPublicIpOnLaunch: false
AvailabilityZone: !Ref "PrimaryAZ"
privateSubnet2:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref vpc
CidrBlock: 10.192.30.0/24
MapPublicIpOnLaunch: false
AvailabilityZone: !Ref "SecondaryAZ"
privateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
privateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref privateRouteTable1
SubnetId: !Ref privateSubnet1
privateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
privateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref privateRouteTable1
SubnetId: !Ref privateSubnet1
noIngressSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: "no-ingress-sg"
GroupDescription: "Security group with no ingress rule"
VpcId: !Ref vpc
Outputs:
apiGatewayInvokeURL:
Value: !Sub "https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}"
lambdaArn:
Value: !GetAtt "squashApp.Arn"
In "squashLambdaRole", try adding "apigateway.amazonaws.com" in service principal list + attach a AWS managed policy named "AWSLambdaRole".
It solved my problem but I did not understand why :/
The only thing I can think of is, the API gateway source Arn should end with a resource path. In your case, it ends with a /.
I would add an * at the end to allow any resource path to invoke the lambda. You can change it to a specific path once you get it working.
lambdaApiGatewayInvoke:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt "squashApp.Arn"
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/*/*"
hope this helps. good luck.
I'm using cloudformation to define an api gateway that has 4 method defined: GET, POST, PUT and DELETE.
I want to use those 4 method to trigger my lambda. When this template is deployed. The lambda shows with only the DELETE method of the API Gateway.
How can I defined my lambda in cloudformation so that it will take all 4 methods?
Resources:
lambdaExecutionRole:
Type: "AWS::IAM::Role"
Properties:
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
lambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Ref AWS::StackName
VpcConfig:
SubnetIds:
- {"Fn::ImportValue": !Sub "${networkStackName}-${AWS::Region}-privateSubnetAZ1"}
- {"Fn::ImportValue": !Sub "${networkStackName}-${AWS::Region}-privateSubnetAZ2"}
SecurityGroupIds:
- {"Fn::ImportValue": !Sub "${securityStackName}-${AWS::Region}-sgDNSRestrictedAccess"}
Runtime: dotnetcore2.1
Handler: MY::LAMBDA.HANDLER::NAME
MemorySize: 128
Role: !GetAtt lambdaExecutionRole.Arn
Timeout: 30
Code:
S3Bucket: bucket-name
S3Key: bucket-key.zip
lambdaInvokePermission:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}"
Action: 'lambda:InvokeFunction'
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGatewayRestApi}/*"
DependsOn:
- lambdaFunction
apiGatewayRestApi:
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: !Ref AWS::StackName
EndpointConfiguration:
Types:
- REGIONAL
apiGatewayResourcePath:
Type: "AWS::ApiGateway::Resource"
Properties:
RestApiId: !Ref apiGatewayRestApi
ParentId: !GetAtt
- apiGatewayRestApi
- RootResourceId
PathPart: !Ref apiGatewayProxyPath
DependsOn:
- apiGatewayRestApi
apiGatewayPostMethod:
Type: "AWS::ApiGateway::Method"
Properties:
RestApiId: !Ref apiGatewayRestApi
ResourceId: !Ref apiGatewayResourcePath
HttpMethod: POST
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Join
- ":"
- - "arn"
- !Ref AWS::Partition
- "apigateway"
- !Ref AWS::Region
- "lambda:path/2015-03-31/functions/arn"
- !Ref AWS::Partition
- "lambda"
- !Ref AWS::Region
- !Ref AWS::AccountId
- "function"
- !Join
- "/"
- - !Sub "${AWS::StackName}"
- "invocations"
DependsOn:
- apiGatewayResourcePath
apiGatewayGetMethod:
Type: "AWS::ApiGateway::Method"
Properties:
RestApiId: !Ref apiGatewayRestApi
ResourceId: !Ref apiGatewayResourcePath
HttpMethod: GET
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: GET
Uri: !Join
- ":"
- - "arn"
- !Ref AWS::Partition
- "apigateway"
- !Ref AWS::Region
- "lambda:path/2015-03-31/functions/arn"
- !Ref AWS::Partition
- "lambda"
- !Ref AWS::Region
- !Ref AWS::AccountId
- "function"
- !Join
- "/"
- - !Sub "${AWS::StackName}"
- "invocations"
DependsOn:
- apiGatewayResourcePath
apiGatewayPutMethod:
Type: "AWS::ApiGateway::Method"
Properties:
RestApiId: !Ref apiGatewayRestApi
ResourceId: !Ref apiGatewayResourcePath
HttpMethod: PUT
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: PUT
Uri: !Join
- ":"
- - "arn"
- !Ref AWS::Partition
- "apigateway"
- !Ref AWS::Region
- "lambda:path/2015-03-31/functions/arn"
- !Ref AWS::Partition
- "lambda"
- !Ref AWS::Region
- !Ref AWS::AccountId
- "function"
- !Join
- "/"
- - !Sub "${AWS::StackName}"
- "invocations"
DependsOn:
- apiGatewayResourcePath
apiGatewayDeleteMethod:
Type: "AWS::ApiGateway::Method"
Properties:
RestApiId: !Ref apiGatewayRestApi
ResourceId: !Ref apiGatewayResourcePath
HttpMethod: DELETE
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: DELETE
Uri: !Join
- ":"
- - "arn"
- !Ref AWS::Partition
- "apigateway"
- !Ref AWS::Region
- "lambda:path/2015-03-31/functions/arn"
- !Ref AWS::Partition
- "lambda"
- !Ref AWS::Region
- !Ref AWS::AccountId
- "function"
- !Join
- "/"
- - !Sub "${AWS::StackName}"
- "invocations"
DependsOn:
- apiGatewayResourcePath
apiGatewayDeployment:
Type: "AWS::ApiGateway::Deployment"
Properties:
RestApiId: !Ref apiGatewayRestApi
DependsOn:
- apiGatewayPostMethod
- apiGatewayGetMethod
- apiGatewayDeleteMethod
- apiGatewayPutMethod
apiGatewayStage:
Type: "AWS::ApiGateway::Stage"
Properties:
StageName: app
RestApiId: !Ref apiGatewayRestApi
DeploymentId: !Ref apiGatewayDeployment
MethodSettings:
- ResourcePath: !Sub "/${apiGatewayProxyPath}"
HttpMethod: POST
MetricsEnabled: true
LoggingLevel: INFO
- ResourcePath: !Sub "/${apiGatewayProxyPath}"
HttpMethod: GET
MetricsEnabled: true
LoggingLevel: INFO
- ResourcePath: !Sub "/${apiGatewayProxyPath}"
HttpMethod: PUT
MetricsEnabled: true
LoggingLevel: INFO
- ResourcePath: !Sub "/${apiGatewayProxyPath}"
HttpMethod: DELETE
MetricsEnabled: true
LoggingLevel: INFO
DependsOn:
- apiGatewayDeployment
The IntegrationHttpMethod is always POST for Lambda proxy integration. See lambda proxy integration - step 5
Important
For Lambda integrations, you must use the HTTP method of POST for the integration request, according to the specification of the Lambda service action for function invocations. The IAM role of apigAwsProxyRole must have policies allowing the apigateway service to invoke Lambda functions. For more information about IAM permissions, see API Gateway Permissions Model for Invoking an API.
So your e.g. GET method integration should look like
apiGatewayGetMethod:
Type: "AWS::ApiGateway::Method"
Properties:
RestApiId: !Ref apiGatewayRestApi
ResourceId: !Ref apiGatewayResourcePath
HttpMethod: GET
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Join
- ":"
- - "arn"
- !Ref AWS::Partition
- "apigateway"
- !Ref AWS::Region
- "lambda:path/2015-03-31/functions/arn"
- !Ref AWS::Partition
- "lambda"
- !Ref AWS::Region
- !Ref AWS::AccountId
- "function"
- !Join
- "/"
- - !Sub "${AWS::StackName}"
- "invocations"
DependsOn:
- apiGatewayResourcePath
Getting "EIP will not stabilize" errors. Code is below... might be the code, or could be cloudformation bug.
I would like to attach 2 EIPs to the interface, one to the primary private IP and one to the secondary private IP. It works when I do it from the console.
I can also add 2 private IPs and a single EIP attached to either the primary or secondary private IP successfully if I comment out the 'VIP' or 'EIP' code. Eithe one works, but not both together.
#ServerOne.
ServerOne:
Type: AWS::EC2::Instance
Properties:
AvailabilityZone: !Ref AvailabilityZoneA
DisableApiTermination: !Ref disableInstanceDeletion
ImageId: !FindInMap [ RegionMap, !Ref "AWS::Region", ServerOne ]
InstanceType: !FindInMap [ InstanceSizingMap, !Ref StackSizing, ServerOne ]
EbsOptimized: !FindInMap [ InstanceSizingMap, !Ref StackSizing, ebsOptimizedInstances ]
BlockDeviceMappings:
- DeviceName: "/dev/sda1"
Ebs:
DeleteOnTermination: !FindInMap [ InstanceSizingMap, !Ref StackSizing, DeleteOnTermination ]
KeyName: !Ref SSHKeyName
Monitoring: 'false'
NetworkInterfaces:
-
NetworkInterfaceId: !Ref ServerOneInterface
DeviceIndex: 0
Tags:
- Key: Name
Value: ServerOne
- Key: Role
Value: Infrastructure
# ServerOne Server Network. 2 Private IPs, 2 EIPs all on one interface.
ServerOneEIP:
Type: AWS::EC2::EIP
DependsOn: IGW
Properties:
InstanceId: !Ref ServerOne
Domain: vpc
ServerOneVIP:
Type: AWS::EC2::EIP
DependsOn: IGW
Properties:
InstanceId: !Ref ServerOne
Domain: vpc
ServerOneEIPAssociation:
Type: AWS::EC2::EIPAssociation
DependsOn: ServerOneVIPAssociation
Properties:
AllocationId: !GetAtt ServerOneEIP.AllocationId
NetworkInterfaceId: !Ref ServerOneInterface
PrivateIpAddress: !GetAtt ServerOneInterface.PrimaryPrivateIpAddress
ServerOneVIPAssociation:
Type: AWS::EC2::EIPAssociation
DependsOn: IGW
Properties:
AllocationId: !GetAtt ServerOneVIP.AllocationId
NetworkInterfaceId: !Ref ServerOneInterface
PrivateIpAddress: !Select [ 0, !GetAtt ServerOneInterface.SecondaryPrivateIpAddresses ]
ServerOneInterface:
Type: AWS::EC2::NetworkInterface
Properties:
SubnetId: !Ref PublicSubnetA
SecondaryPrivateIpAddressCount: 1
Description: ServerOne Network Interface
GroupSet: [
!Ref PuppetClientSG ]
# ServerOne is doing NAT, so Source/Dest is false.
SourceDestCheck: false
Code is below.
---
Parameters:
Subnet:
Description: ID of the Subnet the instance should be launched in, this will
link the instance to the same VPC.
Type: List<AWS::EC2::Subnet::Id>
Resources:
EIP1:
Type: AWS::EC2::EIP
Properties:
Domain: VPC
EIP2:
Type: AWS::EC2::EIP
Properties:
Domain: VPC
Association1:
Type: AWS::EC2::EIPAssociation
DependsOn:
- ENI
- EIP1
Properties:
AllocationId:
Fn::GetAtt:
- EIP1
- AllocationId
NetworkInterfaceId:
Ref: ENI
PrivateIpAddress:
Fn::GetAtt:
- ENI
- PrimaryPrivateIpAddress
Association2:
Type: AWS::EC2::EIPAssociation
DependsOn:
- ENI
- EIP2
Properties:
AllocationId:
Fn::GetAtt:
- EIP2
- AllocationId
NetworkInterfaceId:
Ref: ENI
PrivateIpAddress:
Fn::Select:
- '0'
- Fn::GetAtt:
- ENI
- SecondaryPrivateIpAddresses
ENI:
Type: AWS::EC2::NetworkInterface
Properties:
SecondaryPrivateIpAddressCount: 1
SourceDestCheck: false
SubnetId:
Fn::Select:
- '0'
- Ref: Subnet
OpenVPN:
Type: AWS::EC2::Instance
DependsOn:
- ENI
Properties:
InstanceType: t2.micro
AvailabilityZone: us-east-2a
NetworkInterfaces:
- NetworkInterfaceId:
Ref: ENI
DeviceIndex: '0'
ImageId: ami-8a7859ef
KeyName: jimkey