├── .github ├── prerequisites │ ├── README.md │ └── github-actions-oidc-federation-and-role.yml └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets ├── SantiagoGarciaArangoCDK.png ├── aws_cdk_fastapi_lambda.drawio └── aws_cdk_fastapi_lambda.png ├── cdk.context.json ├── cdk.json ├── cdk ├── add_tags.py ├── app.py └── stacks │ ├── __init__.py │ └── cdk_lambda_fastapi_stack.py ├── deploy_backend.sh ├── important_commands.sh ├── lambda-layers ├── Makefile └── fastapi │ ├── Makefile │ └── requirements.txt ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── src ├── __init__.py └── lambdas │ ├── __init__.py │ └── api │ └── main.py ├── terraform ├── .terraform.lock.hcl ├── backend.tf ├── lambda.tf ├── locals.tf ├── provider.tf └── variables.tf └── tests ├── __init__.py └── unit ├── __init__.py ├── cdk └── test_cdk_lambda_fastapi.py └── lambdas ├── __init__.py ├── api ├── __init__.py └── test_main.py └── conftest.py /.github/prerequisites/README.md: -------------------------------------------------------------------------------- 1 | # AWS PREREQUISITES FOR THE GITHUB ACTIONS CI/CD PIPELINE 2 | 3 | Inspired on: 4 | 5 | - https://github.com/aws-actions/configure-aws-credentials/tree/main/examples 6 | 7 | The CI/CD uses aws-action `configure-aws-credentials` with OIDC federation. Prior to using this example project, the user needs to deploy the [github-actions-oidc-federation-and-role](github-actions-oidc-federation-and-role.yml) CloudFormation template in the AWS account they want to deploy the solution. Specify the GitHub Organization name, repository name, and the specific branch you want to deploy on. 8 | 9 | To use the example you will need to set the following GitHub Action Secrets: 10 | 11 | | Secret Key | Used With | Description | 12 | | --------------- | ------------------------- | ------------------------ | 13 | | AWS_ACCOUNT_ID | configure-aws-credentials | The AWS account ID | 14 | | AWS_DEPLOY_ROLE | configure-aws-credentials | The name of the IAM role | 15 | -------------------------------------------------------------------------------- /.github/prerequisites/github-actions-oidc-federation-and-role.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: Github Actions configuration - OIDC IAM IdP and associated role CI/CD 4 | 5 | Parameters: 6 | 7 | GitHubOrganization: 8 | Type: String 9 | Description: This is the root organization or personal account where repos are stored (Case Sensitive) 10 | 11 | RepositoryName: 12 | Type: String 13 | Description: The repo(s) these roles will have access to. (Use * for all org or personal repos) 14 | Default: "*" 15 | 16 | BranchName: 17 | Type: String 18 | Description: Name of the git branch to to trust. (Use * for all branches) 19 | Default: "*" 20 | 21 | RoleName: 22 | Type: String 23 | Description: Name the Role 24 | 25 | UseExistingProvider: 26 | Type: String 27 | Description: "Only one GitHub Provider can exists. Choose yes if one is already present in account" 28 | Default: "no" 29 | AllowedValues: 30 | - "yes" 31 | - "no" 32 | 33 | Conditions: 34 | 35 | CreateProvider: !Equals ["no", !Ref UseExistingProvider] 36 | 37 | Resources: 38 | 39 | IdpGitHubOidc: 40 | Type: AWS::IAM::OIDCProvider 41 | Condition: CreateProvider 42 | Properties: 43 | Url: https://token.actions.githubusercontent.com 44 | ClientIdList: 45 | - sts.amazonaws.com 46 | - !Sub https://github.com/${GitHubOrganization}/${RepositoryName} 47 | ThumbprintList: 48 | - 6938fd4d98bab03faadb97b34396831e3780aea1 49 | Tags: 50 | - Key: Name 51 | Value: !Sub ${RoleName}-OIDC-Provider 52 | 53 | RoleGithubActions: 54 | Type: AWS::IAM::Role 55 | Properties: 56 | RoleName: !Ref RoleName 57 | AssumeRolePolicyDocument: 58 | Statement: 59 | - Effect: Allow 60 | Action: sts:AssumeRoleWithWebIdentity 61 | Principal: 62 | Federated: !If 63 | - CreateProvider 64 | - !Ref IdpGitHubOidc 65 | - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com 66 | Condition: 67 | StringLike: 68 | token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrganization}/${RepositoryName}:ref:refs/heads/${BranchName} 69 | # ManagedPolicyArns: 70 | # ## edit the managed policy to give least privileges 71 | # - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess 72 | 73 | RoleGithubActionsPolicies: 74 | Type: "AWS::IAM::Policy" 75 | Properties: 76 | PolicyName: !Sub ${RoleName}-Policy 77 | PolicyDocument: 78 | Version: "2012-10-17" 79 | Statement: 80 | - Effect: "Allow" 81 | Action: "sts:AssumeRole" 82 | Resource: "arn:aws:iam::*:role/cdk-*" 83 | Roles: 84 | - !Ref RoleGithubActions 85 | 86 | Outputs: 87 | 88 | IdpGitHubOidc: 89 | Condition: CreateProvider 90 | Description: "ARN of Github OIDC Provider" 91 | Value: !GetAtt IdpGitHubOidc.Arn 92 | 93 | RoleGithubActionsARN: 94 | Description: "CICD Role for GitHub Actions" 95 | Value: !GetAtt RoleGithubActions.Arn -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: [ 'main', 'feature/**'] 6 | tags: [ 'v*'] 7 | env: 8 | AWS_DEFAULT_REGION: us-east-1 9 | AWS_DEFAULT_OUTPUT: json 10 | 11 | jobs: 12 | code-quality: 13 | name: Check coding standards 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: echo "Job triggered by ${{ github.event_name }} event." 18 | - run: echo "Job running on a ${{ runner.os }} server hosted by GitHub." 19 | - run: echo "Branch name is ${{ github.ref }} and repository is ${{ github.repository }}." 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.11 23 | - name: Install Poetry 24 | uses: snok/install-poetry@v1 25 | with: 26 | virtualenvs-create: true 27 | virtualenvs-in-project: true 28 | installer-parallel: true 29 | - name: Install Poetry dependencies 30 | run: poetry install --no-interaction 31 | - name: Check code formatting 32 | run: poetry run poe black-check 33 | 34 | cdk-synth: 35 | name: CDK Synth 36 | runs-on: ubuntu-latest 37 | needs: code-quality 38 | permissions: 39 | id-token: write # This is required for requesting the JWT 40 | contents: read # This is required for actions/checkout 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: actions/setup-python@v4 44 | with: 45 | python-version: 3.11 46 | 47 | - name: Install Poetry 48 | uses: snok/install-poetry@v1 49 | with: 50 | virtualenvs-create: true 51 | virtualenvs-in-project: true 52 | installer-parallel: true 53 | 54 | - name: Install Poetry dependencies 55 | run: poetry install --no-interaction 56 | 57 | - name: Set up NodeJs 58 | uses: actions/setup-node@v3 59 | with: 60 | node-version: "20" 61 | 62 | - name: Install CDK 63 | run: | 64 | npm install -g aws-cdk 65 | 66 | # # MY OLD AUTH CONFIG (NOW WITH GITHUB OIDC TOKEN) 67 | # - name: Configure AWS credentials 68 | # uses: aws-actions/configure-aws-credentials@master 69 | # with: 70 | # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} 71 | # aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} 72 | # aws-region: "us-east-1" 73 | 74 | - name: Configure AWS Credentials 75 | uses: aws-actions/configure-aws-credentials@v4 76 | with: 77 | aws-region: ${{ env.AWS_DEFAULT_REGION }} 78 | role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_DEPLOY_ROLE }} 79 | role-session-name: myGitHubActions 80 | 81 | # Sample STS get caller identity for tests 82 | - name: sts get-caller-identity 83 | run: | 84 | aws sts get-caller-identity 85 | 86 | - name: Synth CDK to CloudFormation Template 87 | run: | 88 | source .venv/bin/activate 89 | cdk synth 90 | 91 | - name: Archive CDK Synth results (no assets) 92 | uses: actions/upload-artifact@v3 93 | with: 94 | name: cdk-synth-folder 95 | path: | 96 | ./cdk.out 97 | !./cdk.out/asset.* 98 | retention-days: 1 99 | 100 | iac-checkov: 101 | name: IaC Checkov Validations 102 | runs-on: ubuntu-latest 103 | needs: cdk-synth 104 | steps: 105 | - uses: actions/checkout@v3 106 | 107 | - name: Dowload CDK Synth results 108 | uses: actions/download-artifact@v3 109 | with: 110 | name: cdk-synth-folder 111 | path: ./cdk-synth-output-folder 112 | 113 | - name: Display files in the output folder 114 | run: ls -lrta 115 | working-directory: ./cdk-synth-output-folder 116 | 117 | - name: Run Checkov action 118 | id: checkov 119 | uses: bridgecrewio/checkov-action@v12 120 | with: 121 | directory: cdk-synth-output-folder/ 122 | framework: cloudformation 123 | soft_fail: true # optional: do not return an error code if there are failed checks 124 | skip_check: CKV_AWS_2 # optional: skip a specific check_id. can be comma separated list 125 | quiet: true # optional: display only failed checks 126 | log_level: WARNING # optional: set log level. Default WARNING 127 | 128 | cdk-deploy: 129 | name: Deploy CDK 130 | runs-on: ubuntu-latest 131 | needs: iac-checkov 132 | if: github.ref == 'refs/heads/main' 133 | permissions: 134 | id-token: write # This is required for requesting the JWT 135 | contents: read # This is required for actions/checkout 136 | steps: 137 | - uses: actions/checkout@v3 138 | - uses: actions/setup-python@v4 139 | with: 140 | python-version: 3.11 141 | 142 | - name: Install Poetry 143 | uses: snok/install-poetry@v1 144 | with: 145 | virtualenvs-create: true 146 | virtualenvs-in-project: true 147 | installer-parallel: true 148 | 149 | - name: Install Poetry dependencies 150 | run: poetry install --no-interaction 151 | 152 | - name: Set up NodeJs 153 | uses: actions/setup-node@v3 154 | with: 155 | node-version: "20" 156 | 157 | - name: Install CDK 158 | run: npm install -g aws-cdk 159 | 160 | # # MY OLD AUTH CONFIG (NOW WITH GITHUB OIDC TOKEN) 161 | # - name: Configure AWS credentials 162 | # uses: aws-actions/configure-aws-credentials@master 163 | # with: 164 | # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} 165 | # aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} 166 | # aws-region: "us-east-1" 167 | 168 | - name: Configure AWS Credentials 169 | uses: aws-actions/configure-aws-credentials@v4 170 | with: 171 | aws-region: ${{ env.AWS_DEFAULT_REGION }} 172 | role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_DEPLOY_ROLE }} 173 | role-session-name: myGitHubActions 174 | 175 | # NOTE: for now no manual approvals are required 176 | - name: Deploy to AWS 177 | run: | 178 | source .venv/bin/activate 179 | cdk deploy --require-approval=never 180 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # OWN IGNORES 3 | ################################################################################ 4 | .vscode 5 | *.zip 6 | temp.* 7 | .DS_Store 8 | 9 | # To ignore Lambda Layers files (like Python dependencies) 10 | modules 11 | 12 | 13 | ################################################################################ 14 | # CDK IGNORES 15 | ################################################################################ 16 | 17 | # CDK asset staging directory 18 | .cdk.staging 19 | cdk.out 20 | 21 | *.swp 22 | package-lock.json 23 | 24 | 25 | ################################################################################ 26 | # CDK IGNORES 27 | ################################################################################ 28 | 29 | # Crash log 30 | crash.log 31 | 32 | # For built boxes 33 | *.box 34 | 35 | ### Packer Patch ### 36 | # ignore temporary output files 37 | output-*/ 38 | 39 | ### Terraform ### 40 | # Local .terraform directories 41 | **/.terraform/* 42 | 43 | # .tfstate files 44 | *.tfstate 45 | *.tfstate.* 46 | 47 | # Crash log files 48 | 49 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 50 | # .tfvars files are managed as part of configuration and so should be included in 51 | # version control. 52 | # 53 | # example.tfvars 54 | 55 | # Ignore override files as they are usually used to override resources locally and so 56 | # are not checked in 57 | override.tf 58 | override.tf.json 59 | *_override.tf 60 | *_override.tf.json 61 | 62 | # Include override files you do wish to add to version control using negated pattern 63 | # !example_override.tf 64 | 65 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 66 | # example: *tfplan* 67 | 68 | 69 | ################################################################################ 70 | # PYTHON IGNORES 71 | ################################################################################ 72 | 73 | # Byte-compiled / optimized / DLL files 74 | __pycache__/ 75 | *.py[cod] 76 | *$py.class 77 | 78 | # C extensions 79 | *.so 80 | 81 | # Distribution / packaging 82 | .Python 83 | build/ 84 | develop-eggs/ 85 | dist/ 86 | downloads/ 87 | eggs/ 88 | .eggs/ 89 | lib/ 90 | lib64/ 91 | parts/ 92 | sdist/ 93 | var/ 94 | wheels/ 95 | share/python-wheels/ 96 | *.egg-info/ 97 | .installed.cfg 98 | *.egg 99 | MANIFEST 100 | 101 | # PyInstaller 102 | # Usually these files are written by a python script from a template 103 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 104 | *.manifest 105 | *.spec 106 | 107 | # Installer logs 108 | pip-log.txt 109 | pip-delete-this-directory.txt 110 | 111 | # Unit test / coverage reports 112 | htmlcov/ 113 | .tox/ 114 | .nox/ 115 | .coverage 116 | .coverage.* 117 | .cache 118 | nosetests.xml 119 | coverage.xml 120 | *.cover 121 | *.py,cover 122 | .hypothesis/ 123 | .pytest_cache/ 124 | cover/ 125 | 126 | # Translations 127 | *.mo 128 | *.pot 129 | 130 | # Django stuff: 131 | *.log 132 | local_settings.py 133 | db.sqlite3 134 | db.sqlite3-journal 135 | 136 | # Flask stuff: 137 | instance/ 138 | .webassets-cache 139 | 140 | # Scrapy stuff: 141 | .scrapy 142 | 143 | # Sphinx documentation 144 | docs/_build/ 145 | 146 | # PyBuilder 147 | .pybuilder/ 148 | target/ 149 | 150 | # Jupyter Notebook 151 | .ipynb_checkpoints 152 | 153 | # IPython 154 | profile_default/ 155 | ipython_config.py 156 | 157 | # pyenv 158 | # For a library or package, you might want to ignore these files since the code is 159 | # intended to run in multiple environments; otherwise, check them in: 160 | # .python-version 161 | 162 | # pipenv 163 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 164 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 165 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 166 | # install all needed dependencies. 167 | #Pipfile.lock 168 | 169 | # poetry 170 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 171 | # This is especially recommended for binary packages to ensure reproducibility, and is more 172 | # commonly ignored for libraries. 173 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 174 | #poetry.lock 175 | 176 | # pdm 177 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 178 | #pdm.lock 179 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 180 | # in version control. 181 | # https://pdm.fming.dev/#use-with-ide 182 | .pdm.toml 183 | 184 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 185 | __pypackages__/ 186 | 187 | # Celery stuff 188 | celerybeat-schedule 189 | celerybeat.pid 190 | 191 | # SageMath parsed files 192 | *.sage.py 193 | 194 | # Environments 195 | .env 196 | .venv 197 | env/ 198 | venv/ 199 | ENV/ 200 | env.bak/ 201 | venv.bak/ 202 | 203 | # Spyder project settings 204 | .spyderproject 205 | .spyproject 206 | 207 | # Rope project settings 208 | .ropeproject 209 | 210 | # mkdocs documentation 211 | /site 212 | 213 | # mypy 214 | .mypy_cache/ 215 | .dmypy.json 216 | dmypy.json 217 | 218 | # Pyre type checker 219 | .pyre/ 220 | 221 | # pytype static type analyzer 222 | .pytype/ 223 | 224 | # Cython debug symbols 225 | cython_debug/ 226 | 227 | # PyCharm 228 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 229 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 230 | # and can be added to the global gitignore or merged into this file. For a more nuclear 231 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 232 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | cd lambda-layers && $(MAKE) 3 | 4 | clean: 5 | cd lambda-layers && $(MAKE) clean 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :trumpet: AWS CDK FASTAPI LAMBDA :trumpet: 2 | 3 |
4 | 5 | ## Overview 🔮 6 | 7 | This is a custom example API project deployed on AWS with the following specifications: 8 | 9 | - Infrastructure as Code with [AWS CDK-Python](https://aws.amazon.com/cdk/) 10 | - Source Code with [AWS Lambda Functions](https://aws.amazon.com/lambda/) built with [Python Runtime](https://www.python.org) 11 | - API Framework with [FastAPI](https://fastapi.tiangolo.com) 12 | - Tests with [PyTest Framework](https://docs.pytest.org/) 13 | - Dependencies and Environments managed with [Python Poetry](https://python-poetry.org) 14 | 15 | This project was inspired by the following videos: 16 | 17 | - [Werner Vogels on the AWS Cloud Development Kit (AWS CDK)](https://youtu.be/AYYTrDaEwLs) 18 | 19 | The information of this repository is based on many online resources, so feel free to use it as a guide for your future projects!.
20 | 21 | ## How to run this project? :dizzy: 22 | 23 | All projects are well commented (even over-commented sometimes for clarity).
24 | 25 | The necessary commands to deploy/destroy the solution can be found at: 26 | 27 | - [`important_commands.sh`](important_commands.sh) 28 | 29 | > Note: please update the commands based on your needs (account, region, etc...) 30 | 31 | ## AWS CDK :cloud: 32 | 33 | [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) is an amazing open-source software development framework to programmatically define cloud-based applications with familiar languages.
34 | 35 | My personal opinion is that you should learn about CDK when you feel comfortable with cloud-based solutions with IaC on top of [AWS Cloudformation](https://aws.amazon.com/cloudformation/). At that moment, I suggest that if you need to enhance your architectures, it's a good moment to use these advanced approaches.
36 | 37 | The best way to start is from the [Official AWS Cloud Development Kit (AWS CDK) v2 Documentation](https://docs.aws.amazon.com/cdk/v2/guide/home.html).
38 | 39 | ## Dependencies :vertical_traffic_light: 40 | 41 | The dependencies are explained in detail for each project, but the most important ones are Node, Python and the AWS-CDK libraries.
42 | 43 | My advice is to primary understand the basics on how CDK works, and then, develop amazing projects with this incredible AWS tool!.
44 | 45 | ### Software dependencies (based on project) 46 | 47 | - [Visual Studio Code](https://code.visualstudio.com/)
48 | Visual Studio Code is my main code editor for high-level programming. This is not absolutely necessary, but from my experience, it gives us a great performance and outstanding extensions to level-up our software development.
49 | 50 | - [NodeJs](https://nodejs.org/en/)
51 | NodeJs is a JavaScript runtime built on Chrome's V8 JavaScript engine programming language. The community is amazing and lets us handle async functionalities in elegant ways. In this case, we need it for the main "CDK" library, that is built on top of NodeJS.
52 | 53 | - [Python](https://www.python.org/)
54 | Python is an amazing dynamic programming language that allow us to work fast, with easy and powerful integration with different software solutions. We will use the Python CDK libraries.
55 | 56 | ### Libraries and Package dependencies (depending on the scenario) 57 | 58 | - [CDK CLI (Toolkit)](https://docs.aws.amazon.com/cdk/v2/guide/cli.html)
59 | To work with the CDK, it is important to install the main toolkit as a NodeJs global dependency. Then, feel free to install the specific language AWS-CDK library (for example: [aws-cdk.core](https://pypi.org/project/aws-cdk.core/)).
60 | 61 | - [AWS CLI](https://aws.amazon.com/cli/)
62 | The AWS Command Line Interface (AWS CLI) is a unified tool to manage your AWS services. We will use it for connecting to our AWS account from the terminal (authentication and authorization towards AWS).
63 | 64 | ## Special thanks :gift: 65 | 66 | - Thanks to all contributors of the great OpenSource projects that I am using.
67 | 68 | ## Author :musical_keyboard: 69 | 70 | ### Santiago Garcia Arango 71 | 72 | 73 | 74 | 77 | 80 | 81 |
75 |

Curious DevOps Engineer passionate about advanced cloud-based solutions and deployments in AWS. I am convinced that today's greatest challenges must be solved by people that love what they do.

76 |
78 |

79 |
82 | 83 | ## LICENSE 84 | 85 | Copyright 2023 Santiago Garcia Arango. 86 | -------------------------------------------------------------------------------- /assets/SantiagoGarciaArangoCDK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/assets/SantiagoGarciaArangoCDK.png -------------------------------------------------------------------------------- /assets/aws_cdk_fastapi_lambda.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /assets/aws_cdk_fastapi_lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/assets/aws_cdk_fastapi_lambda.png -------------------------------------------------------------------------------- /cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "main_resources_name": "fastapi-lambda-cdk", 3 | "tags": { 4 | "Owner": "Santiago Garcia Arango", 5 | "Source": "https://github.com/san99tiago/aws-fastapi-lambda", 6 | "Usage": "Sample project to illustrate a quick easy FastAPI deployment on Lambda Functions" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "make install && python3 cdk/app.py" 3 | } 4 | -------------------------------------------------------------------------------- /cdk/add_tags.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | 3 | 4 | def add_tags_to_app( 5 | app: cdk.App, main_resources_name: str, deployment_environment: str 6 | ) -> None: 7 | """ 8 | Function to add custom tags to app in a centralized fashion. 9 | 10 | :param app: (aws_cdk.App) to apply tags to. 11 | :param main_resources_name: (str) the main solution name being deployed. 12 | :param deployment_environment: (str) value of the tag "environment". 13 | """ 14 | 15 | app_tags = cdk.Tags.of(app) 16 | app_tags.add("MainResourcesName", main_resources_name) 17 | app_tags.add("Environment", deployment_environment) 18 | 19 | # Add tags from CDK context 20 | context_tags = app.node.try_get_context("tags") 21 | for key in context_tags: 22 | app_tags.add(key, context_tags[key]) 23 | -------------------------------------------------------------------------------- /cdk/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ################################################################################ 4 | # CDK SOLUTION FOR: APIGATEWAY-SQS-LAMBDA (TEMPLATE) 5 | ################################################################################ 6 | 7 | # Built-in imports 8 | import os 9 | 10 | # External imports 11 | import aws_cdk as cdk 12 | 13 | # Own imports 14 | import add_tags 15 | from stacks.cdk_lambda_fastapi_stack import LambdaFunctionFastAPIStack 16 | 17 | 18 | print("--> Deployment AWS configuration (safety first):") 19 | print("CDK_DEFAULT_ACCOUNT", os.environ.get("CDK_DEFAULT_ACCOUNT")) 20 | print("CDK_DEFAULT_REGION", os.environ.get("CDK_DEFAULT_REGION")) 21 | 22 | 23 | app: cdk.App = cdk.App() 24 | 25 | 26 | # Configurations for the deployment (obtained from env vars and CDK context) 27 | DEPLOYMENT_ENVIRONMENT = os.environ.get("DEPLOYMENT_ENVIRONMENT", "dev") 28 | NAME_PREFIX = os.environ.get("NAME_PREFIX", "") 29 | MAIN_RESOURCES_NAME = app.node.try_get_context("main_resources_name") 30 | 31 | 32 | stack = LambdaFunctionFastAPIStack( 33 | app, 34 | "{}-{}".format(MAIN_RESOURCES_NAME, DEPLOYMENT_ENVIRONMENT), 35 | NAME_PREFIX, 36 | MAIN_RESOURCES_NAME, 37 | DEPLOYMENT_ENVIRONMENT, 38 | env={ 39 | "account": os.environ.get("CDK_DEFAULT_ACCOUNT"), 40 | "region": os.environ.get("CDK_DEFAULT_REGION"), 41 | }, 42 | description="Stack for {} infrastructure in {} environment".format( 43 | MAIN_RESOURCES_NAME, DEPLOYMENT_ENVIRONMENT 44 | ), 45 | ) 46 | 47 | add_tags.add_tags_to_app( 48 | stack, 49 | MAIN_RESOURCES_NAME, 50 | DEPLOYMENT_ENVIRONMENT, 51 | ) 52 | 53 | app.synth() 54 | -------------------------------------------------------------------------------- /cdk/stacks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/cdk/stacks/__init__.py -------------------------------------------------------------------------------- /cdk/stacks/cdk_lambda_fastapi_stack.py: -------------------------------------------------------------------------------- 1 | # Built-in imports 2 | import os 3 | 4 | # External imports 5 | from aws_cdk import ( 6 | Stack, 7 | Duration, 8 | CfnOutput, 9 | aws_lambda, 10 | RemovalPolicy, 11 | ) 12 | from constructs import Construct 13 | 14 | 15 | class LambdaFunctionFastAPIStack(Stack): 16 | """ 17 | Class to create the infrastructure on AWS. 18 | """ 19 | 20 | def __init__( 21 | self, 22 | scope: Construct, 23 | construct_id: str, 24 | name_prefix: str, 25 | main_resources_name: str, 26 | deployment_environment: str, 27 | **kwargs, 28 | ) -> None: 29 | super().__init__(scope, construct_id, **kwargs) 30 | 31 | # Input parameters 32 | self.construct_id = construct_id 33 | self.name_prefix = name_prefix 34 | self.main_resources_name = main_resources_name 35 | self.deployment_environment = deployment_environment 36 | 37 | # Main methods for the deployment 38 | self.create_lambda_layers() 39 | self.create_lambda_functions() 40 | 41 | # Create CloudFormation outputs 42 | self.generate_cloudformation_outputs() 43 | 44 | def create_lambda_layers(self): 45 | """ 46 | Create the Lambda layers that are necessary for the additional runtime 47 | dependencies of the Lambda Functions. 48 | """ 49 | 50 | # Layer for "FastAPI" and "Mangum" Adapter libraries 51 | self.lambda_layer_fastapi = aws_lambda.LayerVersion( 52 | self, 53 | "LambdaLayer-FastAPI", 54 | layer_version_name=f"{self.main_resources_name}-{self.deployment_environment}", 55 | code=aws_lambda.Code.from_asset("lambda-layers/fastapi/modules"), 56 | compatible_runtimes=[ 57 | aws_lambda.Runtime.PYTHON_3_11, 58 | aws_lambda.Runtime.PYTHON_3_12, 59 | ], 60 | description="Lambda Layer for Python with library", 61 | compatible_architectures=[aws_lambda.Architecture.X86_64], 62 | removal_policy=RemovalPolicy.DESTROY, 63 | ) 64 | 65 | def create_lambda_functions(self): 66 | """ 67 | Create the Lambda Functions for the FastAPI server. 68 | """ 69 | # Get relative path for folder that contains Lambda function source 70 | # ! Note--> we must obtain parent dirs to create path (that"s why there is "os.path.dirname()") 71 | PATH_TO_LAMBDA_FUNCTION_FOLDER = os.path.join( 72 | os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 73 | "src", 74 | "lambdas", 75 | ) 76 | self.lambda_fastapi: aws_lambda.Function = aws_lambda.Function( 77 | self, 78 | "Lambda-FastAPI", 79 | function_name=f"{self.main_resources_name}-{self.deployment_environment}", 80 | runtime=aws_lambda.Runtime.PYTHON_3_12, 81 | handler="api/main.handler", 82 | code=aws_lambda.Code.from_asset(PATH_TO_LAMBDA_FUNCTION_FOLDER), 83 | timeout=Duration.seconds(20), 84 | memory_size=128, 85 | environment={ 86 | "ENVIRONMENT": self.deployment_environment, 87 | "LOG_LEVEL": "DEBUG", 88 | }, 89 | layers=[ 90 | self.lambda_layer_fastapi, 91 | ], 92 | ) 93 | 94 | # NOTE: If IAM-based based auth needed, update the "auth_type" to "AWS_IAM" 95 | self.lambda_function_url = self.lambda_fastapi.add_function_url( 96 | auth_type=aws_lambda.FunctionUrlAuthType.NONE 97 | ) 98 | 99 | def generate_cloudformation_outputs(self): 100 | """ 101 | Method to add the relevant CloudFormation outputs. 102 | """ 103 | 104 | CfnOutput( 105 | self, 106 | "DeploymentEnvironment", 107 | value=self.deployment_environment, 108 | description="Deployment environment", 109 | ) 110 | 111 | CfnOutput( 112 | self, 113 | "LambdaFunctionRootUrl", 114 | value=self.lambda_function_url.url, 115 | description="Root URL to invoke Lambda Function", 116 | ) 117 | 118 | CfnOutput( 119 | self, 120 | "LambdaFunctionDocsUrl", 121 | value=f"{self.lambda_function_url.url}docs", 122 | description="Documentation URL to invoke Lambda Function", 123 | ) 124 | -------------------------------------------------------------------------------- /deploy_backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #################################################################################################### 4 | # STEPS EXECUTED TO DEPLOY THE BACKEND PROJECT (SIMPLIFIED WITHOUT POETRY TOOL) 5 | #################################################################################################### 6 | 7 | # Install Python dependencies with Poetry 8 | pip install -r requirements.txt 9 | 10 | # Configure deployment environment 11 | export AWS_DEFAULT_REGION=us-east-1 12 | export DEPLOYMENT_ENVIRONMENT=prod 13 | 14 | # Initialize CDK (Cloud Development Kit) 15 | ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) 16 | cdk bootstrap aws://${ACCOUNT_ID}/us-east-1 17 | 18 | # Deploy the backend 19 | cdk deploy --require-approval never 20 | -------------------------------------------------------------------------------- /important_commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # PART 1: Configure NodeJs, Python and CDK libraries 5 | ################################################################################ 6 | 7 | # Install NodeJs and Python 8 | # --> https://nodejs.org/en/download/ 9 | # --> https://www.python.org/downloads/ 10 | 11 | # Verify that NodeJs/npm is installed correctly 12 | node --version 13 | npm --version 14 | 15 | # Verify that Python/pip is installed correctly 16 | python --version || python3 --version 17 | pip --version || pip3 --version 18 | 19 | # Install AWS-CDK (on NodeJs) 20 | sudo npm install -g aws-cdk 21 | 22 | # Verify correct install of AWS-CDK 23 | npm list --global | grep aws-cdk 24 | 25 | 26 | ################################################################################ 27 | # PART 2: Initial Project Setup (Only run these at the beginning) 28 | ################################################################################ 29 | 30 | # Configure AWS credentials (follow steps) 31 | aws configure 32 | # --> Alternative 1: Environment variables added to terminal session 33 | # --> Alternative 2: AWS Cloud9 with the right permissions 34 | 35 | # Bootstrap CDK (provision initial resources to work with CDK.. S3, roles, etc) 36 | #! Change "ACCOUNT-NUMBER" and "REGION" to your needed values 37 | cdk bootstrap aws://ACCOUNT-NUMBER/REGION 38 | 39 | # Install poetry (for managing Python dependencies) 40 | pip install poetry 41 | 42 | # Install poetry dependencies for the virtual environment 43 | poetry install 44 | 45 | 46 | ################################################################################ 47 | # PART 3: Main CDK and Python commands (most used) 48 | ################################################################################ 49 | 50 | # Activate Python virtual environment with Poetry tool 51 | poetry shell 52 | 53 | # Run unit tests 54 | poe test-unit 55 | 56 | # Deploy commands 57 | export DEPLOYMENT_ENVIRONMENT=dev 58 | cdk synth 59 | cdk deploy 60 | 61 | 62 | ################################################################################ 63 | # PART 4: Other CDK usefull commands 64 | ################################################################################ 65 | 66 | # Help 67 | cdk --help 68 | cdk deploy --help 69 | 70 | # Lists the stacks in the app 71 | cdk list 72 | 73 | # Synthesizes and prints the CloudFormation template for the specified stack(s) 74 | cdk synthesize 75 | 76 | # Deploys the CDK Toolkit staging stack (necessary resources in AWS account) 77 | cdk bootstrap 78 | 79 | # Deploys the specified stack(s) 80 | cdk deploy 81 | 82 | # Destroys the specified stack(s) 83 | cdk destroy 84 | 85 | # Compares the specified stack with the deployed stack or a local CloudFormation template 86 | cdk diff 87 | 88 | # Displays metadata about the specified stack 89 | cdk metadata 90 | 91 | # Creates a new CDK project in the current directory from a specified template 92 | cdk init 93 | 94 | # Manages cached context values 95 | cdk context 96 | 97 | # Opens the CDK API reference in your browser 98 | cdk docs 99 | 100 | # Checks your CDK project for potential problems 101 | cdk doctor 102 | -------------------------------------------------------------------------------- /lambda-layers/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | cd fastapi && $(MAKE) 3 | 4 | clean: 5 | cd fastapi && $(MAKE) clean 6 | -------------------------------------------------------------------------------- /lambda-layers/fastapi/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | [ -d "modules/python" ] || pip install -r requirements.txt -t modules/python/ --platform manylinux2014_x86_64 --python-version 3.12 --implementation cp --only-binary=:all: --upgrade 3 | 4 | clean: 5 | rm -rf modules 6 | -------------------------------------------------------------------------------- /lambda-layers/fastapi/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.109.0 2 | mangum==0.17.0 3 | pydantic==2.6.1 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aws-fastapi-lambda" 3 | version = "1.0.0" 4 | description = "Sample project to illustrate a real quick FastAPI deployment on Lambda Functions" 5 | authors = ["Santiago Garcia Arango "] 6 | license = "Apache" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.11" 11 | aws-cdk-lib = "2.130.0" 12 | constructs = ">=10.0.0,<11.0.0" 13 | 14 | [tool.poetry.group.dev.dependencies] 15 | poethepoet = "^0.24.0" 16 | pytest = "^7.4.4" 17 | pytest-mock = "^3.12.0" 18 | coverage = "^7.4.0" 19 | black = "^23.12.1" 20 | boto3 = "^1.34.14" 21 | moto = "^4.2.13" 22 | aws-lambda-powertools = {version = "^2.31.0"} 23 | fastapi = {extras = ["all"], version = "^0.109.0"} 24 | mangum = "^0.17.0" 25 | pydantic = "^2.5.3" 26 | 27 | [tool.pytest.ini_options] 28 | minversion = "7.0" 29 | pythonpath = [ 30 | "cdk", 31 | "src/lambdas", 32 | ] 33 | 34 | [build-system] 35 | requires = ["poetry-core"] 36 | build-backend = "poetry.core.masonry.api" 37 | 38 | [tool.poe.tasks] 39 | local-fastapi = "uvicorn src.lambdas.api.main:app --reload" 40 | test-unit = ["_make","_test_unit", "_coverage_html"] 41 | test-unit-lambdas = ["_test_unit_lambdas", "_coverage_html"] 42 | test-unit-cdk = ["_make","_test_unit_cdk", "_coverage_html"] 43 | black-format = "black ." 44 | black-check = "black . --check --diff -v" 45 | _make = "make all" 46 | _test_unit = "coverage run -m pytest tests/unit" 47 | _test_unit_lambdas = "coverage run -m pytest tests/unit/lambdas" 48 | _test_unit_cdk = "coverage run -m pytest tests/unit/cdk" 49 | _coverage_html = "coverage html" 50 | 51 | [tool.coverage.run] 52 | branch = true 53 | source = ["src", "cdk"] 54 | omit = [ 55 | "**/__init__.py" 56 | ] 57 | 58 | [tool.coverage.report] 59 | show_missing = false 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.130.0 2 | constructs==10.3.0 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/src/__init__.py -------------------------------------------------------------------------------- /src/lambdas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/src/lambdas/__init__.py -------------------------------------------------------------------------------- /src/lambdas/api/main.py: -------------------------------------------------------------------------------- 1 | # Built-in imports 2 | import os 3 | from typing import Union 4 | 5 | # External imports 6 | from fastapi import FastAPI 7 | from mangum import Mangum 8 | from pydantic import BaseModel 9 | 10 | 11 | app = FastAPI( 12 | description="Simple FastAPI server that runs on top of Lambda Functions.", 13 | contact={"Santiago Garcia Arango": "san99tiago@gmail.com"}, 14 | title="Simple FastAPI Example", 15 | version="1.0.0", 16 | ) 17 | 18 | 19 | class Item(BaseModel): 20 | name: str 21 | price: float 22 | is_offer: Union[bool, None] = None 23 | 24 | 25 | @app.get("/") 26 | async def root(): 27 | return {"message": "Hello by Santi"} 28 | 29 | 30 | @app.get("/items/{item_id}") 31 | def read_item(item_id: int, q: Union[str, None] = None): 32 | return {"item_id": item_id, "q": q} 33 | 34 | 35 | @app.put("/items/{item_id}") 36 | def update_item(item_id: int, item: Item): 37 | return {"item_name": item.name, "item_id": item_id} 38 | 39 | 40 | # This is the Lambda Function's entrypoint (handler) 41 | handler = Mangum(app) 42 | -------------------------------------------------------------------------------- /terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/archive" { 5 | version = "2.4.2" 6 | hashes = [ 7 | "h1:1eOz9vM/55vnQjxk23RhnYga7PZq8n2rGxG+2Vx2s6w=", 8 | "zh:08faed7c9f42d82bc3d406d0d9d4971e2d1c2d34eae268ad211b8aca57b7f758", 9 | "zh:3564112ed2d097d7e0672378044a69b06642c326f6f1584d81c7cdd32ebf3a08", 10 | "zh:53cd9afd223c15828c1916e68cb728d2be1cbccb9545568d6c2b122d0bac5102", 11 | "zh:5ae4e41e3a1ce9d40b6458218a85bbde44f21723943982bca4a3b8bb7c103670", 12 | "zh:5b65499218b315b96e95c5d3463ea6d7c66245b59461217c99eaa1611891cd2c", 13 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 14 | "zh:7f45b35a8330bebd184c2545a41782ff58240ed6ba947274d9881dd5da44b02e", 15 | "zh:87e67891033214e55cfead1391d68e6a3bf37993b7607753237e82aa3250bb71", 16 | "zh:de3590d14037ad81fc5cedf7cfa44614a92452d7b39676289b704a962050bc5e", 17 | "zh:e7e6f2ea567f2dbb3baa81c6203be69f9cd6aeeb01204fd93e3cf181e099b610", 18 | "zh:fd24d03c89a7702628c2e5a3c732c0dede56fa75a08da4a1efe17b5f881c88e2", 19 | "zh:febf4b7b5f3ff2adff0573ef6361f09b6638105111644bdebc0e4f575373935f", 20 | ] 21 | } 22 | 23 | provider "registry.terraform.io/hashicorp/aws" { 24 | version = "5.38.0" 25 | hashes = [ 26 | "h1:axFddT4mkdtZREgkDXwXdzZGm1qxheF0fLN7S7bJJX4=", 27 | "zh:0d58264440fd28b6729990b48d8fd61e732f5570689d17bbbc0c5f2324d3dd00", 28 | "zh:175e24a3d399495fc91da359cc30a9fe06b7eeb98804816abcf1493859f6d28e", 29 | "zh:244a1f56d6710cc1a643f602a185b46d3cd064f6df60330006f92ab32f3ff60c", 30 | "zh:30dd99413867b1be808b656551a2f0452e4e37787f963780c51f1f85bf406441", 31 | "zh:3629d4e212c8ffd8e74c4ab9e9d22ca7fff803052366d011c014591fa65beb48", 32 | "zh:521badb184bbdde5dddb1228f7a241997db52ea51c9f8039ed5a626362952cf4", 33 | "zh:5580a937e1f5fa59c16c4b9802079aa45a16c7c69e5b7d4e97aebf2c0fb4bd00", 34 | "zh:87b801057d492ff0adc82ce6251871d87bdf5890749fe5753f447ec6fe4710ff", 35 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 36 | "zh:9c44e0c143f1d021440e9c448a9bc595f51a95e6cc382fcffe9db6d3b17f24c2", 37 | "zh:b7e6b7b182932a3dbb6ca5f8ebb8d37befe1456f3dffaafb37cee07dc0473696", 38 | "zh:d43fcf4f59cf79b1be3bec164d95fe9edc3fe39195a83226b911918a6538c8b3", 39 | "zh:ec3e383ce1e414f0bd7d3fe73409ff7d2777a5da27248b70fd5df1df323d920b", 40 | "zh:f729b443179bb115bbcbb0369fe46640de1c6dbd627b52694e9b3b8a41ec7881", 41 | "zh:fd532b707746145d3c6d3507bca2b8d44cc618b3d5006db99426221b71db7da7", 42 | ] 43 | } 44 | 45 | provider "registry.terraform.io/hashicorp/null" { 46 | version = "3.2.2" 47 | hashes = [ 48 | "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", 49 | "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", 50 | "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", 51 | "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3", 52 | "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606", 53 | "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546", 54 | "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539", 55 | "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452", 56 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 57 | "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422", 58 | "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae", 59 | "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1", 60 | "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e", 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /terraform/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | bucket = "san99tiago-terraform-backend-dev" # Update to another backend as needed 4 | key = "terraform.fastapi.json" 5 | region = "us-east-1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /terraform/lambda.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "lambda_trust_policy" { 2 | statement { 3 | effect = "Allow" 4 | actions = ["sts:AssumeRole"] 5 | principals { 6 | type = "Service" 7 | identifiers = ["lambda.amazonaws.com"] 8 | } 9 | } 10 | } 11 | 12 | resource "aws_iam_role" "lambda_role" { 13 | name = "${var.main_resources_name}-role-${var.environment}" 14 | assume_role_policy = data.aws_iam_policy_document.lambda_trust_policy.json 15 | } 16 | 17 | # Add "AWSLambdaBasicExecutionRole" to the role for the Lambda Function 18 | resource "aws_iam_role_policy_attachment" "lambda_basic_execution_role" { 19 | role = aws_iam_role.lambda_role.name 20 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 21 | } 22 | 23 | # Create ZIP file for the source code at deployment time 24 | data "archive_file" "lambda_source_package" { 25 | type = "zip" 26 | source_dir = "${local.src_root_path}/lambdas" 27 | output_path = "${local.src_root_path}/lambda_package.zip" 28 | } 29 | 30 | # Lambda Layer Install (Python dependencies) 31 | resource "null_resource" "lambda_layer_install_deps" { 32 | provisioner "local-exec" { 33 | command = "make install" 34 | working_dir = local.root_path 35 | } 36 | 37 | # Enforce to always execute the command 38 | triggers = { 39 | always_run = "${timestamp()}" 40 | } 41 | } 42 | 43 | # Create ZIP file for Lambda Layer (Python dependencies) 44 | data "archive_file" "lambda_layer_package" { 45 | type = "zip" 46 | source_dir = "${local.lambda_layers_root_path}/fastapi/modules" 47 | output_path = "${local.lambda_layers_root_path}/fastapi/modules/lambda_layer_package.zip" 48 | 49 | depends_on = [null_resource.lambda_layer_install_deps] 50 | } 51 | 52 | # Lambda Layer 53 | resource "aws_lambda_layer_version" "lambda_layer" { 54 | filename = "${local.lambda_layers_root_path}/fastapi/modules/lambda_layer_package.zip" 55 | layer_name = "${var.main_resources_name}-layer" 56 | compatible_runtimes = ["python3.12"] 57 | compatible_architectures = ["x86_64"] 58 | source_code_hash = data.archive_file.lambda_layer_package.output_base64sha256 # Enforce re-deploy on changes 59 | 60 | depends_on = [data.archive_file.lambda_layer_package] 61 | 62 | } 63 | 64 | resource "aws_lambda_function" "lambda" { 65 | function_name = "${var.main_resources_name}-${var.environment}" 66 | filename = "${local.src_root_path}/lambda_package.zip" 67 | handler = "api/main.handler" 68 | role = aws_iam_role.lambda_role.arn 69 | runtime = "python3.12" 70 | timeout = 20 71 | architectures = ["x86_64"] 72 | layers = [aws_lambda_layer_version.lambda_layer.arn] 73 | source_code_hash = data.archive_file.lambda_source_package.output_base64sha256 # Enforce re-deploy on changes 74 | 75 | 76 | environment { 77 | variables = { 78 | ENVIRONMENT = var.environment 79 | } 80 | } 81 | 82 | depends_on = [ 83 | data.archive_file.lambda_source_package, 84 | data.archive_file.lambda_layer_package, 85 | ] 86 | 87 | } 88 | 89 | resource "aws_lambda_function_url" "lambda_url" { 90 | function_name = aws_lambda_function.lambda.function_name 91 | authorization_type = "NONE" 92 | } 93 | -------------------------------------------------------------------------------- /terraform/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # Paths for loading the code related to the Lambda Functions 3 | module_path = abspath(path.module) 4 | root_path = abspath("${path.module}/..") 5 | src_root_path = abspath("${path.module}/../src") 6 | lambda_layers_root_path = abspath("${path.module}/../lambda-layers") 7 | } 8 | -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | 4 | default_tags { 5 | tags = { 6 | "Owner": "Santiago Garcia Arango", 7 | "Source"= "https://github.com/san99tiago/aws-fastapi-lambda", 8 | "Usage"= "Sample project to illustrate a quick easy FastAPI deployment on Lambda Functions" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "environment" { 2 | type = string 3 | description = "Environment for the deployment" 4 | default = "dev" 5 | } 6 | 7 | variable "main_resources_name" { 8 | type = string 9 | description = "Main resources across the deployment" 10 | default = "fastapi-lambda" 11 | } 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/tests/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/cdk/test_cdk_lambda_fastapi.py: -------------------------------------------------------------------------------- 1 | # External imports 2 | import pytest 3 | import aws_cdk as core 4 | import aws_cdk.assertions as assertions 5 | 6 | # Own imports 7 | from cdk.stacks.cdk_lambda_fastapi_stack import LambdaFunctionFastAPIStack 8 | 9 | 10 | app: core.App = core.App() 11 | stack: LambdaFunctionFastAPIStack = LambdaFunctionFastAPIStack( 12 | app, 13 | "test", 14 | "test", 15 | "test", 16 | "test", 17 | ) 18 | template: assertions.Template = assertions.Template.from_stack(stack) 19 | 20 | 21 | def test_app_synthesize_ok(): 22 | app.synth() 23 | 24 | 25 | def test_lambda_function_created(): 26 | match = template.find_resources( 27 | type="AWS::Lambda::Function", 28 | ) 29 | assert len(match) == 1 30 | -------------------------------------------------------------------------------- /tests/unit/lambdas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/tests/unit/lambdas/__init__.py -------------------------------------------------------------------------------- /tests/unit/lambdas/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/san99tiago/aws-fastapi-lambda/f15a97d92bb1ab2cf37ab8d4b75d976f7f2c362b/tests/unit/lambdas/api/__init__.py -------------------------------------------------------------------------------- /tests/unit/lambdas/api/test_main.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from src.lambdas.api.main import app 3 | 4 | client = TestClient(app) 5 | 6 | 7 | def test_read_root(): 8 | response = client.get("/") 9 | assert response.status_code == 200 10 | assert response.json() == {"message": "Hello by Santi"} 11 | 12 | 13 | def test_read_status_no_query(): 14 | response = client.get("/items/123") 15 | assert response.status_code == 200 16 | assert response.json() == {"item_id": 123, "q": None} 17 | 18 | 19 | def test_read_status_with_query(): 20 | response = client.get("/items/456?q=Santi") 21 | assert response.status_code == 200 22 | assert response.json() == {"item_id": 456, "q": "Santi"} 23 | 24 | 25 | def test_update_item_success(): 26 | response = client.put( 27 | "/items/789", 28 | json={ 29 | "name": "test name", 30 | "price": 99.9, 31 | }, 32 | ) 33 | assert response.status_code == 200 34 | assert response.json() == {"item_name": "test name", "item_id": 789} 35 | 36 | 37 | def test_update_item_wrong_payload(): 38 | response = client.put( 39 | "/items/789", 40 | json={ 41 | "name": "test name", 42 | "amount": 99.9, # Intentionally wrong key (not in model) 43 | }, 44 | ) 45 | assert response.status_code == 422 46 | -------------------------------------------------------------------------------- /tests/unit/lambdas/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | 5 | @pytest.fixture(scope="session", autouse=True) 6 | def aws_credentials(): 7 | """Mocked AWS configuration for moto tests (mocks)""" 8 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 9 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 10 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 11 | os.environ["AWS_SESSION_TOKEN"] = "testing" 12 | os.environ["AWS_DEFAULT_REGION"] = "us-east-1" 13 | --------------------------------------------------------------------------------