This shows you the differences between two versions of the page.
— |
samwebapp [2021/09/24 00:25] (current) |
||
---|---|---|---|
Line 1: | Line 1: | ||
+ | = SAM - Creating a WebApp in AWS Toolkit for VS Code = | ||
+ | This this page is the follow up from [[samstepandlambda]] which was also a follow up from [[awstoolkitforvscode]]. We used the AWS Toolkit for VS Code to deploy a Step Functions State Machine and also created a Serverless Application to deploy Lambda functions. Then we created a new SAM to combine these two. Now that we've done that we are going to extend that by creating a frontend for it, we'll create a S3 bucket with a website and then an API Gateway to let the gateway talk to the Step Functions and Lambdas. This is no longer just 10 minute tutorial for [[https:// | ||
+ | |||
+ | = Create a new SAM = | ||
+ | |||
+ | To start fresh we'll copy the SAM-CallCenterApp folder to a new one and call it SAM-CallCenterWebApp. Because we need to change and setup quite a few resources and changes in the file, I'll first explain the added resources and changes and the show you the code. | ||
+ | |||
+ | == Step Function State Machine == | ||
+ | |||
+ | The CallCenterStateMachine we created works very well, but we don't any longer want to start it manually. We'll use a frontend to start it, which means we need an API Gateway to let the frontend (a website on S3) and the state machine talk to each other. This is very easy done by adding an Event property to the state machine. This will automatically create an API Gateway. This is the basic setup: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterStateMachine: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | DefinitionUri: | ||
+ | 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: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterAPI: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | StageName: prod | ||
+ | </ | ||
+ | |||
+ | If you would deploy this however, you'd still have a problem. [[https:// | ||
+ | |||
+ | < | ||
+ | ✔ Add Access-Control-Allow-Headers, | ||
+ | ✔ Add Access-Control-Allow-Headers, | ||
+ | ✔ 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: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterAPI: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | StageName: prod | ||
+ | Cors: | ||
+ | AllowMethods: | ||
+ | AllowHeaders: | ||
+ | AllowOrigin: | ||
+ | GatewayResponses: | ||
+ | DEFAULT_4xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DEFAULT_5xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | 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 " | ||
+ | |||
+ | Now we can change the output to fit our explicit needs and add the final piece to our deployment. | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterAPI: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | StageName: prod | ||
+ | Cors: | ||
+ | AllowMethods: | ||
+ | AllowHeaders: | ||
+ | AllowOrigin: | ||
+ | GatewayResponses: | ||
+ | DEFAULT_4xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DEFAULT_5xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DefinitionBody: | ||
+ | swagger: " | ||
+ | info: | ||
+ | title: " | ||
+ | paths: | ||
+ | /case: | ||
+ | post: | ||
+ | consumes: | ||
+ | - " | ||
+ | responses: | ||
+ | " | ||
+ | description: | ||
+ | headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | type: " | ||
+ | " | ||
+ | description: | ||
+ | x-amazon-apigateway-integration: | ||
+ | credentials: | ||
+ | uri: | ||
+ | Fn::Sub: " | ||
+ | responses: | ||
+ | " | ||
+ | statusCode: " | ||
+ | responseParameters: | ||
+ | method.response.header.Access-Control-Allow-Origin: | ||
+ | " | ||
+ | statusCode: " | ||
+ | requestTemplates: | ||
+ | application/ | ||
+ | Fn::Sub: " | ||
+ | , \" | ||
+ | }" | ||
+ | passthroughBehavior: | ||
+ | httpMethod: " | ||
+ | type: " | ||
+ | options: | ||
+ | consumes: | ||
+ | - " | ||
+ | produces: | ||
+ | - " | ||
+ | responses: | ||
+ | " | ||
+ | description: | ||
+ | headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | type: " | ||
+ | Access-Control-Allow-Methods: | ||
+ | type: " | ||
+ | Access-Control-Allow-Headers: | ||
+ | type: " | ||
+ | x-amazon-apigateway-integration: | ||
+ | responses: | ||
+ | default: | ||
+ | statusCode: " | ||
+ | responseParameters: | ||
+ | method.response.header.Access-Control-Allow-Methods: | ||
+ | method.response.header.Access-Control-Allow-Headers: | ||
+ | method.response.header.Access-Control-Allow-Origin: | ||
+ | responseTemplates: | ||
+ | application/ | ||
+ | requestTemplates: | ||
+ | application/ | ||
+ | passthroughBehavior: | ||
+ | type: " | ||
+ | </ | ||
+ | |||
+ | > 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: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterWebAppBucket: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | AccessControl: | ||
+ | WebsiteConfiguration: | ||
+ | IndexDocument: | ||
+ | ErrorDocument: | ||
+ | | ||
+ | CallCenterWebAppBucketPolicy: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | PolicyDocument: | ||
+ | Id: MyPolicy | ||
+ | Version: 2012-10-17 | ||
+ | Statement: | ||
+ | - Sid: PublicReadForGetBucketObjects | ||
+ | Effect: Allow | ||
+ | Principal: ' | ||
+ | Action: ' | ||
+ | Resource: !Join | ||
+ | - '' | ||
+ | - - ' | ||
+ | - !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: | ||
+ | |||
+ | <code yaml> | ||
+ | WebsiteURL: | ||
+ | Description: | ||
+ | Value: !GetAtt | ||
+ | - CallCenterWebAppBucket | ||
+ | - WebsiteURL | ||
+ | APIInvokeURL: | ||
+ | Description: | ||
+ | Value: !Sub " | ||
+ | StateMachineIamRole: | ||
+ | Description: | ||
+ | Value: !GetAtt CallCenterStateMachineRole.Arn | ||
+ | StateMachineAPIEventIamRole: | ||
+ | Description: | ||
+ | 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: | ||
+ | |||
+ | <code yaml> | ||
+ | AWSTemplateFormatVersion: | ||
+ | Transform: AWS:: | ||
+ | Description: | ||
+ | SAM-CallCenterWebApp | ||
+ | |||
+ | Deploy State Machine, Lambda Functions, API Gateway and S3 bucket. | ||
+ | |||
+ | Resources: | ||
+ | CallCenterStateMachine: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | DefinitionUri: | ||
+ | DefinitionSubstitutions: | ||
+ | OpenCaseFunctionArn: | ||
+ | AssignCaseFunctionArn: | ||
+ | WorkOnCaseFunctionArn: | ||
+ | CloseCaseFunctionArn: | ||
+ | EscalateCaseFunctionArn: | ||
+ | Policies: | ||
+ | - LambdaInvokePolicy: | ||
+ | FunctionName: | ||
+ | - LambdaInvokePolicy: | ||
+ | FunctionName: | ||
+ | - LambdaInvokePolicy: | ||
+ | FunctionName: | ||
+ | - LambdaInvokePolicy: | ||
+ | FunctionName: | ||
+ | - LambdaInvokePolicy: | ||
+ | FunctionName: | ||
+ | Events: | ||
+ | APIEvent: | ||
+ | Type: Api | ||
+ | Properties: | ||
+ | Path: /case | ||
+ | Method: post | ||
+ | RestApiId: !Ref CallCenterAPI | ||
+ | | ||
+ | CallCenterWebAppBucket: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | AccessControl: | ||
+ | WebsiteConfiguration: | ||
+ | IndexDocument: | ||
+ | ErrorDocument: | ||
+ | | ||
+ | CallCenterWebAppBucketPolicy: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | PolicyDocument: | ||
+ | Id: MyPolicy | ||
+ | Version: 2012-10-17 | ||
+ | Statement: | ||
+ | - Sid: PublicReadForGetBucketObjects | ||
+ | Effect: Allow | ||
+ | Principal: ' | ||
+ | Action: ' | ||
+ | Resource: !Join | ||
+ | - '' | ||
+ | - - ' | ||
+ | - !Ref CallCenterWebAppBucket | ||
+ | - /* | ||
+ | Bucket: !Ref CallCenterWebAppBucket | ||
+ | |||
+ | OpenCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: arn: | ||
+ | |||
+ | AssignCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: arn: | ||
+ | |||
+ | WorkOnCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: arn: | ||
+ | |||
+ | CloseCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: arn: | ||
+ | |||
+ | EscalateCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: arn: | ||
+ | |||
+ | # API | ||
+ | CallCenterAPI: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | StageName: prod | ||
+ | Cors: | ||
+ | AllowMethods: | ||
+ | AllowHeaders: | ||
+ | AllowOrigin: | ||
+ | GatewayResponses: | ||
+ | DEFAULT_4xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DEFAULT_5xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DefinitionBody: | ||
+ | swagger: " | ||
+ | info: | ||
+ | title: " | ||
+ | paths: | ||
+ | /case: | ||
+ | post: | ||
+ | consumes: | ||
+ | - " | ||
+ | responses: | ||
+ | " | ||
+ | description: | ||
+ | headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | type: " | ||
+ | " | ||
+ | description: | ||
+ | x-amazon-apigateway-integration: | ||
+ | credentials: | ||
+ | uri: | ||
+ | Fn::Sub: " | ||
+ | responses: | ||
+ | " | ||
+ | statusCode: " | ||
+ | responseParameters: | ||
+ | method.response.header.Access-Control-Allow-Origin: | ||
+ | " | ||
+ | statusCode: " | ||
+ | requestTemplates: | ||
+ | application/ | ||
+ | Fn::Sub: " | ||
+ | , \" | ||
+ | }" | ||
+ | passthroughBehavior: | ||
+ | httpMethod: " | ||
+ | type: " | ||
+ | options: | ||
+ | consumes: | ||
+ | - " | ||
+ | produces: | ||
+ | - " | ||
+ | responses: | ||
+ | " | ||
+ | description: | ||
+ | headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | type: " | ||
+ | Access-Control-Allow-Methods: | ||
+ | type: " | ||
+ | Access-Control-Allow-Headers: | ||
+ | type: " | ||
+ | x-amazon-apigateway-integration: | ||
+ | responses: | ||
+ | default: | ||
+ | statusCode: " | ||
+ | responseParameters: | ||
+ | method.response.header.Access-Control-Allow-Methods: | ||
+ | method.response.header.Access-Control-Allow-Headers: | ||
+ | method.response.header.Access-Control-Allow-Origin: | ||
+ | responseTemplates: | ||
+ | application/ | ||
+ | requestTemplates: | ||
+ | application/ | ||
+ | passthroughBehavior: | ||
+ | type: " | ||
+ | |||
+ | Outputs: | ||
+ | # In AWS Toolkit for VS Code the output is not displayed. Check the output in the AWS Cloudformation Console | ||
+ | WebsiteURL: | ||
+ | Description: | ||
+ | Value: !GetAtt | ||
+ | - CallCenterWebAppBucket | ||
+ | - WebsiteURL | ||
+ | APIInvokeURL: | ||
+ | Description: | ||
+ | Value: !Sub " | ||
+ | StateMachineIamRole: | ||
+ | Description: | ||
+ | Value: !GetAtt CallCenterStateMachineRole.Arn | ||
+ | StateMachineAPIEventIamRole: | ||
+ | Description: | ||
+ | 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' | ||
+ | |||
+ | <code html> | ||
+ | < | ||
+ | <html lang=" | ||
+ | < | ||
+ | <meta charset=" | ||
+ | <meta name=" | ||
+ | <meta http-equiv=" | ||
+ | < | ||
+ | |||
+ | <!-- CSS Stylesheets --> | ||
+ | <link rel=" | ||
+ | |||
+ | <!-- Bootstrap Scripts --> | ||
+ | <script src=" | ||
+ | <script src=" | ||
+ | <script src=" | ||
+ | |||
+ | <!-- Internal CSS --> | ||
+ | < | ||
+ | .container-fluid { | ||
+ | padding: 7% 15%; | ||
+ | } | ||
+ | </ | ||
+ | </ | ||
+ | < | ||
+ | |||
+ | <section id=" | ||
+ | | ||
+ | <div class=" | ||
+ | < | ||
+ | |||
+ | <form class=" | ||
+ | | ||
+ | <div class=" | ||
+ | <div class=" | ||
+ | <div class=" | ||
+ | <span class=" | ||
+ | </ | ||
+ | <input type=" | ||
+ | <div class=" | ||
+ | </ | ||
+ | </ | ||
+ | | ||
+ | <button class=" | ||
+ | |||
+ | </ | ||
+ | | ||
+ | <p class=" | ||
+ | |||
+ | </ | ||
+ | | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | <section id=" | ||
+ | |||
+ | <div class=" | ||
+ | <div class=" | ||
+ | <div class=" | ||
+ | < | ||
+ | </ | ||
+ | | ||
+ | <div class=" | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | | ||
+ | </ | ||
+ | | ||
+ | | ||
+ | </ | ||
+ | |||
+ | <!-- Bootstrap Script | ||
+ | <script src=" | ||
+ | <!-- Form Validation and Sending --> | ||
+ | < | ||
+ | (function() { | ||
+ | 'use strict'; | ||
+ | // | ||
+ | window.addEventListener(' | ||
+ | // Fetch all the forms we want to apply custom Bootstrap validation styles to | ||
+ | var forms = document.getElementsByClassName(' | ||
+ | // Loop over them and prevent submission | ||
+ | var validation = Array.prototype.filter.call(forms, | ||
+ | form.addEventListener(' | ||
+ | if (form.checkValidity() === false) { | ||
+ | event.preventDefault(); | ||
+ | event.stopPropagation(); | ||
+ | } | ||
+ | form.classList.add(' | ||
+ | if (form.checkValidity() === true) { | ||
+ | console.log (" | ||
+ | collectInputData(event); | ||
+ | event.preventDefault(); | ||
+ | } | ||
+ | }, false); | ||
+ | }); | ||
+ | }, false); | ||
+ | })(); | ||
+ | |||
+ | function collectInputData(event) { | ||
+ | // Input | ||
+ | var FormTicketNumber = $("# | ||
+ | // Collect all data into one array | ||
+ | var inputData = { | ||
+ | formTicketNumber: | ||
+ | }; | ||
+ | console.log(" | ||
+ | submitToAPI(inputData); | ||
+ | } | ||
+ | |||
+ | // Used for contact | ||
+ | function submitToAPI(inputData) { | ||
+ | $.ajax({ | ||
+ | method: ' | ||
+ | url: ' | ||
+ | dataType: " | ||
+ | crossDomain: | ||
+ | data: JSON.stringify(inputData), | ||
+ | contentType: | ||
+ | success: completeTicketRequest, | ||
+ | error: function ajaxError(jqXHR, | ||
+ | console.error(' | ||
+ | console.error(' | ||
+ | alert(' | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | |||
+ | function completeTicketRequest(result) { | ||
+ | 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:// | ||
+ | |||
+ | We can define a new role using the default AWS managed IAM policy: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterBasicLambdaRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterBasicLambdaRole | ||
+ | ManagedPolicyArns: | ||
+ | - ' | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | </ | ||
+ | |||
+ | And change the Lambda functions to use it: | ||
+ | <code yaml> | ||
+ | OpenCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: !GetAtt CallCenterBasicLambdaRole.Arn | ||
+ | </ | ||
+ | |||
+ | > Note that naming the IAM roles is generally [[https:// | ||
+ | |||
+ | === State Machine === | ||
+ | |||
+ | For the state machine I checked the 10 minute tutorial for [[https:// | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterBasicStepFunctionsRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterBasicStepFunctionsRole | ||
+ | ManagedPolicyArns: | ||
+ | - ' | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | </ | ||
+ | |||
+ | And this also needs to be updated in the state machine: | ||
+ | <code yaml> | ||
+ | CallCenterStateMachine: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | DefinitionUri: | ||
+ | DefinitionSubstitutions: | ||
+ | OpenCaseFunctionArn: | ||
+ | AssignCaseFunctionArn: | ||
+ | WorkOnCaseFunctionArn: | ||
+ | CloseCaseFunctionArn: | ||
+ | EscalateCaseFunctionArn: | ||
+ | Role: !GetAtt CallCenterBasicStepFunctionsRole.Arn | ||
+ | # Policies: | ||
+ | # - LambdaInvokePolicy: | ||
+ | # | ||
+ | # - LambdaInvokePolicy: | ||
+ | # | ||
+ | # - LambdaInvokePolicy: | ||
+ | # | ||
+ | # - LambdaInvokePolicy: | ||
+ | # | ||
+ | # - LambdaInvokePolicy: | ||
+ | # | ||
+ | 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: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterAPIGatewayRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterAPIGatewayRole | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | Policies: | ||
+ | - | ||
+ | PolicyName: ' | ||
+ | PolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Action: | ||
+ | - ' | ||
+ | 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: | ||
+ | |||
+ | <code yaml> | ||
+ | x-amazon-apigateway-integration: | ||
+ | # | ||
+ | credentials: | ||
+ | </ | ||
+ | |||
+ | 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 < | ||
+ | |||
+ | > Posted this on [[https:// | ||
+ | |||
+ | == CloudWatch == | ||
+ | |||
+ | Now, I like logging, in case stuff goes wrong, and I don't like it if I need to enable it afterwards I have something going wrong. So, let's enable cloudwatch for everything. | ||
+ | |||
+ | === Lambda === | ||
+ | |||
+ | For Lambda we have nothing to do, logging is enabled by default . | ||
+ | |||
+ | === State Machine === | ||
+ | |||
+ | The state machine logging is a bit complex. Not only needs it a predefined log group, it also needs extra permissions. | ||
+ | |||
+ | Add the following permissions to the CallCenterBasicStepFunctionsRole to allow the state machine to log to Cloudwatch: | ||
+ | < | ||
+ | - ' | ||
+ | </ | ||
+ | |||
+ | Add the following resource to the template to create a loggroup with as an extra to delete all logs after 30 days: | ||
+ | <code yaml> | ||
+ | CallCenterStateMachineLogGroup: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | LogGroupName: | ||
+ | RetentionInDays: | ||
+ | </ | ||
+ | |||
+ | And the following to the properties of the state machine resource: | ||
+ | <code yaml> | ||
+ | Logging: | ||
+ | Destinations: | ||
+ | - CloudWatchLogsLogGroup: | ||
+ | LogGroupArn: | ||
+ | IncludeExecutionData: | ||
+ | Level: ALL | ||
+ | </ | ||
+ | |||
+ | > Note: see [[https:// | ||
+ | |||
+ | === API GateWay === | ||
+ | |||
+ | API logging is done on the stage level, you can add this part to the API Gateway properties: | ||
+ | |||
+ | <code yaml> | ||
+ | MethodSettings: | ||
+ | - LoggingLevel: | ||
+ | ResourcePath: | ||
+ | HttpMethod: ' | ||
+ | </ | ||
+ | |||
+ | ==== API Gateway Account ==== | ||
+ | |||
+ | Now the previous setting defines the logging level but it actually doesn' | ||
+ | |||
+ | So we need an extra IAM role with permissions to log to CloudWatch, which is actually a managed AWS policy: | ||
+ | <code yaml> | ||
+ | CallCenterAPIGatewayAccountRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterAPIGatewayAccountRole | ||
+ | ManagedPolicyArns: | ||
+ | - ' | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | </ | ||
+ | |||
+ | And we need to define an extra resource of APIGateway Account type: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterAPIGatewayAccount: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CloudWatchRoleArn: | ||
+ | </ | ||
+ | |||
+ | > Note that if you already have this configured, this will be overwritten, | ||
+ | |||
+ | == 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: | ||
+ | |||
+ | <code yaml> | ||
+ | CallCenterWebAppBucket: | ||
+ | Type: AWS:: | ||
+ | DeletionPolicy: | ||
+ | Properties: | ||
+ | BucketName: !FindInMap [AccountMap, | ||
+ | AccessControl: | ||
+ | WebsiteConfiguration: | ||
+ | IndexDocument: | ||
+ | ErrorDocument: | ||
+ | </ | ||
+ | |||
+ | And this as the mapping: | ||
+ | |||
+ | <code yaml> | ||
+ | Mappings: | ||
+ | AccountMap: #Different settings based on the AWS Account to which is being deployed | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | </ | ||
+ | |||
+ | > Notice that upon deletion of the stack the s3 bucket is now being retained, resulting in the error "< | ||
+ | |||
+ | = End Result = | ||
+ | |||
+ | The total template.yaml: | ||
+ | <code yaml | template.yaml> | ||
+ | AWSTemplateFormatVersion: | ||
+ | Transform: AWS:: | ||
+ | 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 | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | |||
+ | Resources: | ||
+ | CallCenterStateMachine: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | DefinitionUri: | ||
+ | DefinitionSubstitutions: | ||
+ | OpenCaseFunctionArn: | ||
+ | AssignCaseFunctionArn: | ||
+ | WorkOnCaseFunctionArn: | ||
+ | CloseCaseFunctionArn: | ||
+ | EscalateCaseFunctionArn: | ||
+ | Role: !GetAtt CallCenterBasicStepFunctionsRole.Arn | ||
+ | Logging: | ||
+ | Destinations: | ||
+ | - CloudWatchLogsLogGroup: | ||
+ | LogGroupArn: | ||
+ | IncludeExecutionData: | ||
+ | Level: ALL | ||
+ | Events: | ||
+ | APIEvent: | ||
+ | Type: Api | ||
+ | Properties: | ||
+ | Path: /case | ||
+ | Method: post | ||
+ | RestApiId: !Ref CallCenterAPI | ||
+ | | ||
+ | CallCenterWebAppBucket: | ||
+ | Type: AWS:: | ||
+ | DeletionPolicy: | ||
+ | Properties: | ||
+ | BucketName: !FindInMap [AccountMap, | ||
+ | AccessControl: | ||
+ | WebsiteConfiguration: | ||
+ | IndexDocument: | ||
+ | ErrorDocument: | ||
+ | | ||
+ | CallCenterWebAppBucketPolicy: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | PolicyDocument: | ||
+ | Id: MyPolicy | ||
+ | Version: 2012-10-17 | ||
+ | Statement: | ||
+ | - Sid: PublicReadForGetBucketObjects | ||
+ | Effect: Allow | ||
+ | Principal: ' | ||
+ | Action: ' | ||
+ | Resource: !Join | ||
+ | - '' | ||
+ | - - ' | ||
+ | - !Ref CallCenterWebAppBucket | ||
+ | - /* | ||
+ | Bucket: !Ref CallCenterWebAppBucket | ||
+ | |||
+ | OpenCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: !GetAtt CallCenterBasicLambdaRole.Arn | ||
+ | |||
+ | AssignCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: !GetAtt CallCenterBasicLambdaRole.Arn | ||
+ | |||
+ | WorkOnCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: !GetAtt CallCenterBasicLambdaRole.Arn | ||
+ | |||
+ | CloseCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: !GetAtt CallCenterBasicLambdaRole.Arn | ||
+ | |||
+ | EscalateCaseFunction: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CodeUri: functions/ | ||
+ | Handler: app.handler | ||
+ | Runtime: nodejs12.x | ||
+ | Role: !GetAtt CallCenterBasicLambdaRole.Arn | ||
+ | |||
+ | # API | ||
+ | CallCenterAPI: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | StageName: prod | ||
+ | Cors: | ||
+ | AllowMethods: | ||
+ | AllowHeaders: | ||
+ | AllowOrigin: | ||
+ | MethodSettings: | ||
+ | - LoggingLevel: | ||
+ | ResourcePath: | ||
+ | HttpMethod: ' | ||
+ | GatewayResponses: | ||
+ | DEFAULT_4xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DEFAULT_5xx: | ||
+ | ResponseParameters: | ||
+ | Headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | Access-Control-Allow-Methods: | ||
+ | Access-Control-Allow-Headers: | ||
+ | DefinitionBody: | ||
+ | swagger: " | ||
+ | info: | ||
+ | title: " | ||
+ | paths: | ||
+ | /case: | ||
+ | post: | ||
+ | consumes: | ||
+ | - " | ||
+ | responses: | ||
+ | " | ||
+ | description: | ||
+ | headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | type: " | ||
+ | " | ||
+ | description: | ||
+ | x-amazon-apigateway-integration: | ||
+ | credentials: | ||
+ | uri: | ||
+ | Fn::Sub: " | ||
+ | responses: | ||
+ | " | ||
+ | statusCode: " | ||
+ | responseParameters: | ||
+ | method.response.header.Access-Control-Allow-Origin: | ||
+ | " | ||
+ | statusCode: " | ||
+ | requestTemplates: | ||
+ | application/ | ||
+ | Fn::Sub: " | ||
+ | , \" | ||
+ | }" | ||
+ | passthroughBehavior: | ||
+ | httpMethod: " | ||
+ | type: " | ||
+ | options: | ||
+ | consumes: | ||
+ | - " | ||
+ | produces: | ||
+ | - " | ||
+ | responses: | ||
+ | " | ||
+ | description: | ||
+ | headers: | ||
+ | Access-Control-Allow-Origin: | ||
+ | type: " | ||
+ | Access-Control-Allow-Methods: | ||
+ | type: " | ||
+ | Access-Control-Allow-Headers: | ||
+ | type: " | ||
+ | x-amazon-apigateway-integration: | ||
+ | responses: | ||
+ | default: | ||
+ | statusCode: " | ||
+ | responseParameters: | ||
+ | method.response.header.Access-Control-Allow-Methods: | ||
+ | method.response.header.Access-Control-Allow-Headers: | ||
+ | method.response.header.Access-Control-Allow-Origin: | ||
+ | responseTemplates: | ||
+ | application/ | ||
+ | requestTemplates: | ||
+ | application/ | ||
+ | passthroughBehavior: | ||
+ | type: " | ||
+ | |||
+ | CallCenterAPIGatewayAccount: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | CloudWatchRoleArn: | ||
+ | |||
+ | #IAM | ||
+ | CallCenterBasicLambdaRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterBasicLambdaRole | ||
+ | ManagedPolicyArns: | ||
+ | - ' | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | |||
+ | CallCenterBasicStepFunctionsRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterBasicStepFunctionsRole | ||
+ | ManagedPolicyArns: | ||
+ | - ' | ||
+ | - ' | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | |||
+ | CallCenterAPIGatewayRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterAPIGatewayRole | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | Policies: | ||
+ | - | ||
+ | PolicyName: ' | ||
+ | PolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Action: | ||
+ | - ' | ||
+ | Resource: !GetAtt CallCenterStateMachine.Arn | ||
+ | | ||
+ | CallCenterAPIGatewayAccountRole: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | Description: | ||
+ | RoleName: !Join | ||
+ | - '' | ||
+ | - - !Ref AWS:: | ||
+ | - ' | ||
+ | - !Ref AWS::Region | ||
+ | - ' | ||
+ | - CallCenterAPIGatewayAccountRole | ||
+ | ManagedPolicyArns: | ||
+ | - ' | ||
+ | AssumeRolePolicyDocument: | ||
+ | Version: ' | ||
+ | Statement: | ||
+ | - | ||
+ | Effect: Allow | ||
+ | Principal: | ||
+ | Service: | ||
+ | - ' | ||
+ | Action: | ||
+ | - ' | ||
+ | |||
+ | CallCenterStateMachineLogGroup: | ||
+ | Type: AWS:: | ||
+ | Properties: | ||
+ | LogGroupName: | ||
+ | RetentionInDays: | ||
+ | |||
+ | Outputs: | ||
+ | # In AWS Toolkit for VS Code the output is not displayed. Check the output in the AWS Cloudformation Console | ||
+ | WebsiteURL: | ||
+ | Description: | ||
+ | Value: !GetAtt | ||
+ | - CallCenterWebAppBucket | ||
+ | - WebsiteURL | ||
+ | APIInvokeURL: | ||
+ | Description: | ||
+ | Value: !Sub " | ||
+ | </ | ||
+ | |||
+ | = 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' | ||
+ | |||
+ | <code yaml> | ||
+ | # CaseResource: | ||
+ | # Type: AWS:: | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | |||
+ | # OptionsMethod: | ||
+ | # Type: AWS:: | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # - StatusCode: 200 | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # - StatusCode: 200 | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | |||
+ | # APIDeployment: | ||
+ | # | ||
+ | # Type: AWS:: | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | |||
+ | |||
+ | # PostMethod: | ||
+ | # Type: AWS:: | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # - StatusCode: 200 | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | # - StatusCode: 200 | ||
+ | # | ||
+ | # | ||
+ | | ||
+ | # APIDeployment: | ||
+ | # | ||
+ | # - PostMethod | ||
+ | # - OptionsMethod | ||
+ | # Type: AWS:: | ||
+ | # | ||
+ | # | ||
+ | # | ||
+ | </ | ||
+ | |||
+ | = Resources = | ||
+ | |||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | |||
+ | {{tag> |