├── .github └── workflows │ └── pipeline.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── StepFunctions_Paths.png ├── sample_payload.json └── stepfunctions_graph.png ├── functions ├── check-address │ ├── app.js │ └── package.json └── check-identity │ ├── app.js │ └── package.json ├── statemachine └── application_service.asl.json └── template.yaml /.github/workflows/pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'feature**' 8 | 9 | env: 10 | PIPELINE_USER_ACCESS_KEY_ID: ${{ secrets.PIPELINE_USER_ACCESS_KEY }} 11 | PIPELINE_USER_SECRET_ACCESS_KEY: ${{ secrets.PIPELINE_USER_SECRET_KEY }} 12 | PIPELINE_EMAIL_PARAMETER: 'ParameterKey=Email,ParameterValue=${{ secrets.PIPELINE_EMAIL_PARAMETER }}' 13 | SAM_TEMPLATE: template.yaml 14 | TESTING_STACK_NAME: serverless-account-signup-service-dev 15 | TESTING_PIPELINE_EXECUTION_ROLE: arn:aws:iam::974751372104:role/aws-sam-cli-managed-dev-pipe-PipelineExecutionRole-Q9A7G4LOX5BN 16 | TESTING_CLOUDFORMATION_EXECUTION_ROLE: arn:aws:iam::974751372104:role/aws-sam-cli-managed-dev-p-CloudFormationExecutionR-18LQAK4926GWR 17 | TESTING_ARTIFACTS_BUCKET: aws-sam-cli-managed-dev-pipeline-artifactsbucket-euleen5obg94 18 | # If there are functions with "Image" PackageType in your template, 19 | # uncomment the line below and add "--image-repository ${TESTING_IMAGE_REPOSITORY}" to 20 | # testing "sam package" and "sam deploy" commands. 21 | # TESTING_IMAGE_REPOSITORY = '0123456789.dkr.ecr.region.amazonaws.com/repository-name' 22 | TESTING_REGION: us-east-2 23 | PROD_STACK_NAME: serverless-account-signup-service-prod 24 | PROD_PIPELINE_EXECUTION_ROLE: arn:aws:iam::974751372104:role/aws-sam-cli-managed-prod-pip-PipelineExecutionRole-1F7W1Q6GKOILR 25 | PROD_CLOUDFORMATION_EXECUTION_ROLE: arn:aws:iam::974751372104:role/aws-sam-cli-managed-prod-CloudFormationExecutionR-S8DG6OJ0JWPL 26 | PROD_ARTIFACTS_BUCKET: aws-sam-cli-managed-prod-pipeline-artifactsbucket-1kutudaamomvl 27 | # If there are functions with "Image" PackageType in your template, 28 | # uncomment the line below and add "--image-repository ${PROD_IMAGE_REPOSITORY}" to 29 | # prod "sam package" and "sam deploy" commands. 30 | # PROD_IMAGE_REPOSITORY = '0123456789.dkr.ecr.region.amazonaws.com/repository-name' 31 | PROD_REGION: us-east-2 32 | 33 | jobs: 34 | test: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - run: | 39 | # trigger the tests here 40 | 41 | build-and-deploy-feature: 42 | # this stage is triggered only for feature branches (feature*), 43 | # which will build the stack and deploy to a stack named with branch name. 44 | if: startsWith(github.ref, 'refs/heads/feature') 45 | needs: [test] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v2 49 | - uses: actions/setup-python@v2 50 | - uses: aws-actions/setup-sam@v1 51 | - run: sam build --template ${SAM_TEMPLATE} --use-container 52 | 53 | - name: Assume the testing pipeline user role 54 | uses: aws-actions/configure-aws-credentials@v1 55 | with: 56 | aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} 57 | aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} 58 | aws-region: ${{ env.TESTING_REGION }} 59 | role-to-assume: ${{ env.TESTING_PIPELINE_EXECUTION_ROLE }} 60 | role-session-name: feature-deployment 61 | role-duration-seconds: 3600 62 | role-skip-session-tagging: true 63 | 64 | - name: Deploy to feature stack in the testing account 65 | shell: bash 66 | run: | 67 | sam deploy --stack-name $(echo ${GITHUB_REF##*/} | tr -cd '[a-zA-Z0-9-]') \ 68 | --capabilities CAPABILITY_IAM \ 69 | --region ${TESTING_REGION} \ 70 | --s3-bucket ${TESTING_ARTIFACTS_BUCKET} \ 71 | --no-fail-on-empty-changeset \ 72 | --role-arn ${TESTING_CLOUDFORMATION_EXECUTION_ROLE} \ 73 | --parameter-overrides ${PIPELINE_EMAIL_PARAMETER} 74 | 75 | build-and-package: 76 | if: github.ref == 'refs/heads/main' 77 | needs: [test] 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v2 81 | - uses: actions/setup-python@v2 82 | - uses: aws-actions/setup-sam@v1 83 | 84 | - name: Build resources 85 | run: sam build --template ${SAM_TEMPLATE} --use-container 86 | 87 | - name: Assume the testing pipeline user role 88 | uses: aws-actions/configure-aws-credentials@v1 89 | with: 90 | aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} 91 | aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} 92 | aws-region: ${{ env.TESTING_REGION }} 93 | role-to-assume: ${{ env.TESTING_PIPELINE_EXECUTION_ROLE }} 94 | role-session-name: testing-packaging 95 | role-duration-seconds: 3600 96 | role-skip-session-tagging: true 97 | 98 | - name: Upload artifacts to testing artifact buckets 99 | run: | 100 | sam package \ 101 | --s3-bucket ${TESTING_ARTIFACTS_BUCKET} \ 102 | --region ${TESTING_REGION} \ 103 | --output-template-file packaged-testing.yaml 104 | 105 | - uses: actions/upload-artifact@v2 106 | with: 107 | name: packaged-testing.yaml 108 | path: packaged-testing.yaml 109 | 110 | - name: Assume the prod pipeline user role 111 | uses: aws-actions/configure-aws-credentials@v1 112 | with: 113 | aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} 114 | aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} 115 | aws-region: ${{ env.PROD_REGION }} 116 | role-to-assume: ${{ env.PROD_PIPELINE_EXECUTION_ROLE }} 117 | role-session-name: prod-packaging 118 | role-duration-seconds: 3600 119 | role-skip-session-tagging: true 120 | 121 | - name: Upload artifacts to production artifact buckets 122 | run: | 123 | sam package \ 124 | --s3-bucket ${PROD_ARTIFACTS_BUCKET} \ 125 | --region ${PROD_REGION} \ 126 | --output-template-file packaged-prod.yaml 127 | 128 | - uses: actions/upload-artifact@v2 129 | with: 130 | name: packaged-prod.yaml 131 | path: packaged-prod.yaml 132 | 133 | deploy-testing: 134 | if: github.ref == 'refs/heads/main' 135 | needs: [build-and-package] 136 | runs-on: ubuntu-latest 137 | steps: 138 | - uses: actions/checkout@v2 139 | - uses: actions/setup-python@v2 140 | - uses: aws-actions/setup-sam@v1 141 | - uses: actions/download-artifact@v4.1.7 142 | with: 143 | name: packaged-testing.yaml 144 | 145 | - name: Assume the testing pipeline user role 146 | uses: aws-actions/configure-aws-credentials@v1 147 | with: 148 | aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} 149 | aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} 150 | aws-region: ${{ env.TESTING_REGION }} 151 | role-to-assume: ${{ env.TESTING_PIPELINE_EXECUTION_ROLE }} 152 | role-session-name: testing-deployment 153 | role-duration-seconds: 3600 154 | role-skip-session-tagging: true 155 | 156 | - name: Deploy to testing account 157 | run: | 158 | sam deploy --stack-name ${TESTING_STACK_NAME} \ 159 | --template packaged-testing.yaml \ 160 | --capabilities CAPABILITY_IAM \ 161 | --region ${TESTING_REGION} \ 162 | --s3-bucket ${TESTING_ARTIFACTS_BUCKET} \ 163 | --no-fail-on-empty-changeset \ 164 | --role-arn ${TESTING_CLOUDFORMATION_EXECUTION_ROLE} \ 165 | --parameter-overrides ${PIPELINE_EMAIL_PARAMETER} 166 | 167 | integration-test: 168 | if: github.ref == 'refs/heads/main' 169 | needs: [deploy-testing] 170 | runs-on: ubuntu-latest 171 | steps: 172 | - uses: actions/checkout@v2 173 | - run: | 174 | # trigger the integration tests here 175 | 176 | deploy-prod: 177 | if: github.ref == 'refs/heads/main' 178 | needs: [integration-test] 179 | runs-on: ubuntu-latest 180 | # Configure GitHub Action Environment to have a manual approval step before deployment to production 181 | # https://docs.github.com/en/actions/reference/environments 182 | # environment: 183 | steps: 184 | - uses: actions/checkout@v2 185 | - uses: actions/setup-python@v2 186 | - uses: aws-actions/setup-sam@v1 187 | - uses: actions/download-artifact@v4.1.7 188 | with: 189 | name: packaged-prod.yaml 190 | 191 | - name: Assume the prod pipeline user role 192 | uses: aws-actions/configure-aws-credentials@v1 193 | with: 194 | aws-access-key-id: ${{ env.PIPELINE_USER_ACCESS_KEY_ID }} 195 | aws-secret-access-key: ${{ env.PIPELINE_USER_SECRET_ACCESS_KEY }} 196 | aws-region: ${{ env.PROD_REGION }} 197 | role-to-assume: ${{ env.PROD_PIPELINE_EXECUTION_ROLE }} 198 | role-session-name: prod-deployment 199 | role-duration-seconds: 3600 200 | role-skip-session-tagging: true 201 | 202 | - name: Deploy to production account 203 | run: | 204 | sam deploy --stack-name ${PROD_STACK_NAME} \ 205 | --template packaged-prod.yaml \ 206 | --capabilities CAPABILITY_IAM \ 207 | --region ${PROD_REGION} \ 208 | --s3-bucket ${PROD_ARTIFACTS_BUCKET} \ 209 | --no-fail-on-empty-changeset \ 210 | --role-arn ${PROD_CLOUDFORMATION_EXECUTION_ROLE} \ 211 | --parameter-overrides ${PIPELINE_EMAIL_PARAMETER} 212 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,node,linux,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### Node ### 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | *.pid.lock 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (http://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Typescript v1 declaration files 59 | typings/ 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | 79 | 80 | ### OSX ### 81 | *.DS_Store 82 | .AppleDouble 83 | .LSOverride 84 | 85 | # Icon must end with two \r 86 | Icon 87 | 88 | # Thumbnails 89 | ._* 90 | 91 | # Files that might appear in the root of a volume 92 | .DocumentRevisions-V100 93 | .fseventsd 94 | .Spotlight-V100 95 | .TemporaryItems 96 | .Trashes 97 | .VolumeIcon.icns 98 | .com.apple.timemachine.donotpresent 99 | 100 | # Directories potentially created on remote AFP share 101 | .AppleDB 102 | .AppleDesktop 103 | Network Trash Folder 104 | Temporary Items 105 | .apdisk 106 | 107 | ### Windows ### 108 | # Windows thumbnail cache files 109 | Thumbs.db 110 | ehthumbs.db 111 | ehthumbs_vista.db 112 | 113 | # Folder config file 114 | Desktop.ini 115 | 116 | # Recycle Bin used on file shares 117 | $RECYCLE.BIN/ 118 | 119 | # Windows Installer files 120 | *.cab 121 | *.msi 122 | *.msm 123 | *.msp 124 | 125 | # Windows shortcuts 126 | *.lnk 127 | 128 | 129 | # End of https://www.gitignore.io/api/osx,node,linux,windows 130 | samconfig.toml 131 | .aws-sam 132 | assets/StepFunctions_Paths.drawio -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to use JSON Path in Step Functions 2 | 3 | This application shows how to build a Standard or an Express Step Functions workflow by writing minimal code and leveraging JSONPath. [JSONPath](https://goessner.net/articles/JsonPath/) is XPath for JSON. We will see in detail how JSONPath helps while developing Step Functions. 4 | 5 | ## Application Use Case 6 | The example application used in this case is based on insurance domain. New customers shop for insurances and apply for a new account providing their basic information and their interests in the types of insurances (home, auto, boat, etc). 7 | 8 | The information provided by an insurance customer is taken as an input to this Step Functions workflow. Step Function is responsible for doing below operations on the input payload in this order: 9 | 10 | 1. Verify identity of the user 11 | 2. Verify address of the user 12 | 3. Approve the new account application if above checks pass 13 | 4. Deny the new account application if any of the above checks fail 14 | 5. Upon approval: 15 | a. insert user information in DynamoDB Accounts Table 16 | b. check "home" insurance interests and put them in a queue to be processed by a different application 17 | c. send an email notification to user about application approval 18 | 6. Upon deny: 19 | a. send an email notification to user about application denial 20 | 21 | ### How does it look in Step Functions 22 | ![Workflow](./assets/stepfunctions_graph.png) 23 | 24 | Now, this Step Function can be a standard workflow or a synchronous express workflow according to your need. What we are going to look at is the flexibility of handling payload across state transitions. 25 | 26 | Here is a sample payload: 27 | 28 | ```json 29 | { 30 | "data": { 31 | "firstname": "Jane", 32 | "lastname": "Doe", 33 | "identity": { 34 | "email": "jdoe@example.com", 35 | "ssn": "123-45-6789" 36 | }, 37 | "address": { 38 | "street": "123 Main St", 39 | "city": "Columbus", 40 | "state": "OH", 41 | "zip": "43219" 42 | }, 43 | "interests": [ 44 | {"category": "home", "type": "own", "yearBuilt": 2004 }, 45 | {"category": "auto", "type": "car", "yearBuilt": 2012 }, 46 | {"category": "boat", "type": "snowmobile", "yearBuilt": 2020 }, 47 | {"category": "auto", "type": "motorcycle", "yearBuilt": 2018 }, 48 | {"category": "auto", "type": "RV", "yearBuilt": 2015 }, 49 | {"category": "home", "type": "business", "yearBuilt": 2009 } 50 | ] 51 | } 52 | } 53 | ``` 54 | 55 | The actual insurance logic for new user account application may be different and sophisticated but for the sake of simplicity we are considering the above payload which has `data` about the new user and user's `interests` in the types of insurance from the insurance provider 56 | 57 | Before we jump into the State Machine, let's look at how Step Functions use JSONPath for the Paths that are natively available in Step Functions 58 | 59 | ### Paths in Step Function 60 | ![Paths](./assets/StepFunctions_Paths.png) 61 | 62 | The main fields that filter and control the flow from state to state in Amazon States Language are: 63 | 64 | - `InputPath` Determines "WHAT" a task needs as input 65 | - `Parameters` Determines "HOW" the input should look like before invoking the task 66 | - `ResultSelector` Determines "WHAT to choose" from task's output 67 | - `ResultPath` Determines "WHERE to put" the chosen task's output 68 | - `OutputPath` Determines "WHAT to send to next" state 69 | 70 | The key thing to focus here is that downstream states depend on what their previous states provide/feed them with. 71 | 72 | Let's take the sample payload shown above and apply those paths. You can do the same with [Data flow Simulator present in Step Functions AWS Console](https://us-east-2.console.aws.amazon.com/states/home?region=us-east-2#/simulator): 73 | 74 | `"InputPath": '$.data.address'` makes below JSON available as input for the task 75 | ```json 76 | { 77 | "street": "123 Main St", 78 | "city": "Columbus", 79 | "state": "OH", 80 | "zip": "43219" 81 | } 82 | ``` 83 | `Parameters` are applied after `InputPath` which is used as a mechanism as to HOW the underlying task accepts its input payload. For example, in the below example `Parameters` receive the payload that has been provided by `InputPath` above and then applies an intrinsic function `States.Format` on the payload items to create a string as shown below. 84 | 85 | ```json 86 | "Parameters": { 87 | "addressString.$": "States.Format('{}, {} - {}', $.city, $.state, $.zip)" 88 | } 89 | ``` 90 | 91 | ```json 92 | { 93 | "addressString": "Columbus, OH - 43219" 94 | } 95 | ``` 96 | 97 | The underlying task (Lambda or DDB or SQS or SNS , etc) might be accepting a parameter `addressString` instead of an address json object. Here, `Parameters` provide the capability to focus on "HOW" the input should look like before invoking the task. 98 | 99 | Once the task is invoked, let's assume that this is an AWS Lambda integration, which validates the address and returns the output payload as address approved and would look something like this: 100 | 101 | ```json 102 | { 103 | "ExecutedVersion": "$LATEST", 104 | "Payload": { 105 | "statusCode": "200", 106 | "body": "{\"approved\": true}" 107 | }, 108 | "SdkHttpMetadata": { 109 | "HttpHeaders": { 110 | "Connection": "keep-alive", 111 | "Content-Length": "43", 112 | "Content-Type": "application/json", 113 | "Date": "Thu, 16 Apr 2020 17:58:15 GMT", 114 | "X-Amz-Executed-Version": "$LATEST", 115 | "x-amzn-Remapped-Content-Length": "0", 116 | "x-amzn-RequestId": "88fba57b-adbe-467f-abf4-daca36fc9028", 117 | "X-Amzn-Trace-Id": "root=1-5e989cb6-90039fd8971196666b022b62;sampled=0" 118 | }, 119 | "HttpStatusCode": 200 120 | }, 121 | "SdkResponseMetadata": { 122 | "RequestId": "88fba57b-adbe-467f-abf4-daca36fc9028" 123 | }, 124 | "StatusCode": 200 125 | } 126 | ``` 127 | 128 | Now, this Address validation approval has to be provided to the downstream states for additional business logic. However, the downstream states do not care about anything else other than `Payload.body` from above json. That is where a combination of intrinsic function and `ResultSelector` becomes useful. 129 | 130 | ```json 131 | "ResultSelector": { 132 | "identity.$": "States.StringToJson($.Payload.body)" 133 | } 134 | ``` 135 | 136 | `ResultSelector` is responsible to pickup "WHAT" is needed from the task output. Here it takes the json string `$.Payload.body` and applies `States.StringToJson` to convert string to json and put the json under `identity` 137 | 138 | ```json 139 | { 140 | "identity": { 141 | "approved": true 142 | } 143 | } 144 | ``` 145 | 146 | Now, the next question will be WHERE should the above result go to in the initial payload so that the downstream states will have access to the actual input payload plus the results from the previous states. That is where `ResultPath` comes into picture. 147 | 148 | `ResultPath: "$.results"` informs state machine that any result selected from the task output (actual output if none specified) should go under `results` and `results` should get added to actual incoming payload. Thus, the output from `ResultPath` would look like: 149 | 150 | ```json 151 | { 152 | "data": { 153 | "firstname": "Jane", 154 | "lastname": "Doe", 155 | "identity": { 156 | "email": "jdoe@example.com", 157 | "ssn": "123-45-6789" 158 | }, 159 | "address": { 160 | "street": "123 Main St", 161 | "city": "Columbus", 162 | "state": "OH", 163 | "zip": "43219" 164 | }, 165 | "interests": [ 166 | ... 167 | ] 168 | }, 169 | "results": { 170 | "identity": { 171 | "approved": true 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | The good thing about above json is not only that it has results from an operation but also that the incoming payload is intact for downstream business logic consumption. 178 | 179 | This pattern ensures that the previous state keeps the payload properly hydrated for the next state. 180 | 181 | A good pattern will follow the above until you reach an end state. In this sample app, you would see an end state with a Step output payload like this: 182 | 183 | ```json 184 | { 185 | "data": { 186 | "firstname": "Jane", 187 | "lastname": "Doe", 188 | "identity": { 189 | "email": "jdoe@example.com", 190 | "ssn": "123-45-6789" 191 | }, 192 | "address": { 193 | "street": "123 Main St", 194 | "city": "Columbus", 195 | "state": "OH", 196 | "zip": "43219" 197 | }, 198 | "interests": [ 199 | ... 200 | ] 201 | }, 202 | "results": { 203 | "addressResult": { 204 | "approved": true, 205 | "message": "address validation passed" 206 | }, 207 | "identityResult": { 208 | "approved": true, 209 | "message": "identity validation passed" 210 | }, 211 | "accountAddition": { 212 | "statusCode": 200 213 | }, 214 | "homeInsuranceInterests": { 215 | "statusCode": 200 216 | }, 217 | "sendApprovedNotification": { 218 | "statusCode": 200 219 | } 220 | } 221 | } 222 | ``` 223 | 224 | but in an ideal state scenario you would not want to send the input payload back to the caller of the step function. That is where you have the opportunity to use `OutputPath` which will just send the response or the results that acts as a response from the service 225 | 226 | `"OutputPath": "$.results"` would yield 227 | 228 | ```json 229 | { 230 | "addressResult": { 231 | "approved": true, 232 | "message": "address validation passed" 233 | }, 234 | "identityResult": { 235 | "approved": true, 236 | "message": "identity validation passed" 237 | }, 238 | "accountAddition": { 239 | "statusCode": 200 240 | }, 241 | "homeInsuranceInterests": { 242 | "statusCode": 200 243 | }, 244 | "sendApprovedNotification": { 245 | "statusCode": 200 246 | } 247 | } 248 | ``` 249 | 250 | Now, think of a Synchronous Express Workflow and the workflow sending above response back to caller. Less noise, more fine grained response 251 | 252 | ### Advanced JSON Path 253 | Let's checkout an advanced JSON path scenario. For this lets pick up the interests from the input payload. The state machine in this sample app focuses on filtering out `interests` of category `home`. In a real enterprise scenario, it makes a lot of sense. 'Home' insurance can be a different Business Unit than 'Auto' or 'Boat' insurance. Using advanced JSONPath, we can filter out `home` specific interests and either call a home insurance service, or put event on home insurance event bus using EventBridge, or put all home related interest in an `HomeInterestsQueue` where independent consumer applications can poll and work on it. 254 | 255 | This what this sample app has as `Home Insurance Interests` state: 256 | 257 | ```json 258 | ... 259 | "Home Insurance Interests": { 260 | "Type": "Task", 261 | "Resource": "arn:aws:states:::sqs:sendMessage", 262 | "InputPath": "$..interests[?(@.category==home)]", 263 | "Parameters": { 264 | "QueueUrl": "${HomeInsuranceInterestQueueArn}", 265 | "MessageBody.$": "$" 266 | }, 267 | "ResultSelector": { 268 | "statusCode.$": "$.SdkHttpMetadata.HttpStatusCode" 269 | }, 270 | "ResultPath": "$.results.homeInsuranceInterests", 271 | "Next": "Approved Message" 272 | } 273 | ... 274 | ``` 275 | 276 | `"InputPath": "$..interests[?(@.category==home)]"` filters all `home` insurance related interests 277 | 278 | ```json 279 | [ 280 | { 281 | "category": "home", 282 | "type": "own", 283 | "yearBuilt": "2004", 284 | "estimatedValue": "300000" 285 | }, 286 | { 287 | "category": "home", 288 | "type": "business", 289 | "yearBuilt": "2009", 290 | "estimatedValue": "450000" 291 | } 292 | ] 293 | ``` 294 | 295 | and adds the array as a message in an SQS Queue. 296 | 297 | 298 | It uses advanced JSONPath with `$..` notation and `[?(@.category==home)]` filters. 299 | 300 | Additional, information on JSONPath can be found [here](https://goessner.net/articles/JsonPath/) 301 | 302 | This filtering is not just applicable to `home` insurance interests but can be extended similarly to other categories and other business logic 303 | 304 | ## Other Resources 305 | In addition to this sample app and notes above, please also take a look at [the blog on Data flow Simulator](https://aws.amazon.com/blogs/compute/modeling-workflow-input-output-path-processing-with-data-flow-simulator/) 306 | 307 | ## Deploy the application 308 | To build and deploy your application for the first time, run the following in your shell: 309 | 310 | ```bash 311 | sam build && sam deploy --guided 312 | ``` 313 | 314 | Subsequent build and deploys can just be: 315 | ```bash 316 | sam build && sam deploy 317 | ``` 318 | 319 | ## Run the application 320 | To run the application, use below aws cli command. Replace the state machine arn from the output of deployment steps above. 321 | 322 | ```bash 323 | aws stepfunctions start-execution \ 324 | --state-machine-arn \ 325 | --input "{\"data\":{\"firstname\":\"Jane\",\"lastname\":\"Doe\",\"identity\":{\"email\":\"jdoe@example.com\",\"ssn\":\"123-45-6789\"},\"address\":{\"street\":\"123 Main St\",\"city\":\"Columbus\",\"state\":\"OH\",\"zip\":\"43219\"},\"interests\":[{\"category\":\"home\",\"type\":\"own\",\"yearBuilt\":2004},{\"category\":\"auto\",\"type\":\"car\",\"yearBuilt\":2012},{\"category\":\"boat\",\"type\":\"snowmobile\",\"yearBuilt\":2020},{\"category\":\"auto\",\"type\":\"motorcycle\",\"yearBuilt\":2018},{\"category\":\"auto\",\"type\":\"RV\",\"yearBuilt\":2015},{\"category\":\"home\",\"type\":\"business\",\"yearBuilt\":2009}]}}" 326 | ``` 327 | 328 | ## Cleanup 329 | 330 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: 331 | 332 | ```bash 333 | sam delete 334 | ``` 335 | 336 | ## GitHub Actions Integration 337 | This repo is integrated with GitHub actions 338 | -------------------------------------------------------------------------------- /assets/StepFunctions_Paths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-account-signup-service/c125cc19963ebef397b41fee1038bc28bfe67a7a/assets/StepFunctions_Paths.png -------------------------------------------------------------------------------- /assets/sample_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "firstname": "Jane", 4 | "lastname": "Doe", 5 | "identity": { 6 | "email": "jdoe@example.com", 7 | "ssn": "123-45-6789" 8 | }, 9 | "address": { 10 | "street": "123 Main St", 11 | "city": "Columbus", 12 | "state": "OH", 13 | "zip": "43219" 14 | }, 15 | "interests": [ 16 | {"category": "home", "type": "own", "yearBuilt": 2004}, 17 | {"category": "auto", "type": "car", "yearBuilt": 2012}, 18 | {"category": "boat", "type": "snowmobile", "yearBuilt": 2020}, 19 | {"category": "auto", "type": "motorcycle", "yearBuilt": 2018}, 20 | {"category": "auto", "type": "RV", "yearBuilt": 2015}, 21 | {"category": "home", "type": "business", "yearBuilt": 2009} 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /assets/stepfunctions_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-account-signup-service/c125cc19963ebef397b41fee1038bc28bfe67a7a/assets/stepfunctions_graph.png -------------------------------------------------------------------------------- /functions/check-address/app.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | exports.lambdaHandler = async event => { 7 | const { street, city, state, zip } = event; 8 | console.log(`Address information: ${street}, ${city}, ${state} - ${zip}`); 9 | 10 | const approved = [street, city, state, zip].every(i => i?.trim().length > 0); 11 | 12 | return { 13 | statusCode: 200, 14 | body: JSON.stringify({ 15 | approved, 16 | message: `address validation ${ approved ? 'passed' : 'failed'}` 17 | }) 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /functions/check-address/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check_address", 3 | "version": "1.0.0", 4 | "description": "Lambda Function to check address", 5 | "main": "app.js", 6 | "repository": "TODO", 7 | "author": "SAM CLI", 8 | "license": "MIT" 9 | } -------------------------------------------------------------------------------- /functions/check-identity/app.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | const ssnRegex = /^\d{3}-?\d{2}-?\d{4}$/; 7 | const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; 8 | 9 | exports.lambdaHandler = async event => { 10 | const { ssn, email } = event; 11 | console.log(`SSN: ${ssn} and email: ${email}`); 12 | 13 | const approved = ssnRegex.test(ssn) && emailRegex.test(email); 14 | 15 | return { 16 | statusCode: 200, 17 | body: JSON.stringify({ 18 | approved, 19 | message: `identity validation ${approved ? 'passed' : 'failed'}` 20 | }) 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /functions/check-identity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check_identity", 3 | "version": "1.0.0", 4 | "description": "Lambda Function to check Identity", 5 | "main": "app.js", 6 | "repository": "TODO", 7 | "author": "SAM CLI", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /statemachine/application_service.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "This is a state machine for account application service", 3 | "StartAt": "Verification", 4 | "States": { 5 | "Verification": { 6 | "Type": "Parallel", 7 | "Branches": [ 8 | { 9 | "StartAt": "Check Identity", 10 | "States": { 11 | "Check Identity": { 12 | "Type": "Task", 13 | "Resource": "arn:aws:states:::lambda:invoke", 14 | "InputPath": "$.data.identity", 15 | "Parameters": { 16 | "FunctionName": "${CheckIdentityFunctionArn}", 17 | "Payload.$": "$" 18 | }, 19 | "ResultSelector": { 20 | "identity.$": "States.StringToJson($.Payload.body)" 21 | }, 22 | "Retry": [ 23 | { 24 | "ErrorEquals": [ 25 | "Lambda.ServiceException", 26 | "Lambda.AWSLambdaException", 27 | "Lambda.SdkClientException" 28 | ], 29 | "IntervalSeconds": 2, 30 | "MaxAttempts": 2, 31 | "BackoffRate": 2 32 | } 33 | ], 34 | "End": true 35 | } 36 | } 37 | }, 38 | { 39 | "StartAt": "Check Address", 40 | "States": { 41 | "Check Address": { 42 | "Type": "Task", 43 | "Resource": "arn:aws:states:::lambda:invoke", 44 | "InputPath": "$.data.address", 45 | "Parameters": { 46 | "FunctionName": "${CheckAddressFunctionArn}", 47 | "Payload.$": "$" 48 | }, 49 | "ResultSelector": { 50 | "address.$": "States.StringToJson($.Payload.body)" 51 | }, 52 | "Retry": [ 53 | { 54 | "ErrorEquals": [ 55 | "Lambda.ServiceException", 56 | "Lambda.AWSLambdaException", 57 | "Lambda.SdkClientException" 58 | ], 59 | "IntervalSeconds": 2, 60 | "MaxAttempts": 2, 61 | "BackoffRate": 2 62 | } 63 | ], 64 | "End": true 65 | } 66 | } 67 | } 68 | ], 69 | "ResultSelector": { 70 | "identityResult.$": "$[0].identity", 71 | "addressResult.$": "$[1].address" 72 | }, 73 | "ResultPath": "$.results", 74 | "Next": "Approve or Deny" 75 | }, 76 | "Approve or Deny": { 77 | "Type": "Choice", 78 | "Choices": [ 79 | { 80 | "And": [ 81 | { 82 | "Variable": "$.results.identityResult.approved", 83 | "BooleanEquals": true 84 | }, 85 | { 86 | "Variable": "$.results.addressResult.approved", 87 | "BooleanEquals": true 88 | } 89 | ], 90 | "Next": "Add Account", 91 | "Comment": "Application Approved" 92 | } 93 | ], 94 | "Default": "Deny Message" 95 | }, 96 | "Add Account": { 97 | "Type": "Task", 98 | "Resource": "arn:aws:states:::dynamodb:putItem", 99 | "InputPath": "$.data", 100 | "Parameters": { 101 | "TableName": "${AccountsTable}", 102 | "Item": { 103 | "email": { 104 | "S.$": "$.identity.email" 105 | }, 106 | "firstname": { 107 | "S.$": "$.firstname" 108 | }, 109 | "lastname": { 110 | "S.$": "$.lastname" 111 | }, 112 | "address": { 113 | "S.$": "States.Format('{}, {}, {} - {}', $.address.street, $.address.city, $.address.state, $.address.zip)" 114 | } 115 | } 116 | }, 117 | "ResultSelector": { 118 | "statusCode.$": "$.SdkHttpMetadata.HttpStatusCode" 119 | }, 120 | "ResultPath": "$.results.accountAddition", 121 | "Next": "Home Insurance Interests" 122 | }, 123 | "Home Insurance Interests": { 124 | "Type": "Task", 125 | "Resource": "arn:aws:states:::sqs:sendMessage", 126 | "InputPath": "$..interests[?(@.category==home)]", 127 | "Parameters": { 128 | "QueueUrl": "${HomeInsuranceInterestQueueArn}", 129 | "MessageBody.$": "$" 130 | }, 131 | "ResultSelector": { 132 | "statusCode.$": "$.SdkHttpMetadata.HttpStatusCode" 133 | }, 134 | "ResultPath": "$.results.homeInsuranceInterests", 135 | "Next": "Approved Message" 136 | }, 137 | "Approved Message": { 138 | "Type": "Task", 139 | "Resource": "arn:aws:states:::sns:publish", 140 | "Parameters": { 141 | "TopicArn": "${SendCustomerNotificationSNSTopicArn}", 142 | "Message.$": "States.Format('Hello {}, your application has been approved.', $.data.firstname)" 143 | }, 144 | "ResultSelector": { 145 | "statusCode.$": "$.SdkHttpMetadata.HttpStatusCode" 146 | }, 147 | "ResultPath": "$.results.sendApprovedNotification", 148 | "OutputPath": "$.results", 149 | "End": true 150 | }, 151 | "Deny Message": { 152 | "Type": "Task", 153 | "Resource": "arn:aws:states:::sns:publish", 154 | "Parameters": { 155 | "TopicArn": "${SendCustomerNotificationSNSTopicArn}", 156 | "Message.$": "States.Format('Hello {}, your application has been denied because validation of provided data failed', $.data.firstname)" 157 | }, 158 | "ResultSelector": { 159 | "statusCode.$": "$.SdkHttpMetadata.HttpStatusCode" 160 | }, 161 | "ResultPath": "$.results.sendDenyNotification", 162 | "OutputPath": "$.results", 163 | "End": true 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | serverless-account-signup-service 5 | 6 | Parameters: 7 | Email: 8 | Type: String 9 | Description: A valid email that will be subscribed to the SNS topic for approval or deny notifications 10 | AllowedPattern: "^(.+)@(\\S+)$" 11 | 12 | Resources: 13 | NewAccountApplicationStateMachine: 14 | Type: AWS::Serverless::StateMachine 15 | Properties: 16 | DefinitionUri: statemachine/application_service.asl.json 17 | DefinitionSubstitutions: 18 | CheckIdentityFunctionArn: !GetAtt CheckIdentityFunction.Arn 19 | CheckAddressFunctionArn: !GetAtt CheckAddressFunction.Arn 20 | AccountsTable: !Ref AccountsTable 21 | SendCustomerNotificationSNSTopicArn: !Ref SendCustomerNotificationSNSTopic 22 | HomeInsuranceInterestQueueArn: !Ref HomeInsuranceInterestQueue 23 | Policies: 24 | - LambdaInvokePolicy: 25 | FunctionName: !Ref CheckIdentityFunction 26 | - LambdaInvokePolicy: 27 | FunctionName: !Ref CheckAddressFunction 28 | - DynamoDBWritePolicy: 29 | TableName: !Ref AccountsTable 30 | - SNSPublishMessagePolicy: 31 | TopicName: !GetAtt SendCustomerNotificationSNSTopic.TopicName 32 | - SQSSendMessagePolicy: 33 | QueueName: !GetAtt HomeInsuranceInterestQueue.QueueName 34 | 35 | CheckIdentityFunction: 36 | Type: AWS::Serverless::Function 37 | Properties: 38 | CodeUri: functions/check-identity/ 39 | Handler: app.lambdaHandler 40 | Runtime: nodejs14.x 41 | 42 | CheckIdentityFunctionLogGroup: 43 | Type: AWS::Logs::LogGroup 44 | Properties: 45 | LogGroupName: !Sub "/aws/lambda/${CheckIdentityFunction}" 46 | RetentionInDays: 7 47 | 48 | CheckAddressFunction: 49 | Type: AWS::Serverless::Function 50 | Properties: 51 | CodeUri: functions/check-address/ 52 | Handler: app.lambdaHandler 53 | Runtime: nodejs14.x 54 | 55 | CheckAddressFunctionLogGroup: 56 | Type: AWS::Logs::LogGroup 57 | Properties: 58 | LogGroupName: !Sub "/aws/lambda/${CheckAddressFunction}" 59 | RetentionInDays: 7 60 | 61 | AccountsTable: 62 | Type: AWS::Serverless::SimpleTable 63 | Properties: 64 | PrimaryKey: 65 | Name: email 66 | Type: String 67 | ProvisionedThroughput: 68 | ReadCapacityUnits: 1 69 | WriteCapacityUnits: 1 70 | 71 | SendCustomerNotificationSNSTopic: 72 | Type: AWS::SNS::Topic 73 | Properties: 74 | Subscription: 75 | - Endpoint: !Ref Email 76 | Protocol: email 77 | 78 | HomeInsuranceInterestQueue: 79 | Type: AWS::SQS::Queue 80 | 81 | Outputs: 82 | NewAccountApplicationStateMachine: 83 | Description: "New Account Application State Machine ARN" 84 | Value: !Ref NewAccountApplicationStateMachine --------------------------------------------------------------------------------