├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── auth.yaml ├── images ├── Serverless_Application_Model_A.png └── Serverless_Application_Model_B.png ├── product-mock-service ├── __init__.py ├── get_product.py ├── get_products.py ├── product_list.json └── requirements.txt ├── product-mock.yaml ├── sam_stacks ├── requirements.txt └── shared.py ├── samconfig.toml ├── shopping-cart-service ├── __init__.py ├── add_to_cart.py ├── checkout_cart.py ├── db_stream_handler.py ├── delete_from_cart.py ├── get_cart_total.py ├── list_cart.py ├── migrate_cart.py ├── requirements.txt ├── tests │ ├── __init__.py │ └── test_example.py ├── update_cart.py └── utils.py ├── shoppingcart-service.yaml └── template.yml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 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 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | 246 | 247 | # pipenv 248 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 249 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 250 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 251 | # install all needed dependencies. 252 | #Pipfile.lock 253 | 254 | # celery beat schedule file 255 | celerybeat-schedule 256 | 257 | 258 | # mypy 259 | .dmypy.json 260 | dmypy.json 261 | 262 | # Pyre type checker 263 | .pyre/ 264 | 265 | /packaged-*.yml 266 | 267 | 268 | .idea 269 | .idea/* 270 | 271 | # Build files 272 | .aws-sam -------------------------------------------------------------------------------- /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 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Serverless Application Model (SAM) Nested Stack Sample 2 | [Amazon Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) is an open-source framework for building serverless applications. It provides shorthand syntax to express functions, APIs, databases, and event source mappings. With just a few lines per resource, you can define the application you want and model it using YAML. During deployment, SAM transforms and expands the SAM syntax into AWS CloudFormation syntax, enabling you to build serverless applications faster. 3 | 4 | This code uses AWS SAM templates to automate the deployment of nested applications. A nested application is an application within another application. Parent applications call their child applications. These are loosely coupled components of a serverless architecture. Using nested applications, you can rapidly build highly sophisticated serverless architectures by reusing services or components that are independently authored and maintained but are composed using AWS SAM and the Serverless Application Repository. Nested applications help you to build applications that are more powerful, avoid duplicated work, and ensure consistency and best practices across your teams and organizations. To demonstrate nested applications, the pattern deploys a sample AWS serverless shopping cart application. 5 | 6 | ## Target architecture 7 | 8 | 9 | ![SAM-A](./images/Serverless_Application_Model_A.png) 10 | 11 | In this solution setup, AWS SAM CLI serves as the interface for AWS CloudFormation stacks. AWS SAM templates automatically deploy nested applications. The parent SAM template calls the child templates, and the parent CloudFormation stack deploys the child stacks. Each child stack builds the AWS resources that are defined in the AWS SAM CloudFormation templates 12 | 13 | ![SAM-B](./images/Serverless_Application_Model_B.png) 14 | 15 | ## Prerequisites 16 | 17 | 1. An active AWS account 18 | 19 | 2. An existing virtual private cloud (VPC) and subnets 20 | 21 | 3. Python wheel library installed using pip install wheel, if it’s not already installed 22 | 23 | 24 | ## Deployment Steps 25 | 26 | Check out this APG Pattern for detailed deployment instructions: [Automate deployment of nested applications using AWS SAM](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/automate-deployment-of-nested-applications-using-aws-sam.html?did=pg_card&trk=pg_card) 27 | 28 | ### 1. Deploy the applications. 29 | 30 | To launch the SAM template code that creates the nested application CloudFormation stacks and deploys code in the AWS environment, run the following command: 31 | 32 | ``` 33 | $sam deploy --guided --stack-name shopping-cart-nested-stack --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND 34 | 35 | ``` 36 | 37 | ### 2. Verify the deployment 38 | 39 | To review and verify the AWS CloudFormation stacks and all AWS resources that were defined in the AWS SAM templates, log in to the AWS Management Console. For more information, see the Additional information section. 40 | 41 | 42 | ### 3. Clean up 43 | To clean up the resources, run the command: 44 | 45 | ``` 46 | $sam delete 47 | 48 | ``` 49 | 50 | ## Useful commands 51 | 52 | * `sam build` Builds a serverless application and prepares it for subsequent steps 53 | * `sam deploy` Deploys an AWS SAM application. 54 | * `sam init` Initializes a serverless application with an AWS SAM template 55 | * `sam logs` Fetches logs that are generated by your Lambda function. 56 | * `sam package` Packages an AWS SAM application. 57 | * `sam publish` Publish an AWS SAM application to the AWS Serverless Application Repository 58 | * `sam validate` Verifies whether an AWS SAM template file is valid. 59 | 60 | ## Security 61 | 62 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 63 | 64 | 65 | ## License 66 | 67 | This project is licensed under the Apache-2.0 License. 68 | 69 | -------------------------------------------------------------------------------- /auth.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | auth-resources 5 | 6 | SAM Template for auth resources 7 | 8 | 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | 13 | Resources: 14 | CognitoUserPool: 15 | Type: AWS::Cognito::UserPool 16 | Properties: 17 | UserPoolName: !Sub ${AWS::StackName}-UserPool 18 | AutoVerifiedAttributes: 19 | - email 20 | UserPoolClient: 21 | Type: AWS::Cognito::UserPoolClient 22 | Properties: 23 | ClientName: my-app 24 | GenerateSecret: false 25 | UserPoolId: !Ref CognitoUserPool 26 | ExplicitAuthFlows: 27 | - ADMIN_NO_SRP_AUTH 28 | 29 | UserPoolSSM: 30 | Type: AWS::SSM::Parameter 31 | Properties: 32 | Type: String 33 | Name: /serverless-shopping-cart-demo/auth/user-pool-id 34 | Value: !Ref CognitoUserPool 35 | 36 | UserPoolARNSSM: 37 | Type: AWS::SSM::Parameter 38 | Properties: 39 | Type: String 40 | Name: /serverless-shopping-cart-demo/auth/user-pool-arn 41 | Value: !GetAtt CognitoUserPool.Arn 42 | 43 | UserPoolAppClientSSM: 44 | Type: AWS::SSM::Parameter 45 | Properties: 46 | Type: String 47 | Name: /serverless-shopping-cart-demo/auth/user-pool-client-id 48 | Value: !Ref UserPoolClient 49 | 50 | Outputs: 51 | CognitoUserPoolId: 52 | Description: "Cognito User Pool ID" 53 | Value: !Ref CognitoUserPool 54 | 55 | CognitoAppClientId: 56 | Description: "Cognito App Client ID" 57 | Value: !Ref UserPoolClient 58 | 59 | UserPoolARNSSM: 60 | Description: "UserPool ID" 61 | Value: !Ref UserPoolARNSSM -------------------------------------------------------------------------------- /images/Serverless_Application_Model_A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-sam-nested-stack-sample/86e635bcdc96d1520782ba5b0d40e7e0b9f4ada0/images/Serverless_Application_Model_A.png -------------------------------------------------------------------------------- /images/Serverless_Application_Model_B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-sam-nested-stack-sample/86e635bcdc96d1520782ba5b0d40e7e0b9f4ada0/images/Serverless_Application_Model_B.png -------------------------------------------------------------------------------- /product-mock-service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-sam-nested-stack-sample/86e635bcdc96d1520782ba5b0d40e7e0b9f4ada0/product-mock-service/__init__.py -------------------------------------------------------------------------------- /product-mock-service/get_product.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from aws_lambda_powertools import Logger, Tracer 5 | 6 | logger = Logger() 7 | tracer = Tracer() 8 | 9 | with open("product_list.json", "r") as product_list: 10 | product_list = json.load(product_list) 11 | 12 | HEADERS = { 13 | "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN"), 14 | "Access-Control-Allow-Headers": "Content-Type", 15 | "Access-Control-Allow-Methods": "OPTIONS,POST,GET", 16 | } 17 | 18 | 19 | @logger.inject_lambda_context(log_event=True) 20 | @tracer.capture_lambda_handler 21 | def lambda_handler(event, context): 22 | """ 23 | Return single product based on path parameter. 24 | """ 25 | path_params = event["pathParameters"] 26 | product_id = path_params.get("product_id") 27 | logger.debug("Retriving product_id: %s", product_id) 28 | product = next( 29 | (item for item in product_list if item["productId"] == product_id), None 30 | ) 31 | 32 | return { 33 | "statusCode": 200, 34 | "headers": HEADERS, 35 | "body": json.dumps({"product": product}), 36 | } 37 | -------------------------------------------------------------------------------- /product-mock-service/get_products.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from aws_lambda_powertools import Logger, Tracer 5 | 6 | logger = Logger() 7 | tracer = Tracer() 8 | 9 | with open('product_list.json', 'r') as product_list: 10 | product_list = json.load(product_list) 11 | 12 | HEADERS = { 13 | "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN"), 14 | "Access-Control-Allow-Headers": "Content-Type", 15 | "Access-Control-Allow-Methods": "OPTIONS,POST,GET", 16 | } 17 | 18 | 19 | @logger.inject_lambda_context(log_event=True) 20 | @tracer.capture_lambda_handler 21 | def lambda_handler(event, context): 22 | """ 23 | Return list of all products. 24 | """ 25 | logger.debug("Fetching product list") 26 | 27 | return { 28 | "statusCode": 200, 29 | "headers": HEADERS, 30 | "body": json.dumps({"products": product_list}), 31 | } 32 | -------------------------------------------------------------------------------- /product-mock-service/product_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "fruit", 4 | "createdDate": "2017-04-17T01:14:03 -02:00", 5 | "description": "Culpa non veniam deserunt dolor irure elit cupidatat culpa consequat nulla irure aliqua.", 6 | "modifiedDate": "2019-03-13T12:18:27 -01:00", 7 | "name": "packaged strawberries", 8 | "package": { 9 | "height": 948, 10 | "length": 455, 11 | "weight": 54, 12 | "width": 905 13 | }, 14 | "pictures": [ 15 | "http://placehold.it/32x32" 16 | ], 17 | "price": 716, 18 | "productId": "4c1fadaa-213a-4ea8-aa32-58c217604e3c", 19 | "tags": [ 20 | "mollit", 21 | "ad", 22 | "eiusmod", 23 | "irure", 24 | "tempor" 25 | ] 26 | }, 27 | { 28 | "category": "sweets", 29 | "createdDate": "2017-04-06T06:21:36 -02:00", 30 | "description": "Dolore ipsum eiusmod dolore aliquip laborum laborum aute ipsum commodo id irure duis ipsum.", 31 | "modifiedDate": "2019-09-21T12:08:48 -02:00", 32 | "name": "candied prunes", 33 | "package": { 34 | "height": 329, 35 | "length": 179, 36 | "weight": 293, 37 | "width": 741 38 | }, 39 | "pictures": [ 40 | "http://placehold.it/32x32" 41 | ], 42 | "price": 35, 43 | "productId": "d2580eff-d105-45a5-9b21-ba61995bc6da", 44 | "tags": [ 45 | "laboris", 46 | "dolor", 47 | "in", 48 | "labore", 49 | "duis" 50 | ] 51 | }, 52 | { 53 | "category": "fruit", 54 | "createdDate": "2017-03-17T03:06:53 -01:00", 55 | "description": "Reprehenderit aliquip consequat quis excepteur et et esse exercitation adipisicing dolore nulla consequat.", 56 | "modifiedDate": "2019-11-25T12:32:49 -01:00", 57 | "name": "fresh prunes", 58 | "package": { 59 | "height": 736, 60 | "length": 567, 61 | "weight": 41, 62 | "width": 487 63 | }, 64 | "pictures": [ 65 | "http://placehold.it/32x32" 66 | ], 67 | "price": 2, 68 | "productId": "a6dd7187-40b6-4cb5-b73c-aecd655c6d9a", 69 | "tags": [ 70 | "nisi", 71 | "quis", 72 | "sint", 73 | "adipisicing", 74 | "pariatur" 75 | ] 76 | }, 77 | { 78 | "category": "vegetable", 79 | "createdDate": "2018-07-17T02:14:55 -02:00", 80 | "description": "Minim qui elit dolor est commodo excepteur ea voluptate eu dolor culpa magna.", 81 | "modifiedDate": "2019-09-05T03:36:34 -02:00", 82 | "name": "packaged tomatoes", 83 | "package": { 84 | "height": 4, 85 | "length": 756, 86 | "weight": 607, 87 | "width": 129 88 | }, 89 | "pictures": [ 90 | "http://placehold.it/32x32" 91 | ], 92 | "price": 97, 93 | "productId": "c0fbcc6b-7a70-41ac-aac4-f8fd237dc62e", 94 | "tags": [ 95 | "dolor", 96 | "officia", 97 | "fugiat", 98 | "officia", 99 | "voluptate" 100 | ] 101 | }, 102 | { 103 | "category": "vegetable", 104 | "createdDate": "2017-07-25T12:00:11 -02:00", 105 | "description": "Labore dolore velit mollit aute qui magna elit excepteur officia cupidatat ea ea aliqua.", 106 | "modifiedDate": "2019-10-04T06:32:14 -02:00", 107 | "name": "fresh tomatoes", 108 | "package": { 109 | "height": 881, 110 | "length": 252, 111 | "weight": 66, 112 | "width": 431 113 | }, 114 | "pictures": [ 115 | "http://placehold.it/32x32" 116 | ], 117 | "price": 144, 118 | "productId": "cb40c919-033a-47d6-8d00-1d73e2df20fe", 119 | "tags": [ 120 | "nostrud", 121 | "elit", 122 | "Lorem", 123 | "occaecat", 124 | "duis" 125 | ] 126 | }, 127 | { 128 | "category": "vegetable", 129 | "createdDate": "2017-01-07T05:28:03 -01:00", 130 | "description": "Ad eiusmod cupidatat duis dolor mollit labore mollit eu.", 131 | "modifiedDate": "2019-04-03T10:36:25 -02:00", 132 | "name": "fresh lettuce", 133 | "package": { 134 | "height": 813, 135 | "length": 932, 136 | "weight": 457, 137 | "width": 436 138 | }, 139 | "pictures": [ 140 | "http://placehold.it/32x32" 141 | ], 142 | "price": 51, 143 | "productId": "12929eb9-3eb7-4217-99e4-1a39c39217b6", 144 | "tags": [ 145 | "ad", 146 | "ipsum", 147 | "est", 148 | "eiusmod", 149 | "duis" 150 | ] 151 | }, 152 | { 153 | "category": "meat", 154 | "createdDate": "2018-12-03T12:33:44 -01:00", 155 | "description": "Amet cupidatat anim ipsum pariatur sit eu.", 156 | "modifiedDate": "2019-04-17T06:31:47 -02:00", 157 | "name": "packaged steak", 158 | "package": { 159 | "height": 707, 160 | "length": 417, 161 | "weight": 491, 162 | "width": 549 163 | }, 164 | "pictures": [ 165 | "http://placehold.it/32x32" 166 | ], 167 | "price": 894, 168 | "productId": "9fd9ef32-493f-4188-99f5-3aa809aa4fa9", 169 | "tags": [ 170 | "fugiat", 171 | "velit", 172 | "non", 173 | "magna", 174 | "laboris" 175 | ] 176 | }, 177 | { 178 | "category": "vegetable", 179 | "createdDate": "2017-04-27T06:48:08 -02:00", 180 | "description": "Labore est aliqua laborum ea laboris voluptate cillum aute duis occaecat.", 181 | "modifiedDate": "2019-11-01T10:23:57 -01:00", 182 | "name": "fresh lettuce", 183 | "package": { 184 | "height": 21, 185 | "length": 311, 186 | "weight": 817, 187 | "width": 964 188 | }, 189 | "pictures": [ 190 | "http://placehold.it/32x32" 191 | ], 192 | "price": 452, 193 | "productId": "20db6331-1084-48ff-8c4f-c1d98a6a1aa4", 194 | "tags": [ 195 | "laborum", 196 | "in", 197 | "aliquip", 198 | "sint", 199 | "quis" 200 | ] 201 | }, 202 | { 203 | "category": "sweet", 204 | "createdDate": "2017-11-24T04:01:33 -01:00", 205 | "description": "Fugiat sunt in eu eu occaecat.", 206 | "modifiedDate": "2019-05-19T05:53:56 -02:00", 207 | "name": "half-eaten cake", 208 | "package": { 209 | "height": 337, 210 | "length": 375, 211 | "weight": 336, 212 | "width": 1 213 | }, 214 | "pictures": [ 215 | "http://placehold.it/32x32" 216 | ], 217 | "price": 322, 218 | "productId": "8c843a54-27d7-477c-81b3-c21db12ed1c9", 219 | "tags": [ 220 | "officia", 221 | "proident", 222 | "officia", 223 | "commodo", 224 | "nisi" 225 | ] 226 | }, 227 | { 228 | "category": "dairy", 229 | "createdDate": "2018-05-29T11:46:28 -02:00", 230 | "description": "Aliqua officia magna do ipsum laboris anim magna nulla sit labore nulla qui duis.", 231 | "modifiedDate": "2019-05-29T05:33:49 -02:00", 232 | "name": "leftover cheese", 233 | "package": { 234 | "height": 267, 235 | "length": 977, 236 | "weight": 85, 237 | "width": 821 238 | }, 239 | "pictures": [ 240 | "http://placehold.it/32x32" 241 | ], 242 | "price": 163, 243 | "productId": "8d2024c0-6c05-4691-a0ff-dd52959bd1df", 244 | "tags": [ 245 | "excepteur", 246 | "ipsum", 247 | "nulla", 248 | "nisi", 249 | "velit" 250 | ] 251 | }, 252 | { 253 | "category": "bakery", 254 | "createdDate": "2018-09-22T05:22:38 -02:00", 255 | "description": "Ullamco commodo cupidatat reprehenderit eu sunt.", 256 | "modifiedDate": "2019-03-11T06:10:38 -01:00", 257 | "name": "fresh croissants", 258 | "package": { 259 | "height": 122, 260 | "length": 23, 261 | "weight": 146, 262 | "width": 694 263 | }, 264 | "pictures": [ 265 | "http://placehold.it/32x32" 266 | ], 267 | "price": 634, 268 | "productId": "867ecb2b-ef08-446e-8360-b63f60969e3d", 269 | "tags": [ 270 | "labore", 271 | "dolor", 272 | "aliquip", 273 | "nulla", 274 | "aute" 275 | ] 276 | }, 277 | { 278 | "category": "meat", 279 | "createdDate": "2018-09-12T07:24:46 -02:00", 280 | "description": "Eu ullamco irure qui labore qui duis mollit eiusmod adipisicing fugiat adipisicing nostrud ut non.", 281 | "modifiedDate": "2019-10-28T01:25:50 -01:00", 282 | "name": "packaged ham", 283 | "package": { 284 | "height": 902, 285 | "length": 278, 286 | "weight": 775, 287 | "width": 31 288 | }, 289 | "pictures": [ 290 | "http://placehold.it/32x32" 291 | ], 292 | "price": 77, 293 | "productId": "684011fc-ecfd-4557-a6df-9fc977365826", 294 | "tags": [ 295 | "voluptate", 296 | "laborum", 297 | "exercitation", 298 | "anim", 299 | "anim" 300 | ] 301 | }, 302 | { 303 | "category": "bakery", 304 | "createdDate": "2017-06-12T09:15:36 -02:00", 305 | "description": "Eu culpa nulla est et anim sint amet.", 306 | "modifiedDate": "2019-08-22T04:22:39 -02:00", 307 | "name": "fresh bread", 308 | "package": { 309 | "height": 551, 310 | "length": 976, 311 | "weight": 47, 312 | "width": 846 313 | }, 314 | "pictures": [ 315 | "http://placehold.it/32x32" 316 | ], 317 | "price": 805, 318 | "productId": "b027697d-a070-4c8f-8b9a-b8c80b2eb0ba", 319 | "tags": [ 320 | "nostrud", 321 | "in", 322 | "duis", 323 | "laboris", 324 | "minim" 325 | ] 326 | }, 327 | { 328 | "category": "sweet", 329 | "createdDate": "2018-09-06T06:03:43 -02:00", 330 | "description": "Mollit proident aliquip consectetur irure qui veniam laboris aliqua proident id fugiat esse nulla.", 331 | "modifiedDate": "2019-10-16T10:53:33 -02:00", 332 | "name": "candied strawberries", 333 | "package": { 334 | "height": 55, 335 | "length": 32, 336 | "weight": 661, 337 | "width": 694 338 | }, 339 | "pictures": [ 340 | "http://placehold.it/32x32" 341 | ], 342 | "price": 283, 343 | "productId": "7e0dbfa9-a672-4987-a26c-f601d177463a", 344 | "tags": [ 345 | "minim", 346 | "irure", 347 | "in", 348 | "duis", 349 | "labore" 350 | ] 351 | }, 352 | { 353 | "category": "bakery", 354 | "createdDate": "2017-07-23T12:27:34 -02:00", 355 | "description": "Ex non proident et eiusmod et elit est exercitation anim qui ullamco elit.", 356 | "modifiedDate": "2019-09-04T08:25:44 -02:00", 357 | "name": "fresh pie", 358 | "package": { 359 | "height": 718, 360 | "length": 59, 361 | "weight": 18, 362 | "width": 962 363 | }, 364 | "pictures": [ 365 | "http://placehold.it/32x32" 366 | ], 367 | "price": 646, 368 | "productId": "d1d527b8-9cef-4e97-a873-22236f3ee289", 369 | "tags": [ 370 | "in", 371 | "ea", 372 | "excepteur", 373 | "id", 374 | "dolore" 375 | ] 376 | }, 377 | { 378 | "category": "vegetable", 379 | "createdDate": "2018-11-08T04:08:28 -01:00", 380 | "description": "Pariatur deserunt nostrud cupidatat ut officia voluptate adipisicing mollit sunt cillum quis magna dolore aute.", 381 | "modifiedDate": "2019-10-11T10:28:49 -02:00", 382 | "name": "packaged lettuce", 383 | "package": { 384 | "height": 81, 385 | "length": 57, 386 | "weight": 653, 387 | "width": 367 388 | }, 389 | "pictures": [ 390 | "http://placehold.it/32x32" 391 | ], 392 | "price": 197, 393 | "productId": "11663d33-e54d-49da-ba6f-44d016ecde7e", 394 | "tags": [ 395 | "incididunt", 396 | "in", 397 | "adipisicing", 398 | "eu", 399 | "tempor" 400 | ] 401 | }, 402 | { 403 | "category": "meat", 404 | "createdDate": "2018-09-28T04:01:24 -02:00", 405 | "description": "Dolore nulla laboris incididunt laborum.", 406 | "modifiedDate": "2019-08-05T01:06:02 -02:00", 407 | "name": "leftover ham", 408 | "package": { 409 | "height": 246, 410 | "length": 639, 411 | "weight": 354, 412 | "width": 953 413 | }, 414 | "pictures": [ 415 | "http://placehold.it/32x32" 416 | ], 417 | "price": 728, 418 | "productId": "e173d669-b449-4226-af2e-128142abdd30", 419 | "tags": [ 420 | "exercitation", 421 | "magna", 422 | "ex", 423 | "quis", 424 | "ad" 425 | ] 426 | }, 427 | { 428 | "category": "dairy", 429 | "createdDate": "2018-08-23T06:31:47 -02:00", 430 | "description": "Pariatur mollit voluptate enim qui pariatur deserunt elit.", 431 | "modifiedDate": "2019-10-02T10:50:16 -02:00", 432 | "name": "fresh milk", 433 | "package": { 434 | "height": 576, 435 | "length": 948, 436 | "weight": 535, 437 | "width": 646 438 | }, 439 | "pictures": [ 440 | "http://placehold.it/32x32" 441 | ], 442 | "price": 164, 443 | "productId": "2a5b681c-ec7f-4bd4-a51e-57a5b6591f7f", 444 | "tags": [ 445 | "labore", 446 | "id", 447 | "mollit", 448 | "occaecat", 449 | "elit" 450 | ] 451 | }, 452 | { 453 | "category": "vegetable", 454 | "createdDate": "2018-02-21T01:55:54 -01:00", 455 | "description": "Consectetur laborum ipsum ad laboris.", 456 | "modifiedDate": "2019-02-23T08:50:01 -01:00", 457 | "name": "half-eaten lettuce", 458 | "package": { 459 | "height": 348, 460 | "length": 119, 461 | "weight": 723, 462 | "width": 44 463 | }, 464 | "pictures": [ 465 | "http://placehold.it/32x32" 466 | ], 467 | "price": 583, 468 | "productId": "de979b05-9d71-4c7e-b10f-636332ccb6c1", 469 | "tags": [ 470 | "id", 471 | "velit", 472 | "cillum", 473 | "irure", 474 | "aute" 475 | ] 476 | }, 477 | { 478 | "category": "meat", 479 | "createdDate": "2017-05-14T03:39:21 -02:00", 480 | "description": "Aliqua tempor irure qui consectetur exercitation culpa minim magna laboris ex pariatur elit culpa.", 481 | "modifiedDate": "2019-11-24T02:23:27 -01:00", 482 | "name": "fresh steak", 483 | "package": { 484 | "height": 328, 485 | "length": 7, 486 | "weight": 439, 487 | "width": 747 488 | }, 489 | "pictures": [ 490 | "http://placehold.it/32x32" 491 | ], 492 | "price": 996, 493 | "productId": "aa91060a-3601-4cb8-a2cc-025d09c7a9b7", 494 | "tags": [ 495 | "qui", 496 | "dolore", 497 | "culpa", 498 | "est", 499 | "duis" 500 | ] 501 | } 502 | ] -------------------------------------------------------------------------------- /product-mock-service/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-lambda-powertools==1.0.0 -------------------------------------------------------------------------------- /product-mock.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | product-service 5 | 6 | SAM Template for mock product-service 7 | Parameters: 8 | AllowedOrigin: 9 | Type: 'String' 10 | 11 | Globals: 12 | Function: 13 | Timeout: 5 14 | Tracing: Active 15 | AutoPublishAlias: live 16 | Runtime: python3.9 17 | MemorySize: 256 18 | Environment: 19 | Variables: 20 | LOG_LEVEL: "DEBUG" 21 | ALLOWED_ORIGIN: !Ref AllowedOrigin 22 | POWERTOOLS_SERVICE_NAME: product-mock 23 | POWERTOOLS_METRICS_NAMESPACE: ecommerce-app 24 | Api: 25 | EndpointConfiguration: REGIONAL 26 | TracingEnabled: true 27 | OpenApiVersion: '2.0' 28 | Cors: 29 | AllowMethods: "'OPTIONS,POST,GET'" 30 | AllowHeaders: "'Content-Type'" 31 | AllowOrigin: !Sub "'${AllowedOrigin}'" 32 | 33 | Resources: 34 | GetProductFunction: 35 | Type: AWS::Serverless::Function 36 | Properties: 37 | CodeUri: product-mock-service/ 38 | Handler: get_product.lambda_handler 39 | ReservedConcurrentExecutions: 25 40 | Events: 41 | ListCart: 42 | Type: Api 43 | Properties: 44 | Path: /product/{product_id} 45 | Method: get 46 | 47 | GetProductsFunction: 48 | Type: AWS::Serverless::Function 49 | Properties: 50 | CodeUri: product-mock-service/ 51 | Handler: get_products.lambda_handler 52 | ReservedConcurrentExecutions: 25 53 | Events: 54 | ListCart: 55 | Type: Api 56 | Properties: 57 | Path: /product 58 | Method: get 59 | 60 | GetProductApiUrl: 61 | Type: AWS::SSM::Parameter 62 | Properties: 63 | Type: String 64 | Name: /serverless-shopping-cart-demo/products/products-api-url 65 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" 66 | 67 | 68 | Outputs: 69 | ProductApi: 70 | Description: "API Gateway endpoint URL for Prod stage for Product Mock Service" 71 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" 72 | -------------------------------------------------------------------------------- /sam_stacks/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.0 2 | cognitojwt==1.1.0 3 | aws-lambda-powertools==1.0.0 4 | boto3==1.10.34 5 | -------------------------------------------------------------------------------- /sam_stacks/shared.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import datetime 3 | import os 4 | import uuid 5 | from decimal import Decimal 6 | from http.cookies import SimpleCookie 7 | 8 | from aws_lambda_powertools import Tracer 9 | 10 | import cognitojwt 11 | 12 | tracer = Tracer() 13 | 14 | HEADERS = { 15 | "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN"), 16 | "Access-Control-Allow-Headers": "Content-Type", 17 | "Access-Control-Allow-Methods": "OPTIONS,POST,GET", 18 | "Access-Control-Allow-Credentials": True, 19 | } 20 | 21 | 22 | class NotFoundException(Exception): 23 | pass 24 | 25 | 26 | @tracer.capture_method 27 | def handle_decimal_type(obj): 28 | """ 29 | json serializer which works with Decimal types returned from DynamoDB. 30 | """ 31 | if isinstance(obj, Decimal): 32 | if float(obj).is_integer(): 33 | return int(obj) 34 | else: 35 | return float(obj) 36 | raise TypeError 37 | 38 | 39 | @tracer.capture_method 40 | def generate_ttl(days=1): 41 | """ 42 | Generate epoch timestamp for number days in future 43 | """ 44 | future = datetime.datetime.utcnow() + datetime.timedelta(days=days) 45 | return calendar.timegm(future.utctimetuple()) 46 | 47 | 48 | @tracer.capture_method 49 | def get_user_sub(jwt_token): 50 | """ 51 | Validate JWT claims & retrieve user identifier 52 | """ 53 | try: 54 | verified_claims = cognitojwt.decode( 55 | jwt_token, os.environ["AWS_REGION"], os.environ["USERPOOL_ID"] 56 | ) 57 | except (cognitojwt.CognitoJWTException, ValueError): 58 | verified_claims = {} 59 | 60 | return verified_claims.get("sub") 61 | 62 | 63 | @tracer.capture_method 64 | def get_cart_id(event_headers): 65 | """ 66 | Retrieve cart_id from cookies if it exists, otherwise set and return it 67 | """ 68 | cookie = SimpleCookie() 69 | try: 70 | cookie.load(event_headers["cookie"]) 71 | cart_cookie = cookie["cartId"].value 72 | generated = False 73 | except KeyError: 74 | cart_cookie = str(uuid.uuid4()) 75 | generated = True 76 | 77 | return cart_cookie, generated 78 | 79 | 80 | @tracer.capture_method 81 | def get_headers(cart_id): 82 | """ 83 | Get the headers to add to response data 84 | """ 85 | headers = HEADERS 86 | cookie = SimpleCookie() 87 | cookie["cartId"] = cart_id 88 | cookie["cartId"]["max-age"] = (60 * 60) * 24 # 1 day 89 | cookie["cartId"]["secure"] = True 90 | cookie["cartId"]["httponly"] = True 91 | cookie["cartId"]["samesite"] = "None" 92 | cookie["cartId"]["path"] = "/" 93 | headers["Set-Cookie"] = cookie["cartId"].OutputString() 94 | return headers 95 | -------------------------------------------------------------------------------- /samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | [default.deploy.parameters] 3 | stack_name = "shopping-cart-nested-stack" 4 | resolve_s3 = true 5 | s3_prefix = "shopping-cart-nested-stack" 6 | region = "us-west-1" 7 | confirm_changeset = true 8 | capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND" 9 | disable_rollback = true 10 | image_repositories = [] 11 | -------------------------------------------------------------------------------- /shopping-cart-service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-sam-nested-stack-sample/86e635bcdc96d1520782ba5b0d40e7e0b9f4ada0/shopping-cart-service/__init__.py -------------------------------------------------------------------------------- /shopping-cart-service/add_to_cart.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Metrics, Tracer 6 | 7 | from shared import ( 8 | NotFoundException, 9 | generate_ttl, 10 | get_cart_id, 11 | get_headers, 12 | get_user_sub, 13 | ) 14 | from utils import get_product_from_external_service 15 | 16 | logger = Logger() 17 | tracer = Tracer() 18 | metrics = Metrics() 19 | 20 | dynamodb = boto3.resource("dynamodb") 21 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 22 | product_service_url = os.environ["PRODUCT_SERVICE_URL"] 23 | 24 | 25 | @metrics.log_metrics(capture_cold_start_metric=True) 26 | @logger.inject_lambda_context(log_event=True) 27 | @tracer.capture_lambda_handler 28 | def lambda_handler(event, context): 29 | """ 30 | Add a the provided quantity of a product to a cart. Where an item already exists in the cart, the quantities will 31 | be summed. 32 | """ 33 | 34 | try: 35 | request_payload = json.loads(event["body"]) 36 | except KeyError: 37 | return { 38 | "statusCode": 400, 39 | "headers": get_headers(), 40 | "body": json.dumps({"message": "No Request payload"}), 41 | } 42 | product_id = request_payload["productId"] 43 | quantity = request_payload.get("quantity", 1) 44 | cart_id, _ = get_cart_id(event["headers"]) 45 | 46 | # Because this method can be called anonymously, we need to check there's a logged in user 47 | user_sub = None 48 | jwt_token = event["headers"].get("Authorization") 49 | if jwt_token: 50 | user_sub = get_user_sub(jwt_token) 51 | 52 | try: 53 | product = get_product_from_external_service(product_id) 54 | logger.info("No product found with product_id: %s", product_id) 55 | except NotFoundException: 56 | return { 57 | "statusCode": 404, 58 | "headers": get_headers(cart_id=cart_id), 59 | "body": json.dumps({"message": "product not found"}), 60 | } 61 | 62 | if user_sub: 63 | logger.info("Authenticated user") 64 | pk = f"user#{user_sub}" 65 | ttl = generate_ttl( 66 | 7 67 | ) # Set a longer ttl for logged in users - we want to keep their cart for longer. 68 | else: 69 | logger.info("Unauthenticated user") 70 | pk = f"cart#{cart_id}" 71 | ttl = generate_ttl() 72 | 73 | if int(quantity) < 0: 74 | table.update_item( 75 | Key={"pk": pk, "sk": f"product#{product_id}"}, 76 | ExpressionAttributeNames={ 77 | "#quantity": "quantity", 78 | "#expirationTime": "expirationTime", 79 | "#productDetail": "productDetail", 80 | }, 81 | ExpressionAttributeValues={ 82 | ":val": quantity, 83 | ":ttl": ttl, 84 | ":productDetail": product, 85 | ":limit": abs(quantity), 86 | }, 87 | UpdateExpression="ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail", 88 | # Prevent quantity less than 0 89 | ConditionExpression="quantity >= :limit", 90 | ) 91 | else: 92 | table.update_item( 93 | Key={"pk": pk, "sk": f"product#{product_id}"}, 94 | ExpressionAttributeNames={ 95 | "#quantity": "quantity", 96 | "#expirationTime": "expirationTime", 97 | "#productDetail": "productDetail", 98 | }, 99 | ExpressionAttributeValues={ 100 | ":val": quantity, 101 | ":ttl": generate_ttl(), 102 | ":productDetail": product, 103 | }, 104 | UpdateExpression="ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail", 105 | ) 106 | metrics.add_metric(name="CartUpdated", unit="Count", value=1) 107 | 108 | return { 109 | "statusCode": 200, 110 | "headers": get_headers(cart_id), 111 | "body": json.dumps( 112 | {"productId": product_id, "message": "product added to cart"} 113 | ), 114 | } 115 | -------------------------------------------------------------------------------- /shopping-cart-service/checkout_cart.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Metrics, Tracer 6 | from boto3.dynamodb.conditions import Key 7 | 8 | from shared import get_cart_id, get_headers, handle_decimal_type 9 | 10 | logger = Logger() 11 | tracer = Tracer() 12 | metrics = Metrics() 13 | 14 | dynamodb = boto3.resource("dynamodb") 15 | 16 | logger.debug("Initializing DDB Table %s", os.environ["TABLE_NAME"]) 17 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 18 | 19 | 20 | @metrics.log_metrics(capture_cold_start_metric=True) 21 | @logger.inject_lambda_context(log_event=True) 22 | @tracer.capture_lambda_handler 23 | def lambda_handler(event, context): 24 | """ 25 | Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user 26 | is logged in. 27 | """ 28 | cart_id, _ = get_cart_id(event["headers"]) 29 | 30 | try: 31 | # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here 32 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 33 | except KeyError: 34 | 35 | return { 36 | "statusCode": 400, 37 | "headers": get_headers(cart_id), 38 | "body": json.dumps({"message": "Invalid user"}), 39 | } 40 | 41 | # Get all cart items belonging to the user's identity 42 | response = table.query( 43 | KeyConditionExpression=Key("pk").eq(f"user#{user_id}") 44 | & Key("sk").begins_with("product#"), 45 | ConsistentRead=True, # Perform a strongly consistent read here to ensure we get correct and up to date cart 46 | ) 47 | 48 | cart_items = response.get("Items") 49 | # batch_writer will be used to update status for cart entries belonging to the user 50 | with table.batch_writer() as batch: 51 | for item in cart_items: 52 | # Delete ordered items 53 | batch.delete_item(Key={"pk": item["pk"], "sk": item["sk"]}) 54 | 55 | metrics.add_metric(name="CartCheckedOut", unit="Count", value=1) 56 | logger.info({"action": "CartCheckedOut", "cartItems": cart_items}) 57 | 58 | return { 59 | "statusCode": 200, 60 | "headers": get_headers(cart_id), 61 | "body": json.dumps( 62 | {"products": response.get("Items")}, default=handle_decimal_type 63 | ), 64 | } 65 | -------------------------------------------------------------------------------- /shopping-cart-service/db_stream_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import Counter 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Tracer 6 | from boto3.dynamodb import types 7 | 8 | logger = Logger() 9 | tracer = Tracer() 10 | 11 | dynamodb = boto3.resource("dynamodb") 12 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 13 | 14 | deserializer = types.TypeDeserializer() 15 | 16 | 17 | @tracer.capture_method 18 | def dynamodb_to_python(dynamodb_item): 19 | """ 20 | Convert from dynamodb low level format to python dict 21 | """ 22 | return {k: deserializer.deserialize(v) for k, v in dynamodb_item.items()} 23 | 24 | 25 | @logger.inject_lambda_context(log_event=True) 26 | @tracer.capture_lambda_handler 27 | def lambda_handler(event, context): 28 | """ 29 | Handle streams from DynamoDB table 30 | """ 31 | 32 | records = event["Records"] 33 | quantity_change_counter = Counter() 34 | 35 | for record in records: 36 | keys = dynamodb_to_python(record["dynamodb"]["Keys"]) 37 | # NewImage record only exists if the event is INSERT or MODIFY 38 | if record["eventName"] in ("INSERT", "MODIFY"): 39 | new_image = dynamodb_to_python(record["dynamodb"]["NewImage"]) 40 | else: 41 | new_image = {} 42 | 43 | old_image_ddb = record["dynamodb"].get("OldImage") 44 | 45 | if old_image_ddb: 46 | old_image = dynamodb_to_python( 47 | record["dynamodb"].get("OldImage") 48 | ) # Won't exist in case event is INSERT 49 | else: 50 | old_image = {} 51 | 52 | # We want to record the quantity change the change made to the db rather than absolute values 53 | if keys["sk"].startswith("product#"): 54 | quantity_change_counter.update( 55 | { 56 | keys["sk"]: new_image.get("quantity", 0) 57 | - old_image.get("quantity", 0) 58 | } 59 | ) 60 | 61 | for k, v in quantity_change_counter.items(): 62 | table.update_item( 63 | Key={"pk": k, "sk": "totalquantity"}, 64 | ExpressionAttributeNames={"#quantity": "quantity"}, 65 | ExpressionAttributeValues={":val": v}, 66 | UpdateExpression="ADD #quantity :val", 67 | ) 68 | 69 | return { 70 | "statusCode": 200, 71 | } 72 | -------------------------------------------------------------------------------- /shopping-cart-service/delete_from_cart.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Tracer 6 | 7 | logger = Logger() 8 | tracer = Tracer() 9 | 10 | dynamodb = boto3.resource("dynamodb") 11 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 12 | 13 | 14 | @logger.inject_lambda_context(log_event=True) 15 | @tracer.capture_lambda_handler 16 | def lambda_handler(event, context): 17 | """ 18 | Handle messages from SQS Queue containing cart items, and delete them from DynamoDB. 19 | """ 20 | 21 | records = event["Records"] 22 | logger.info(f"Deleting {len(records)} records") 23 | with table.batch_writer() as batch: 24 | for item in records: 25 | item_body = json.loads(item["body"]) 26 | batch.delete_item(Key={"pk": item_body["pk"], "sk": item_body["sk"]}) 27 | 28 | return { 29 | "statusCode": 200, 30 | } 31 | -------------------------------------------------------------------------------- /shopping-cart-service/get_cart_total.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Tracer 6 | 7 | from shared import handle_decimal_type 8 | 9 | logger = Logger() 10 | tracer = Tracer() 11 | 12 | dynamodb = boto3.resource("dynamodb") 13 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 14 | 15 | 16 | @logger.inject_lambda_context(log_event=True) 17 | @tracer.capture_lambda_handler 18 | def lambda_handler(event, context): 19 | """ 20 | List items in shopping cart. 21 | """ 22 | product_id = event["pathParameters"]["product_id"] 23 | response = table.get_item( 24 | Key={"pk": f"product#{product_id}", "sk": "totalquantity"} 25 | ) 26 | quantity = response["Item"]["quantity"] 27 | 28 | return { 29 | "statusCode": 200, 30 | "body": json.dumps( 31 | {"product": product_id, "quantity": quantity}, default=handle_decimal_type 32 | ), 33 | } 34 | -------------------------------------------------------------------------------- /shopping-cart-service/list_cart.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Tracer 6 | from boto3.dynamodb.conditions import Key 7 | 8 | from shared import get_cart_id, get_headers, get_user_sub, handle_decimal_type 9 | 10 | logger = Logger() 11 | tracer = Tracer() 12 | 13 | dynamodb = boto3.resource("dynamodb") 14 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 15 | 16 | 17 | @logger.inject_lambda_context(log_event=True) 18 | @tracer.capture_lambda_handler 19 | def lambda_handler(event, context): 20 | """ 21 | List items in shopping cart. 22 | """ 23 | 24 | cart_id, generated = get_cart_id(event["headers"]) 25 | 26 | # Because this method can be called anonymously, we need to check there's a logged in user 27 | jwt_token = event["headers"].get("Authorization") 28 | if jwt_token: 29 | user_sub = get_user_sub(jwt_token) 30 | key_string = f"user#{user_sub}" 31 | logger.structure_logs(append=True, cart_id=f"user#{user_sub}") 32 | else: 33 | key_string = f"cart#{cart_id}" 34 | logger.structure_logs(append=True, cart_id=f"cart#{cart_id}") 35 | 36 | # No need to query database if the cart_id was generated rather than passed into the function 37 | if generated: 38 | logger.info("cart ID was generated in this request, not fetching cart from DB") 39 | product_list = [] 40 | else: 41 | logger.info("Fetching cart from DB") 42 | response = table.query( 43 | KeyConditionExpression=Key("pk").eq(key_string) 44 | & Key("sk").begins_with("product#"), 45 | ProjectionExpression="sk,quantity,productDetail", 46 | FilterExpression="quantity > :val", # Only return items with more than 0 quantity 47 | ExpressionAttributeValues={":val": 0}, 48 | ) 49 | product_list = response.get("Items", []) 50 | 51 | for product in product_list: 52 | product.update( 53 | (k, v.replace("product#", "")) for k, v in product.items() if k == "sk" 54 | ) 55 | 56 | return { 57 | "statusCode": 200, 58 | "headers": get_headers(cart_id), 59 | "body": json.dumps({"products": product_list}, default=handle_decimal_type), 60 | } 61 | -------------------------------------------------------------------------------- /shopping-cart-service/migrate_cart.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import threading 4 | 5 | import boto3 6 | from aws_lambda_powertools import Logger, Metrics, Tracer 7 | from boto3.dynamodb.conditions import Key 8 | 9 | from shared import generate_ttl, get_cart_id, get_headers, handle_decimal_type 10 | 11 | logger = Logger() 12 | tracer = Tracer() 13 | metrics = Metrics() 14 | 15 | dynamodb = boto3.resource("dynamodb") 16 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 17 | sqs = boto3.resource("sqs") 18 | queue = sqs.Queue(os.environ["DELETE_FROM_CART_SQS_QUEUE"]) 19 | 20 | 21 | @tracer.capture_method 22 | def update_item(user_id, item): 23 | """ 24 | Update an item in the database, adding the quantity of the passed in item to the quantity of any products already 25 | existing in the cart. 26 | """ 27 | table.update_item( 28 | Key={"pk": f"user#{user_id}", "sk": item["sk"]}, 29 | ExpressionAttributeNames={ 30 | "#quantity": "quantity", 31 | "#expirationTime": "expirationTime", 32 | "#productDetail": "productDetail", 33 | }, 34 | ExpressionAttributeValues={ 35 | ":val": item["quantity"], 36 | ":ttl": generate_ttl(days=30), 37 | ":productDetail": item["productDetail"], 38 | }, 39 | UpdateExpression="ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail", 40 | ) 41 | 42 | 43 | @metrics.log_metrics(capture_cold_start_metric=True) 44 | @logger.inject_lambda_context(log_event=True) 45 | @tracer.capture_lambda_handler 46 | def lambda_handler(event, context): 47 | """ 48 | Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user 49 | is logged in. 50 | """ 51 | 52 | cart_id, _ = get_cart_id(event["headers"]) 53 | try: 54 | # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here 55 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 56 | logger.info("Migrating cart_id %s to user_id %s", cart_id, user_id) 57 | except KeyError: 58 | 59 | return { 60 | "statusCode": 400, 61 | "headers": get_headers(cart_id), 62 | "body": json.dumps({"message": "Invalid user"}), 63 | } 64 | 65 | # Get all cart items belonging to the user's anonymous identity 66 | response = table.query( 67 | KeyConditionExpression=Key("pk").eq(f"cart#{cart_id}") 68 | & Key("sk").begins_with("product#") 69 | ) 70 | unauth_cart = response["Items"] 71 | 72 | # Since there's no batch operation available for updating items, and there's no dependency between them, we can 73 | # run them in parallel threads. 74 | thread_list = [] 75 | 76 | for item in unauth_cart: 77 | # Store items with user identifier as pk instead of "unauthenticated" cart ID 78 | # Using threading library to perform updates in parallel 79 | ddb_updateitem_thread = threading.Thread( 80 | target=update_item, args=(user_id, item) 81 | ) 82 | thread_list.append(ddb_updateitem_thread) 83 | ddb_updateitem_thread.start() 84 | 85 | # Delete items with unauthenticated cart ID 86 | # Rather than deleting directly, push to SQS queue to handle asynchronously 87 | queue.send_message(MessageBody=json.dumps(item, default=handle_decimal_type)) 88 | 89 | for ddb_thread in thread_list: 90 | ddb_thread.join() # Block main thread until all updates finished 91 | 92 | if unauth_cart: 93 | metrics.add_metric(name="CartMigrated", unit="Count", value=1) 94 | 95 | response = table.query( 96 | KeyConditionExpression=Key("pk").eq(f"user#{user_id}") 97 | & Key("sk").begins_with("product#"), 98 | ProjectionExpression="sk,quantity,productDetail", 99 | ConsistentRead=True, # Perform a strongly consistent read here to ensure we get correct values after updates 100 | ) 101 | 102 | product_list = response.get("Items", []) 103 | for product in product_list: 104 | product.update( 105 | (k, v.replace("product#", "")) for k, v in product.items() if k == "sk" 106 | ) 107 | 108 | return { 109 | "statusCode": 200, 110 | "headers": get_headers(cart_id), 111 | "body": json.dumps({"products": product_list}, default=handle_decimal_type), 112 | } 113 | -------------------------------------------------------------------------------- /shopping-cart-service/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-sam-nested-stack-sample/86e635bcdc96d1520782ba5b0d40e7e0b9f4ada0/shopping-cart-service/requirements.txt -------------------------------------------------------------------------------- /shopping-cart-service/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-sam-nested-stack-sample/86e635bcdc96d1520782ba5b0d40e7e0b9f4ada0/shopping-cart-service/tests/__init__.py -------------------------------------------------------------------------------- /shopping-cart-service/tests/test_example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | sys.path.append("..") # Add application to path 5 | sys.path.append("./layers/") # Add layer to path 6 | 7 | import shared # noqa: E402 # import from layer 8 | 9 | 10 | class Tests(unittest.TestCase): 11 | """ 12 | Example included to demonstrate how to run unit tests when using lambda layers. 13 | """ 14 | 15 | def setUp(self): 16 | pass 17 | 18 | def test_headers(self): 19 | self.assertEqual(shared.HEADERS.get("Access-Control-Allow-Credentials"), True) 20 | 21 | 22 | if __name__ == "__main__": 23 | unittest.main() 24 | -------------------------------------------------------------------------------- /shopping-cart-service/update_cart.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from aws_lambda_powertools import Logger, Metrics, Tracer 6 | 7 | from shared import ( 8 | NotFoundException, 9 | generate_ttl, 10 | get_cart_id, 11 | get_headers, 12 | get_user_sub, 13 | ) 14 | from utils import get_product_from_external_service 15 | 16 | logger = Logger() 17 | tracer = Tracer() 18 | metrics = Metrics() 19 | 20 | dynamodb = boto3.resource("dynamodb") 21 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 22 | product_service_url = os.environ["PRODUCT_SERVICE_URL"] 23 | 24 | 25 | @metrics.log_metrics(capture_cold_start_metric=True) 26 | @logger.inject_lambda_context(log_event=True) 27 | @tracer.capture_lambda_handler 28 | def lambda_handler(event, context): 29 | """ 30 | Idempotent update quantity of products in a cart. Quantity provided will overwrite existing quantity for a 31 | specific product in cart, rather than adding to it. 32 | """ 33 | 34 | try: 35 | request_payload = json.loads(event["body"]) 36 | except KeyError: 37 | return { 38 | "statusCode": 400, 39 | "headers": get_headers(), 40 | "body": json.dumps({"message": "No Request payload"}), 41 | } 42 | 43 | # retrieve the product_id that was specified in the url 44 | product_id = event["pathParameters"]["product_id"] 45 | 46 | quantity = int(request_payload["quantity"]) 47 | cart_id, _ = get_cart_id(event["headers"]) 48 | 49 | # Because this method can be called anonymously, we need to check if there's a logged in user 50 | user_sub = None 51 | jwt_token = event["headers"].get("Authorization") 52 | if jwt_token: 53 | user_sub = get_user_sub(jwt_token) 54 | 55 | try: 56 | product = get_product_from_external_service(product_id) 57 | except NotFoundException: 58 | logger.info("No product found with product_id: %s", product_id) 59 | return { 60 | "statusCode": 404, 61 | "headers": get_headers(cart_id=cart_id), 62 | "body": json.dumps({"message": "product not found"}), 63 | } 64 | 65 | # Prevent storing negative quantities of things 66 | if quantity < 0: 67 | return { 68 | "statusCode": 400, 69 | "headers": get_headers(cart_id), 70 | "body": json.dumps( 71 | { 72 | "productId": product_id, 73 | "message": "Quantity must not be lower than 0", 74 | } 75 | ), 76 | } 77 | 78 | # Use logged in user's identifier if it exists, otherwise use the anonymous identifier 79 | 80 | if user_sub: 81 | pk = f"user#{user_sub}" 82 | ttl = generate_ttl( 83 | 7 84 | ) # Set a longer ttl for logged in users - we want to keep their cart for longer. 85 | else: 86 | pk = f"cart#{cart_id}" 87 | ttl = generate_ttl() 88 | 89 | table.put_item( 90 | Item={ 91 | "pk": pk, 92 | "sk": f"product#{product_id}", 93 | "quantity": quantity, 94 | "expirationTime": ttl, 95 | "productDetail": product, 96 | } 97 | ) 98 | logger.info("about to add metrics...") 99 | metrics.add_metric(name="CartUpdated", unit="Count", value=1) 100 | 101 | return { 102 | "statusCode": 200, 103 | "headers": get_headers(cart_id), 104 | "body": json.dumps( 105 | {"productId": product_id, "quantity": quantity, "message": "cart updated"} 106 | ), 107 | } 108 | -------------------------------------------------------------------------------- /shopping-cart-service/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | from aws_lambda_powertools import Logger, Tracer 5 | 6 | from shared import NotFoundException 7 | 8 | product_service_url = os.environ["PRODUCT_SERVICE_URL"] 9 | 10 | logger = Logger() 11 | tracer = Tracer() 12 | 13 | 14 | @tracer.capture_method 15 | def get_product_from_external_service(product_id): 16 | """ 17 | Call product API to retrieve product details 18 | """ 19 | response = requests.get(product_service_url + f"/product/{product_id}") 20 | try: 21 | response_dict = response.json()["product"] 22 | except KeyError: 23 | logger.warn("No product found with id %s", product_id) 24 | raise NotFoundException 25 | 26 | return response_dict 27 | -------------------------------------------------------------------------------- /shoppingcart-service.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | shoppingcart-service 5 | 6 | SAM Template for shoppingcart-service 7 | 8 | Parameters: 9 | UserPoolArn: 10 | Type: 'AWS::SSM::Parameter::Value' 11 | Default: '/serverless-shopping-cart-demo/auth/user-pool-arn' 12 | UserPoolId: 13 | Type: 'AWS::SSM::Parameter::Value' 14 | Default: '/serverless-shopping-cart-demo/auth/user-pool-id' 15 | ProductServiceUrl: 16 | Type: 'AWS::SSM::Parameter::Value' 17 | Default: '/serverless-shopping-cart-demo/products/products-api-url' 18 | AllowedOrigin: 19 | Type: 'String' 20 | 21 | Globals: 22 | Function: 23 | Timeout: 5 24 | MemorySize: 512 25 | Tracing: Active 26 | AutoPublishAlias: live 27 | Runtime: python3.9 28 | Environment: 29 | Variables: 30 | TABLE_NAME: !Ref DynamoDBShoppingCartTable 31 | LOG_LEVEL: "INFO" 32 | ALLOWED_ORIGIN: !Ref AllowedOrigin 33 | POWERTOOLS_SERVICE_NAME: shopping-cart 34 | POWERTOOLS_METRICS_NAMESPACE: ecommerce-app 35 | Api: 36 | EndpointConfiguration: REGIONAL 37 | TracingEnabled: true 38 | OpenApiVersion: '2.0' 39 | Cors: 40 | AllowMethods: "'OPTIONS,POST,GET,PUT'" 41 | AllowHeaders: "'Content-Type,Authorization'" 42 | AllowCredentials: true 43 | AllowOrigin: !Sub "'${AllowedOrigin}'" 44 | 45 | Resources: 46 | UtilsLayer: 47 | Type: AWS::Serverless::LayerVersion 48 | Properties: 49 | ContentUri: ./sam_stacks/ 50 | CompatibleRuntimes: 51 | - python3.9 52 | Metadata: 53 | BuildMethod: python3.9 54 | 55 | CartApi: 56 | Type: AWS::Serverless::Api 57 | DependsOn: 58 | - ApiGWAccount 59 | Properties: 60 | StageName: Prod 61 | MethodSettings: 62 | - DataTraceEnabled: True 63 | MetricsEnabled: True 64 | ResourcePath: "/*" 65 | HttpMethod: "*" 66 | LoggingLevel: INFO 67 | Auth: 68 | Authorizers: 69 | CognitoAuthorizer: 70 | UserPoolArn: !Ref UserPoolArn 71 | Identity: # OPTIONAL 72 | Header: Authorization # OPTIONAL; Default: 'Authorization' 73 | UsagePlan: 74 | CreateUsagePlan: PER_API 75 | Description: Usage plan for this API 76 | 77 | ListCartRole: 78 | Type: AWS::IAM::Role 79 | Properties: 80 | AssumeRolePolicyDocument: 81 | Version: "2012-10-17" 82 | Statement: 83 | - Action: 84 | - "sts:AssumeRole" 85 | Effect: "Allow" 86 | Principal: 87 | Service: 88 | - "lambda.amazonaws.com" 89 | 90 | AddToCartRole: 91 | Type: AWS::IAM::Role 92 | Properties: 93 | AssumeRolePolicyDocument: 94 | Version: "2012-10-17" 95 | Statement: 96 | - Action: 97 | - "sts:AssumeRole" 98 | Effect: "Allow" 99 | Principal: 100 | Service: 101 | - "lambda.amazonaws.com" 102 | 103 | LambdaLoggingPolicy: 104 | Type: "AWS::IAM::Policy" 105 | Properties: 106 | PolicyName: LambdaXRayPolicy 107 | PolicyDocument: 108 | Version: "2012-10-17" 109 | Statement: 110 | - 111 | Effect: "Allow" 112 | Action: [ 113 | "xray:PutTraceSegments", 114 | "xray:PutTelemetryRecords", 115 | "logs:CreateLogGroup", 116 | "logs:CreateLogStream", 117 | "logs:PutLogEvents" 118 | ] 119 | Resource: "arn:aws:logs:*:*:*" 120 | - Effect: Allow 121 | Action: 122 | - logs:CreateLogStream 123 | - logs:PutLogEvents 124 | Resource: 125 | - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*:* 126 | Roles: 127 | - !Ref ListCartRole 128 | - !Ref AddToCartRole 129 | 130 | DynamoDBReadPolicy: 131 | Type: "AWS::IAM::Policy" 132 | Properties: 133 | PolicyName: DynamoDBReadPolicy 134 | PolicyDocument: 135 | Version: "2012-10-17" 136 | Statement: 137 | - 138 | Effect: "Allow" 139 | Action: [ 140 | "dynamodb:GetItem", 141 | "dynamodb:Scan", 142 | "dynamodb:Query", 143 | "dynamodb:BatchGetItem", 144 | "dynamodb:DescribeTable" 145 | ] 146 | Resource: 147 | - !GetAtt DynamoDBShoppingCartTable.Arn 148 | Roles: 149 | - !Ref ListCartRole 150 | - !Ref AddToCartRole 151 | 152 | DynamoDBWritePolicy: 153 | Type: "AWS::IAM::Policy" 154 | Properties: 155 | PolicyName: DynamoDBWritePolicy 156 | PolicyDocument: 157 | Version: "2012-10-17" 158 | Statement: 159 | - 160 | Effect: "Allow" 161 | Action: [ 162 | "dynamodb:PutItem", 163 | "dynamodb:UpdateItem", 164 | "dynamodb:ConditionCheckItem", 165 | "dynamodb:DeleteItem", 166 | "dynamodb:BatchWriteItem" 167 | ] 168 | Resource: !GetAtt DynamoDBShoppingCartTable.Arn 169 | Roles: 170 | - !Ref AddToCartRole 171 | 172 | SQSSendMessagePolicy: 173 | Type: "AWS::IAM::Policy" 174 | Properties: 175 | PolicyName: SQSSendMessagePolicy 176 | PolicyDocument: 177 | Version: "2012-10-17" 178 | Statement: 179 | - 180 | Effect: "Allow" 181 | Action: [ 182 | "sqs:SendMessage*" 183 | ] 184 | Resource: !GetAtt CartDeleteSQSQueue.Arn 185 | Roles: 186 | - !Ref AddToCartRole 187 | 188 | ListCartFunction: 189 | Type: AWS::Serverless::Function 190 | DependsOn: 191 | - LambdaLoggingPolicy 192 | Properties: 193 | CodeUri: shopping-cart-service/ 194 | ReservedConcurrentExecutions: 25 195 | Handler: list_cart.lambda_handler 196 | Role: !GetAtt ListCartRole.Arn 197 | Layers: 198 | - !Ref UtilsLayer 199 | Environment: 200 | Variables: 201 | USERPOOL_ID: !Ref UserPoolId 202 | Events: 203 | ListCart: 204 | Type: Api 205 | Properties: 206 | RestApiId: !Ref CartApi 207 | Path: /cart 208 | Method: get 209 | 210 | AddToCartFunction: 211 | Type: AWS::Serverless::Function 212 | DependsOn: 213 | - LambdaLoggingPolicy 214 | Properties: 215 | CodeUri: shopping-cart-service/ 216 | Handler: add_to_cart.lambda_handler 217 | Role: !GetAtt AddToCartRole.Arn 218 | ReservedConcurrentExecutions: 25 219 | Layers: 220 | - !Ref UtilsLayer 221 | Environment: 222 | Variables: 223 | PRODUCT_SERVICE_URL: !Ref ProductServiceUrl 224 | USERPOOL_ID: !Ref UserPoolId 225 | Events: 226 | AddToCart: 227 | Type: Api 228 | Properties: 229 | RestApiId: !Ref CartApi 230 | Path: /cart 231 | Method: post 232 | 233 | UpdateCartFunction: 234 | Type: AWS::Serverless::Function 235 | DependsOn: 236 | - LambdaLoggingPolicy 237 | Properties: 238 | CodeUri: shopping-cart-service/ 239 | Handler: update_cart.lambda_handler 240 | Role: !GetAtt AddToCartRole.Arn 241 | ReservedConcurrentExecutions: 25 242 | Layers: 243 | - !Ref UtilsLayer 244 | Environment: 245 | Variables: 246 | PRODUCT_SERVICE_URL: !Ref ProductServiceUrl 247 | USERPOOL_ID: !Ref UserPoolId 248 | Events: 249 | AddToCart: 250 | Type: Api 251 | Properties: 252 | RestApiId: !Ref CartApi 253 | Path: /cart/{product_id} 254 | Method: put 255 | 256 | MigrateCartFunction: 257 | Type: AWS::Serverless::Function 258 | DependsOn: 259 | - LambdaLoggingPolicy 260 | Properties: 261 | CodeUri: shopping-cart-service/ 262 | Handler: migrate_cart.lambda_handler 263 | Timeout: 30 264 | ReservedConcurrentExecutions: 25 265 | Layers: 266 | - !Ref UtilsLayer 267 | Environment: 268 | Variables: 269 | PRODUCT_SERVICE_URL: !Ref ProductServiceUrl 270 | USERPOOL_ID: !Ref UserPoolId 271 | DELETE_FROM_CART_SQS_QUEUE: !Ref CartDeleteSQSQueue 272 | Role: !GetAtt AddToCartRole.Arn 273 | Events: 274 | AddToCart: 275 | Type: Api 276 | Properties: 277 | RestApiId: !Ref CartApi 278 | Path: /cart/migrate 279 | Method: post 280 | Auth: 281 | Authorizer: CognitoAuthorizer 282 | 283 | CheckoutCartFunction: 284 | Type: AWS::Serverless::Function 285 | DependsOn: 286 | - LambdaLoggingPolicy 287 | Properties: 288 | CodeUri: shopping-cart-service/ 289 | Handler: checkout_cart.lambda_handler 290 | Timeout: 10 291 | ReservedConcurrentExecutions: 25 292 | Layers: 293 | - !Ref UtilsLayer 294 | Environment: 295 | Variables: 296 | PRODUCT_SERVICE_URL: !Ref ProductServiceUrl 297 | USERPOOL_ID: !Ref UserPoolId 298 | Role: !GetAtt AddToCartRole.Arn 299 | Events: 300 | AddToCart: 301 | Type: Api 302 | Properties: 303 | RestApiId: !Ref CartApi 304 | Path: /cart/checkout 305 | Method: post 306 | Auth: 307 | Authorizer: CognitoAuthorizer 308 | 309 | GetCartTotalFunction: 310 | Type: AWS::Serverless::Function 311 | DependsOn: 312 | - LambdaLoggingPolicy 313 | Properties: 314 | CodeUri: shopping-cart-service/ 315 | Handler: get_cart_total.lambda_handler 316 | Timeout: 10 317 | ReservedConcurrentExecutions: 25 318 | Layers: 319 | - !Ref UtilsLayer 320 | Role: !GetAtt ListCartRole.Arn 321 | Events: 322 | GetCartTotal: 323 | Type: Api 324 | Properties: 325 | RestApiId: !Ref CartApi 326 | Path: /cart/{product_id}/total 327 | Method: get 328 | 329 | DeleteFromCartFunction: 330 | Type: AWS::Serverless::Function 331 | DependsOn: 332 | - LambdaLoggingPolicy 333 | Properties: 334 | CodeUri: shopping-cart-service/ 335 | Handler: delete_from_cart.lambda_handler 336 | ReservedConcurrentExecutions: 25 # Keep the ddb spikes down in case of many deletes at once 337 | Policies: 338 | - SQSPollerPolicy: 339 | QueueName: 340 | !GetAtt CartDeleteSQSQueue.QueueName 341 | - Statement: 342 | - Effect: Allow 343 | Action: 344 | - "dynamodb:DeleteItem" 345 | - "dynamodb:BatchWriteItem" 346 | Resource: 347 | - !GetAtt DynamoDBShoppingCartTable.Arn 348 | Layers: 349 | - !Ref UtilsLayer 350 | Environment: 351 | Variables: 352 | USERPOOL_ID: !Ref UserPoolId 353 | Events: 354 | RetrieveFromSQS: 355 | Type: SQS 356 | Properties: 357 | Queue: !GetAtt CartDeleteSQSQueue.Arn 358 | BatchSize: 5 359 | 360 | CartDBStreamHandler: 361 | Type: AWS::Serverless::Function 362 | DependsOn: 363 | - LambdaLoggingPolicy 364 | Properties: 365 | CodeUri: shopping-cart-service/ 366 | Handler: db_stream_handler.lambda_handler 367 | Layers: 368 | - !Ref UtilsLayer 369 | ReservedConcurrentExecutions: 25 370 | Policies: 371 | - AWSLambdaDynamoDBExecutionRole 372 | - Statement: 373 | - Effect: Allow 374 | Action: 375 | - "dynamodb:UpdateItem" 376 | Resource: 377 | - !GetAtt DynamoDBShoppingCartTable.Arn 378 | Events: 379 | Stream: 380 | Type: DynamoDB 381 | Properties: 382 | Stream: !GetAtt DynamoDBShoppingCartTable.StreamArn 383 | BatchSize: 100 384 | MaximumBatchingWindowInSeconds: 60 385 | StartingPosition: LATEST 386 | 387 | DynamoDBShoppingCartTable: 388 | Type: AWS::DynamoDB::Table 389 | Properties: 390 | PointInTimeRecoverySpecification: 391 | PointInTimeRecoveryEnabled: true 392 | SSESpecification: 393 | SSEEnabled: true 394 | AttributeDefinitions: 395 | - AttributeName: pk 396 | AttributeType: S 397 | - AttributeName: sk 398 | AttributeType: S 399 | KeySchema: 400 | - AttributeName: pk 401 | KeyType: HASH 402 | - AttributeName: sk 403 | KeyType: RANGE 404 | BillingMode: PAY_PER_REQUEST 405 | StreamSpecification: 406 | StreamViewType: 'NEW_AND_OLD_IMAGES' 407 | TimeToLiveSpecification: 408 | AttributeName: expirationTime 409 | Enabled: True 410 | 411 | APIGWCloudWatchRole: 412 | Type: 'AWS::IAM::Role' 413 | Properties: 414 | AssumeRolePolicyDocument: 415 | Version: 2012-10-17 416 | Statement: 417 | - Effect: Allow 418 | Principal: 419 | Service: 420 | - apigateway.amazonaws.com 421 | Action: 'sts:AssumeRole' 422 | Path: / 423 | ManagedPolicyArns: 424 | - >- 425 | arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs 426 | ApiGWAccount: 427 | Type: 'AWS::ApiGateway::Account' 428 | Properties: 429 | CloudWatchRoleArn: !GetAtt APIGWCloudWatchRole.Arn 430 | 431 | CartDeleteSQSQueue: 432 | Type: AWS::SQS::Queue 433 | Properties: 434 | VisibilityTimeout: 20 435 | KmsMasterKeyId: alias/aws/sqs 436 | RedrivePolicy: 437 | deadLetterTargetArn: 438 | !GetAtt CartDeleteSQSDLQ.Arn 439 | maxReceiveCount: 5 440 | CartDeleteSQSDLQ: 441 | Type: AWS::SQS::Queue 442 | Properties: 443 | KmsMasterKeyId: alias/aws/sqs 444 | 445 | CartApiUrl: 446 | Type: AWS::SSM::Parameter 447 | Properties: 448 | Type: String 449 | Name: /serverless-shopping-cart-demo/shopping-cart/cart-api-url 450 | Value: !Sub "https://${CartApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" 451 | 452 | Outputs: 453 | CartApi: 454 | Description: "API Gateway endpoint URL for Prod stage for Cart Service" 455 | Value: !Sub "https://${CartApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" 456 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | auth-resources 5 | 6 | SAM Template for auth resources 7 | 8 | Resources: 9 | Auth: 10 | Type: AWS::Serverless::Application 11 | Properties: 12 | Location: auth.yaml 13 | Product: 14 | Type: AWS::Serverless::Application 15 | Properties: 16 | Location: product-mock.yaml 17 | Parameters: 18 | AllowedOrigin: 'http://localhost:8080' 19 | DependsOn: Auth 20 | Shopping: 21 | Type: AWS::Serverless::Application 22 | Properties: 23 | Location: shoppingcart-service.yaml 24 | Parameters: 25 | AllowedOrigin: 'http://localhost:8080' 26 | DependsOn: Product --------------------------------------------------------------------------------