SHIFT

--- Sjoerd Hooft's InFormation Technology ---

User Tools

Site Tools


samwebapp
Differences

This shows you the differences between two versions of the page.

Link to this comparison view

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://aws.amazon.com/getting-started/hands-on/create-a-serverless-workflow-step-functions-lambda/|Step Functions]], we are extending it now. We will try to use the AWS Toolkit as much as we can. 
 +
 += 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::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
 +</code>
 +
 +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::Serverless::Api
 +    Properties: 
 +      StageName: prod
 +</code>
 +
 +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:
 +
 +<code>
 +✔ 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.
 +</code>
 +
 +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::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: "'*'"
 +</code>
 +
 +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. 
 +
 +<code yaml>
 +  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"
 +</code>
 +
 +> 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::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
 +</code>
 +
 +== 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: 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
 +</code>
 +
 +> 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: "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
 +
 +
 +</code>
 +
 +== 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:
 +
 +<code html>
 +<!DOCTYPE html>
 +<html lang="nl">
 +<head>
 +    <meta charset="UTF-8">
 +    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 +    <meta http-equiv="X-UA-Compatible" content="ie=edge">
 +    <title>Call Center Web App</title>
 +
 +    <!-- CSS Stylesheets -->
 +    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
 +
 +    <!-- Bootstrap Scripts -->
 +    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
 +    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
 +    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
 +
 +    <!-- Internal CSS -->
 +    <style>
 +        .container-fluid {
 +            padding: 7% 15%;
 +        }
 +    </style>
 +</head>
 +<body>
 +
 +    <section id="cta">
 +      
 +      <div class="container-fluid">
 +        <p>Please enter your ticket number </p>
 +
 +        <form class="needs-validation" novalidate id="contactform">
 +            
 +            <div class="form-row needs-validation">
 +              <div class="input-group mb-3 col-md-6">
 +                  <div class="input-group-prepend">
 +                    <span class="input-group-text">Ticket number</span>
 +                  </div>
 +                  <input type="number" class="form-control" id="ticketNumber"  maxLength="5" placeholder="A ticket nr" required>
 +                  <div class="invalid-feedback">Your numeric tiket number </div>
 +              </div>
 +            </div>
 +          
 +          <button class="btn btn-primary my-3" type="submit">Submit</button>
 +
 +        </form>
 +        
 +        <p class="mt-2"><small>To check the API callback, press F12 and check the console log. </small></p>
 +
 +      </div>
 +    
 +    </section>
 +
 + 
 +
 +    <section id="footer">
 +
 +        <div class="container-fluid">
 +          <div class="row">
 +            <div class="col-md-6">
 +                <p>Copyright © <script type="text/javascript">document.write(new Date().getFullYear());</script>.</p>
 +            </div>
 +    
 +            <div class="col-md-6">
 +                <p>Designed and created by <a href="https://ctrlaltshift.nl" target="_blank">ctrlaltshift.nl</a>.</p>
 +            </div>
 +          </div>
 +    
 +        </div>
 +  
 +  
 +      </section>
 +
 +    <!-- Bootstrap Script  -->
 +    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
 +    <!-- Form Validation and Sending -->
 +    <script>
 +        (function() {
 +            'use strict';
 +            //console.log("Adding event listeners once the DOM has loaded for form validation and handling");
 +            window.addEventListener('load', function() {
 +            // Fetch all the forms we want to apply custom Bootstrap validation styles to
 +            var forms = document.getElementsByClassName('needs-validation');
 +            // Loop over them and prevent submission
 +            var validation = Array.prototype.filter.call(forms, function(form) {
 +                form.addEventListener('submit', function(event) {
 +                if (form.checkValidity() === false) {
 +                    event.preventDefault();
 +                    event.stopPropagation();
 +                }
 +                form.classList.add('was-validated');
 +                if (form.checkValidity() === true) {
 +                    console.log ("Start collecting form data and sending to backend for processing. ");
 +                    collectInputData(event);
 +                    event.preventDefault();
 +                }
 +                }, false);
 +            });
 +            }, false);
 +        })();
 +
 +        function collectInputData(event) {
 +            // Input
 +            var FormTicketNumber = $("#ticketNumber").val();
 +            // Collect all data into one array
 +            var inputData = {
 +                formTicketNumber: FormTicketNumber,
 +            };
 +            console.log("InputData is collected, start submit to the backend API");
 +            submitToAPI(inputData);
 +        }
 +
 +        // Used for contact
 +        function submitToAPI(inputData) {
 +            $.ajax({
 +                method: 'POST',
 +                url: 'https://xxxxxxxxxxxx.execute-api.eu-west-1.amazonaws.com/prod/case',
 +                dataType: "JSON",
 +                crossDomain: "true",
 +                data: JSON.stringify(inputData),
 +                contentType: 'application/json',
 +                success: completeTicketRequest,
 +                error: function ajaxError(jqXHR, textStatus, errorThrown) {
 +                    console.error('Error requesting call center ticket form: ', textStatus, ', Details: ', errorThrown);
 +                    console.error('Response: ', jqXHR.responseText);
 +                    alert('An error occured:\n' + jqXHR.responseText);
 +                }
 +            });
 +        }
 +
 +        function completeTicketRequest(result) {
 +            console.log('Response received from API: ', result);  
 +        }
 +
 +    </script>
 +
 +    </body>
 +    </html>
 +</code>
 +
 +> 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:
 +
 +<code yaml>
 +  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'
 +</code>
 +
 +And change the Lambda functions to use it:
 +<code yaml>
 +  OpenCaseFunction:
 +    Type: AWS::Serverless::Function 
 +    Properties:
 +      CodeUri: functions/open-case/
 +      Handler: app.handler 
 +      Runtime: nodejs12.x
 +      Role: !GetAtt CallCenterBasicLambdaRole.Arn
 +</code>
 +
 +> 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:
 +
 +<code yaml>
 +  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'
 +</code>
 +
 +And this also needs to be updated in the state machine:
 +<code yaml>
 +  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
 +</code>
 +
 +> 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::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
 +</code>
 +
 +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: !GetAtt CallCenterStateMachineAPIEventRole.Arn
 +                credentials: !GetAtt CallCenterAPIGatewayRole.Arn
 +</code>
 +
 +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 <samstackname>-<statemachinename>APIEventRole-<randomstringof12chars>. This one will have the exact same policy as the role we created above, though named different, and can be deleted. 
 +
 +> Posted this on [[https://stackoverflow.com/questions/64034602/why-is-my-sam-stepfunctions-state-machine-still-creating-the-implicit-api-event|Stack Overflow]] as well. 
 +
 +== 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:
 +<code>
 +- 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
 +</code>
 +
 +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::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]  
 +</code>
 +
 +And the following to the properties of the state machine resource:
 +<code yaml>
 +      Logging:
 +        Destinations:
 +          - CloudWatchLogsLogGroup: 
 +              LogGroupArn: !GetAtt CallCenterStateMachineLogGroup.Arn
 +        IncludeExecutionData: true
 +        Level: ALL
 +</code>
 +
 +> 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:
 +
 +<code yaml>
 +      MethodSettings:
 +        - LoggingLevel: INFO # OFF, ERROR, INFO
 +          ResourcePath: '/*' # allows for logging on any resource
 +          HttpMethod: '*' # allows for logging on any method
 +</code>
 +
 +==== 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:
 +<code yaml>
 +  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'
 +</code>
 +
 +And we need to define an extra resource of APIGateway Account type:
 +
 +<code yaml>
 +  CallCenterAPIGatewayAccount:
 +    Type: AWS::ApiGateway::Account 
 +    Properties: 
 +      CloudWatchRoleArn: !GetAtt CallCenterAPIGatewayAccountRole.Arn
 +</code>
 +
 +> 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:
 +
 +<code yaml>
 +  CallCenterWebAppBucket:
 +    Type: AWS::S3::Bucket
 +    DeletionPolicy: Retain
 +    Properties:
 +      BucketName: !FindInMap [AccountMap, !Ref "AWS::AccountId", s3bucketname]
 +      AccessControl: PublicRead
 +      WebsiteConfiguration:
 +        IndexDocument: index.html
 +        ErrorDocument: error.html
 +</code>
 +
 +And this as the mapping: 
 +
 +<code yaml>
 +Mappings: 
 +  AccountMap: #Different settings based on the AWS Account to which is being deployed
 +    "xxxxxxxxxxxxx":
 +      "s3bucketname": "callcentertestbucket"
 +    "xxxxxxxxxxxxx":
 +      "s3bucketname": "callcenterprodbucket"
 +</code>
 +
 +> Notice that upon deletion of the stack the s3 bucket is now being retained, resulting in the error "<bucketname> already exists". To prevent this, either remove the S3 section from the template.yaml or remove the s3 bucket manually. 
 +
 += End Result =
 +
 +The total template.yaml:
 +<code yaml | template.yaml>
 +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"
 +</code>
 +
 += 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. 
 +
 +<code yaml>
 +  # 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
 +</code>
 +
 += Resources =
 +
 +* [[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}}
samwebapp.txt · Last modified: 2021/09/24 00:25 (external edit)