CallCenterStateMachine:
Type: AWS::Serverless::StateMachine # More info about State Machine Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html
Properties:
DefinitionUri: statemachines/CallCenterStateMachine.asl.json
Events:
APIEvent:
Type: Api
Properties:
Path: /case
Method: post
RestApiId: !Ref CallCenterAPI
Now notice the RestApiId property under the event. This is provided to add more configuration to the API Gateway then is possible by just using the Event property.
== API Gateway ==
As explained before, an API Gateway will be automatically created if you us the event above. Actually, not only the API, but also stages, methods and the actual deployment. If a typical deployment is all you need, this will probably be enough. And using the RestApiId parameter you can even do little tweaks like naming the stage:
CallCenterAPI:
Type: AWS::Serverless::Api
Properties:
StageName: prod
If you would deploy this however, you'd still have a problem. [[https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html|CORS]]. Since the S3 website, and the API Gateway are in different domains you need to enable CORS to have them talk to each other. Simple enough, in the API Gateway, select the resource, click enable CORS, enable all Options and Save:
✔ Add Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin Method Response Headers to OPTIONS method
✔ Add Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin Integration Response Header Mappings to OPTIONS method
✔ Add Access-Control-Allow-Origin Method Response Header to POST method
✔ Add Access-Control-Allow-Origin Integration Response Header Mapping to POST method
Your resource has been configured for CORS. If you see any errors in the resulting output above please check the error message and if necessary attempt to execute the failed step manually via the Method Editor.
And now you need to deploy the gateway afterwards. It's easy, but enabling CORS could save you quite some work. This is however a little bit more complicated. The first part is easy, we'll add CORS properties and default gateway responses to the API Gateway resource properties:
CallCenterAPI:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowMethods: "'POST, GET, OPTIONS, HEAD'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
GatewayResponses:
DEFAULT_4xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DEFAULT_5xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
Now this is sadly not enough. We'll also have to define the methods. For this we will use an existing and working API Gateway.
=== Export API Configuration ===
We will use Swagger to configure the rest of the API Gateway for CORS. With swagger we can define the CORS configuration inside of the DefinitionBody property of the API Gateway definition. It's most easy to pick an existing API gateway that is configured as you want it and then export the Swagger definition:
* Inside the AWS API Gateway go to the API you want to export
* Go to Stages and select the prod stage
* Go to the export tab
* In this case we already deploy an API with integration and some more basic settings setup, but we still need the "Export as Swagger + API Gateway Extensions" option and make sure to select the YAML option, as we do everything in YAML. A download starts, and the code is displayed on the page for easy copy/paste as well
Now we can change the output to fit our explicit needs and add the final piece to our deployment.
CallCenterAPI:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowMethods: "'POST, GET, OPTIONS, HEAD'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
GatewayResponses:
DEFAULT_4xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DEFAULT_5xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DefinitionBody:
swagger: "2.0"
info:
title: "sam-callcentercors" #This is the name of the API
paths:
/case:
post:
consumes:
- "application/json"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
"400":
description: "400 response"
x-amazon-apigateway-integration:
credentials: !GetAtt CallCenterStateMachineAPIEventRole.Arn
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:states:action/StartExecution"
responses:
"200":
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
"400":
statusCode: "400"
requestTemplates:
application/json:
Fn::Sub: "{\"input\": \"$util.escapeJavaScript($input.json('$'))\"\
, \"stateMachineArn\": \"${CallCenterStateMachine.Arn}\"\
}"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
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-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
type: "mock"
> Note that some parts are modified to fit my environment and preferences.
== S3 ==
Now that we have the API Gateway, the State Machine and the Lambda Functions, all we need is the S3 bucket to host a website on (and the actual website, hang on). The S3 bucket needs to be configured as a static website and it needs public access. Now S3 is a service that is nog part of the Serverless stack, but only of CloudFormation itself. Luckily, we can also add regular CloudFormation resources:
CallCenterWebAppBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
CallCenterWebAppBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: MyPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref CallCenterWebAppBucket
- /*
Bucket: !Ref CallCenterWebAppBucket
== Outputs ==
Now there are a few things that might come in hand when deploying a template like this. For example, in the frontend we'll need the API Gateway invoke url, so we know the address to talk too. Also, the url of the S3 bucket might be nice so we can go there directly once we've uploaded the website file and we can test. To use this we can use outputs:
WebsiteURL:
Description: URL for website hosted on S3
Value: !GetAtt
- CallCenterWebAppBucket
- WebsiteURL
APIInvokeURL:
Description: "API Prod stage endpoint"
Value: !Sub "https://${CallCenterAPI}.execute-api.${AWS::Region}.amazonaws.com/prod/case"
StateMachineIamRole:
Description: "Implicit IAM Role created for State Machine"
Value: !GetAtt CallCenterStateMachineRole.Arn
StateMachineAPIEventIamRole:
Description: "Implicit IAM Role created for State Machine API Event"
Value: !GetAtt CallCenterStateMachineAPIEventRole.Arn
> Notice that I also output the ARNs of some of the implicitly created IAM roles, which I needed to test with to fill in the correct credentials role in the API definition file.
== End Template ==
So now that we have all the parts let's combine this together into one masterpiece:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
SAM-CallCenterWebApp
Deploy State Machine, Lambda Functions, API Gateway and S3 bucket.
Resources:
CallCenterStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachines/CallCenterStateMachine.asl.json
DefinitionSubstitutions:
OpenCaseFunctionArn: !GetAtt OpenCaseFunction.Arn
AssignCaseFunctionArn: !GetAtt AssignCaseFunction.Arn
WorkOnCaseFunctionArn: !GetAtt WorkOnCaseFunction.Arn
CloseCaseFunctionArn: !GetAtt CloseCaseFunction.Arn
EscalateCaseFunctionArn: !GetAtt EscalateCaseFunction.Arn
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref OpenCaseFunction
- LambdaInvokePolicy:
FunctionName: !Ref AssignCaseFunction
- LambdaInvokePolicy:
FunctionName: !Ref WorkOnCaseFunction
- LambdaInvokePolicy:
FunctionName: !Ref CloseCaseFunction
- LambdaInvokePolicy:
FunctionName: !Ref EscalateCaseFunction
Events:
APIEvent:
Type: Api
Properties:
Path: /case
Method: post
RestApiId: !Ref CallCenterAPI
CallCenterWebAppBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
CallCenterWebAppBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: MyPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref CallCenterWebAppBucket
- /*
Bucket: !Ref CallCenterWebAppBucket
OpenCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/open-case/
Handler: app.handler
Runtime: nodejs12.x
Role: arn:aws:iam::952941930635:role/service-role/OpenCaseFunction-role-53xmbdiv
AssignCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/assign-case/
Handler: app.handler
Runtime: nodejs12.x
Role: arn:aws:iam::952941930635:role/service-role/OpenCaseFunction-role-53xmbdiv
WorkOnCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/work-on-case/
Handler: app.handler
Runtime: nodejs12.x
Role: arn:aws:iam::952941930635:role/service-role/OpenCaseFunction-role-53xmbdiv
CloseCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/close-case/
Handler: app.handler
Runtime: nodejs12.x
Role: arn:aws:iam::952941930635:role/service-role/OpenCaseFunction-role-53xmbdiv
EscalateCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/escalate-case/
Handler: app.handler
Runtime: nodejs12.x
Role: arn:aws:iam::952941930635:role/service-role/OpenCaseFunction-role-53xmbdiv
# API
CallCenterAPI:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowMethods: "'POST, GET, OPTIONS, HEAD'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
GatewayResponses:
DEFAULT_4xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DEFAULT_5xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DefinitionBody:
swagger: "2.0"
info:
title: "sam-callcenterwebapp" #This is the name of the API
paths:
/case:
post:
consumes:
- "application/json"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
"400":
description: "400 response"
x-amazon-apigateway-integration:
credentials: !GetAtt CallCenterStateMachineAPIEventRole.Arn
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:states:action/StartExecution"
responses:
"200":
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
"400":
statusCode: "400"
requestTemplates:
application/json:
Fn::Sub: "{\"input\": \"$util.escapeJavaScript($input.json('$'))\"\
, \"stateMachineArn\": \"${CallCenterStateMachine.Arn}\"\
}"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
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-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
type: "mock"
Outputs:
# In AWS Toolkit for VS Code the output is not displayed. Check the output in the AWS Cloudformation Console
WebsiteURL:
Description: URL for website hosted on S3
Value: !GetAtt
- CallCenterWebAppBucket
- WebsiteURL
APIInvokeURL:
Description: "API Prod stage endpoint"
Value: !Sub "https://${CallCenterAPI}.execute-api.${AWS::Region}.amazonaws.com/prod/case"
StateMachineIamRole:
Description: "Implicit IAM Role created for State Machine"
Value: !GetAtt CallCenterStateMachineRole.Arn
StateMachineAPIEventIamRole:
Description: "Implicit IAM Role created for State Machine API Event"
Value: !GetAtt CallCenterStateMachineAPIEventRole.Arn
== Front End Website ==
Now we need the website, I created a small one pager with all CSS and javascript on the same page. Notice that it needs the API Invoke URL inside the javascript, so once've deployed check the output section in the AWS CloudFormation console:
Call Center Web App
Please enter your ticket number
To check the API callback, press F12 and check the console log.
> Don't forget to update the url value inside of the submitToAPI function.
== Deploy ==
Just as a summary, follow these steps to make this working:
Open the command palette and search for AWS and select AWS: Deploy Serverless Application
* Select the just created template.yaml from the list
* Select the region to deploy to: Europe (Ireland) - eu-west-1
* Provide the name of the S3 bucket we created earlier: vscode-awstoolkitsam
* Provide the name of the (CloudFormation) stack. Notice that all lambda resources will be created with this name as a prefix (not the step function), so keep the name short and simple: sam-callcenterwebapp
* Now go in the AWS Console and go to Cloudformation. Click on the CloudFormation stack you just deployed and copy the APIInvokeURL value and update the url value in the javascript function as explained above
* Now go to the AWS S3 console and click on the S3 bucket you just created. Upload the html file we've created before and make sure to name it index.html
* Now go back to the AWS Cloudformation console and again in the output section you can click on the link for WebsiteURL. This will take you directly to the website so you can test your webapp.
= Additional Configuration =
== IAM Roles ==
Now, in the file above quite a few IAM roles are created or used. For Lambda I used one of the roles that was created in a previous attempt, and for the State Machine and the API Event of the State Machine new roles are created. Now throw in that in the next section I also want logging I will end up with somewhere about 8 roles (assuming that Lambda also does not uses a predefined role). I decided I wanted a bit more control.
=== Lambda ===
In the [[https://docs.aws.amazon.com/lambda/latest/dg/lambda-permissions.html|Lambda documentation]] is explained that to run a Lambda function you need an execution role, with a minimum of access to Amazon CloudWatch Logs for log streaming.
We can define a new role using the default AWS managed IAM policy:
CallCenterBasicLambdaRole:
Type: AWS::IAM::Role
Properties:
Description: "Basic Lamda execution role"
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterBasicLambdaRole
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'lambda.amazonaws.com'
Action:
- 'sts:AssumeRole'
And change the Lambda functions to use it:
OpenCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/open-case/
Handler: app.handler
Runtime: nodejs12.x
Role: !GetAtt CallCenterBasicLambdaRole.Arn
> Note that naming the IAM roles is generally [[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html|advised against]], but if you do you should include the region like I did.
=== State Machine ===
For the state machine I checked the 10 minute tutorial for [[https://aws.amazon.com/getting-started/hands-on/create-a-serverless-workflow-step-functions-lambda/|Step Functions]] to verify the permissions needed:
CallCenterBasicStepFunctionsRole:
Type: AWS::IAM::Role
Properties:
Description: "Basic Step Functions role. Allows to invoke Lambda Functions. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterBasicStepFunctionsRole
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaRole'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'states.amazonaws.com'
Action:
- 'sts:AssumeRole'
And this also needs to be updated in the state machine:
CallCenterStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachines/CallCenterStateMachine.asl.json
DefinitionSubstitutions:
OpenCaseFunctionArn: !GetAtt OpenCaseFunction.Arn
AssignCaseFunctionArn: !GetAtt AssignCaseFunction.Arn
WorkOnCaseFunctionArn: !GetAtt WorkOnCaseFunction.Arn
CloseCaseFunctionArn: !GetAtt CloseCaseFunction.Arn
EscalateCaseFunctionArn: !GetAtt EscalateCaseFunction.Arn
Role: !GetAtt CallCenterBasicStepFunctionsRole.Arn
# Policies:
# - LambdaInvokePolicy:
# FunctionName: !Ref OpenCaseFunction
# - LambdaInvokePolicy:
# FunctionName: !Ref AssignCaseFunction
# - LambdaInvokePolicy:
# FunctionName: !Ref WorkOnCaseFunction
# - LambdaInvokePolicy:
# FunctionName: !Ref CloseCaseFunction
# - LambdaInvokePolicy:
# FunctionName: !Ref EscalateCaseFunction
Events:
APIEvent:
Type: Api
Properties:
Path: /case
Method: post
RestApiId: !Ref CallCenterAPI
> Notice that you can leave out all the policies and simply put in the role property.
=== State Machine API Event ===
This is the last role and, apparently, the trickiest. This role is used in the API gateway to trigger the Step Function State Machine. There is no AWS managed role for it, so we need to create a role with a custom policy. On top of that there is also a bug that the default role is still being created, even though it's not required any more, and can be deleted without impact. More information on that at the end of this section.
This is the yaml code for the role:
CallCenterAPIGatewayRole:
Type: AWS::IAM::Role
Properties:
Description: "API Gateway role. Allows to invoke Step Functions state machine. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterAPIGatewayRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'apigateway.amazonaws.com'
Action:
- 'sts:AssumeRole'
Policies:
-
PolicyName: 'StateMachine-StartExecution'
PolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Action:
- 'states:StartExecution'
Resource: !GetAtt CallCenterStateMachine.Arn
And then we need to update the API Gateway configuration. Now that is a big part, so I'll only show the section involved:
x-amazon-apigateway-integration:
#credentials: !GetAtt CallCenterStateMachineAPIEventRole.Arn
credentials: !GetAtt CallCenterAPIGatewayRole.Arn
So, now we have a custom Role for the API Gateway to trigger the state machine, which means we can delete the automatically created API Event Role. As said before, this one is still being created but can be deleted. It's called with format like
- 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
Add the following resource to the template to create a loggroup with as an extra to delete all logs after 30 days:
CallCenterStateMachineLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/stepfunctions/CallCenterStateMachine
RetentionInDays: 30 #[1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
And the following to the properties of the state machine resource:
Logging:
Destinations:
- CloudWatchLogsLogGroup:
LogGroupArn: !GetAtt CallCenterStateMachineLogGroup.Arn
IncludeExecutionData: true
Level: ALL
> Note: see [[https://docs.aws.amazon.com/step-functions/latest/dg/cloudwatch-log-level.html|Step Functions Log Levels]] for log level options and implications.
=== API GateWay ===
API logging is done on the stage level, you can add this part to the API Gateway properties:
MethodSettings:
- LoggingLevel: INFO # OFF, ERROR, INFO
ResourcePath: '/*' # allows for logging on any resource
HttpMethod: '*' # allows for logging on any method
==== API Gateway Account ====
Now the previous setting defines the logging level but it actually doesn't really enable logging. For logging, the API Gateway needs access to CloudWatch, and this is definedas a global setting for all API Gateways through the [[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-account.html| API Gateway Account]].
So we need an extra IAM role with permissions to log to CloudWatch, which is actually a managed AWS policy:
CallCenterAPIGatewayAccountRole:
Type: AWS::IAM::Role
Properties:
Description: "API Gateway role, provides access to Cloudwatch for logging. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterAPIGatewayAccountRole
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'apigateway.amazonaws.com'
Action:
- 'sts:AssumeRole'
And we need to define an extra resource of APIGateway Account type:
CallCenterAPIGatewayAccount:
Type: AWS::ApiGateway::Account
Properties:
CloudWatchRoleArn: !GetAtt CallCenterAPIGatewayAccountRole.Arn
> Note that if you already have this configured, this will be overwritten, so you only need to set this up once.
== S3 BucketName Mapping and Retain ==
Now all that's left is to configure the S3 bucket a bit more, I want to set a name, but because s3 buckets need to be unique (worldwide) I have to setup a name per account. I also, just for sanity want to keep the S3 bucket in case I delete the CloudFormation stack.
So that gives me this S3 policy:
CallCenterWebAppBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !FindInMap [AccountMap, !Ref "AWS::AccountId", s3bucketname]
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
And this as the mapping:
Mappings:
AccountMap: #Different settings based on the AWS Account to which is being deployed
"xxxxxxxxxxxxx":
"s3bucketname": "callcentertestbucket"
"xxxxxxxxxxxxx":
"s3bucketname": "callcenterprodbucket"
> Notice that upon deletion of the stack the s3 bucket is now being retained, resulting in the error "
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
SAM-CallCenterWebApp
Deploy State Machine, Lambda Functions, API Gateway and S3 bucket.
Mappings:
AccountMap: #Different settings based on the AWS Account to which is being deployed
"xxxxxxxxxxxxx":
"s3bucketname": "callcentertestbucket"
"xxxxxxxxxxxxx":
"s3bucketname": "callcenterprodbucket"
Resources:
CallCenterStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachines/CallCenterStateMachine.asl.json
DefinitionSubstitutions:
OpenCaseFunctionArn: !GetAtt OpenCaseFunction.Arn
AssignCaseFunctionArn: !GetAtt AssignCaseFunction.Arn
WorkOnCaseFunctionArn: !GetAtt WorkOnCaseFunction.Arn
CloseCaseFunctionArn: !GetAtt CloseCaseFunction.Arn
EscalateCaseFunctionArn: !GetAtt EscalateCaseFunction.Arn
Role: !GetAtt CallCenterBasicStepFunctionsRole.Arn
Logging:
Destinations:
- CloudWatchLogsLogGroup:
LogGroupArn: !GetAtt CallCenterStateMachineLogGroup.Arn
IncludeExecutionData: true
Level: ALL
Events:
APIEvent:
Type: Api
Properties:
Path: /case
Method: post
RestApiId: !Ref CallCenterAPI
CallCenterWebAppBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !FindInMap [AccountMap, !Ref "AWS::AccountId", s3bucketname]
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
CallCenterWebAppBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: MyPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref CallCenterWebAppBucket
- /*
Bucket: !Ref CallCenterWebAppBucket
OpenCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/open-case/
Handler: app.handler
Runtime: nodejs12.x
Role: !GetAtt CallCenterBasicLambdaRole.Arn
AssignCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/assign-case/
Handler: app.handler
Runtime: nodejs12.x
Role: !GetAtt CallCenterBasicLambdaRole.Arn
WorkOnCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/work-on-case/
Handler: app.handler
Runtime: nodejs12.x
Role: !GetAtt CallCenterBasicLambdaRole.Arn
CloseCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/close-case/
Handler: app.handler
Runtime: nodejs12.x
Role: !GetAtt CallCenterBasicLambdaRole.Arn
EscalateCaseFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/escalate-case/
Handler: app.handler
Runtime: nodejs12.x
Role: !GetAtt CallCenterBasicLambdaRole.Arn
# API
CallCenterAPI:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowMethods: "'POST, GET, OPTIONS, HEAD'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
MethodSettings:
- LoggingLevel: INFO # OFF, ERROR, INFO
ResourcePath: '/*' # allows for logging on any resource
HttpMethod: '*' # allows for logging on any method
GatewayResponses:
DEFAULT_4xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DEFAULT_5xx:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
Access-Control-Allow-Methods: "'POST, OPTIONS'"
Access-Control-Allow-Headers: "'*'"
DefinitionBody:
swagger: "2.0"
info:
title: "sam-callcenterwebapp" #This is the name of the API
paths:
/case:
post:
consumes:
- "application/json"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
"400":
description: "400 response"
x-amazon-apigateway-integration:
credentials: !GetAtt CallCenterAPIGatewayRole.Arn
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:states:action/StartExecution"
responses:
"200":
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
"400":
statusCode: "400"
requestTemplates:
application/json:
Fn::Sub: "{\"input\": \"$util.escapeJavaScript($input.json('$'))\"\
, \"stateMachineArn\": \"${CallCenterStateMachine.Arn}\"\
}"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
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-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
type: "mock"
CallCenterAPIGatewayAccount:
Type: AWS::ApiGateway::Account
Properties:
CloudWatchRoleArn: !GetAtt CallCenterAPIGatewayAccountRole.Arn
#IAM
CallCenterBasicLambdaRole:
Type: AWS::IAM::Role
Properties:
Description: "Basic Lamda execution role, provides access to Cloudwatch for logging. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterBasicLambdaRole
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'lambda.amazonaws.com'
Action:
- 'sts:AssumeRole'
CallCenterBasicStepFunctionsRole:
Type: AWS::IAM::Role
Properties:
Description: "Basic Step Functions role. Allows to invoke Lambda Functions, and logging to Cloudwatch. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterBasicStepFunctionsRole
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaRole'
- 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'states.amazonaws.com'
Action:
- 'sts:AssumeRole'
CallCenterAPIGatewayRole:
Type: AWS::IAM::Role
Properties:
Description: "API Gateway role. Allows to invoke Step Functions state machine. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterAPIGatewayRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'apigateway.amazonaws.com'
Action:
- 'sts:AssumeRole'
Policies:
-
PolicyName: 'StateMachine-StartExecution'
PolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Action:
- 'states:StartExecution'
Resource: !GetAtt CallCenterStateMachine.Arn
CallCenterAPIGatewayAccountRole:
Type: AWS::IAM::Role
Properties:
Description: "API Gateway role, provides access to Cloudwatch for logging. "
RoleName: !Join
- ''
- - !Ref AWS::StackName
- '-'
- !Ref AWS::Region
- '-'
- CallCenterAPIGatewayAccountRole
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
-
Effect: Allow
Principal:
Service:
- 'apigateway.amazonaws.com'
Action:
- 'sts:AssumeRole'
CallCenterStateMachineLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/stepfunctions/CallCenterStateMachine
RetentionInDays: 30 #[1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
Outputs:
# In AWS Toolkit for VS Code the output is not displayed. Check the output in the AWS Cloudformation Console
WebsiteURL:
Description: URL for website hosted on S3
Value: !GetAtt
- CallCenterWebAppBucket
- WebsiteURL
APIInvokeURL:
Description: "API Prod stage endpoint"
Value: !Sub "https://${CallCenterAPI}.execute-api.${AWS::Region}.amazonaws.com/prod/case"
= Alternative to Swagger =
As explained above, the API gateway gets automatically created but it was a bit hard to find the correct way to deploy the CORS part as well. Before I figured out the swagger optionI used above I explored the option below, to create all the API Gateway resources separately. I found it more clear on what it does exactly, but it wouldn't work in combination with the automatically created API Gateway (the CloudFormation stack will try to create two methods for case), which means you have to create all the methods from scratch. As I said, I got pretty far and all options below work correctly, just not combined with the SAM template above but I found it a shame to just delete it.
# CaseResource:
# Type: AWS::ApiGateway::Resource
# Properties:
# ParentId: !GetAtt CallCenterAPI.RootResourceId
# PathPart: case
# RestApiId: !Ref CallCenterAPI
# OptionsMethod:
# Type: AWS::ApiGateway::Method
# Properties:
# RestApiId: !Ref CallCenterAPI
# ResourceId: !Ref CaseResource
# HttpMethod: OPTIONS
# AuthorizationType: NONE
# Integration:
# IntegrationResponses:
# - StatusCode: 200
# ResponseParameters:
# method.response.header.Access-Control-Allow-Headers: "'*'"
# method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
# method.response.header.Access-Control-Allow-Origin: "'*'"
# MethodResponses:
# - StatusCode: 200
# ResponseParameters:
# method.response.header.Access-Control-Allow-Headers: true
# method.response.header.Access-Control-Allow-Methods: true
# method.response.header.Access-Control-Allow-Origin: true
# APIDeployment:
# DependsOn: OptionsMethod
# Type: AWS::ApiGateway::Deployment
# Properties:
# RestApiId: !Ref CallCenterAPI
# StageName: prod
# PostMethod:
# Type: AWS::ApiGateway::Method
# Properties:
# RestApiId: !Ref CallCenterAPI
# ResourceId: !Ref CaseResource
# HttpMethod: POST
# AuthorizationType: NONE
# Integration:
# IntegrationResponses:
# - StatusCode: 200
# ResponseParameters:
# method.response.header.Access-Control-Allow-Origin: "'*'"
# MethodResponses:
# - StatusCode: 200
# ResponseParameters:
# method.response.header.Access-Control-Allow-Origin: true
# APIDeployment:
# DependsOn:
# - PostMethod
# - OptionsMethod
# Type: AWS::ApiGateway::Deployment
# Properties:
# RestApiId: !Ref CallCenterAPI
# StageName: prod
= Useful Links =
* [[https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-export-api.html|Export an API Gateway]]
* [[https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-statemachine-statemachineeventsource.html|StateMachine Event Property]]
* [[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-s3.html|S3 Cloudformation examples]]
* [[https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html|CORS]].
* [[https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html|State Machine Resource]]
* [[https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html|SAM policy templates]]
* [[https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html|SAM Function Resource]]
* [[https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-generated-resources.html|SAM Implicit Created Resources]]
* [[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html|Pseudo parameters like AWS::Region and AWS:StackName]]
* [[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html|Intrinsic functions like Join, sub and ref]]
* [[https://docs.aws.amazon.com/step-functions/latest/dg/cloudwatch-log-level.html|Step Functions Log Levels]]
* [[https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html|SAM Mappings]]
{{tag>aws devops scripts}}