├── serverless_flask ├── pages │ ├── __init__.py │ ├── test_index.py │ └── index.py ├── static │ └── style.css ├── templates │ └── index.html ├── lambda.py └── __init__.py ├── .flake8 ├── cdk.context.json ├── pytest.ini ├── .npmignore ├── cdk.json ├── jest.config.js ├── Pipfile ├── test ├── serverless-flask-stack.test.ts └── __snapshots__ │ └── serverless-flask-stack.test.ts.snap ├── conftest.py ├── package.json ├── tsconfig.json ├── LICENSE ├── bin └── serverless-flask.ts ├── .gitignore ├── Makefile ├── README.md ├── lib └── serverless-flask-stack.ts └── Pipfile.lock /serverless_flask/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /serverless_flask/static/style.css: -------------------------------------------------------------------------------- 1 | /* style goes here */ -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = E203 W503 -------------------------------------------------------------------------------- /cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "acknowledged-issue-numbers": [ 3 | 19836 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = build-python node_modules cdk.out .aws-sam .vscode .git -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/serverless-flask.ts", 3 | "context": { 4 | "stage": "dev" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "*" 8 | apig-wsgi = "*" 9 | certifi = "*" 10 | cryptography = "*" 11 | boto3 = "*" 12 | 13 | [dev-packages] 14 | pytest = "*" 15 | flake8 = "*" 16 | black = "*" 17 | 18 | [requires] 19 | python_version = "3.9" 20 | 21 | [pipenv] 22 | allow_prereleases = true 23 | -------------------------------------------------------------------------------- /serverless_flask/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Serverless Flask page 6 | 7 | 8 | 9 | 10 | 11 | Hi! I am running on {{app.config["SERVER_NAME"]}}. Page was generated at {{time}}. 12 | 13 | 14 | -------------------------------------------------------------------------------- /serverless_flask/pages/test_index.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask.testing import FlaskClient 3 | 4 | 5 | def test_index(test_client: FlaskClient): 6 | rv = test_client.get("/") 7 | assert "Hi! I am running on unittest.example.com" in rv.data.decode("utf-8") 8 | 9 | 10 | def test_example_json_api(test_client: FlaskClient): 11 | rv = test_client.get("/example_json_api") 12 | assert rv.headers["Content-Type"] == "application/json" 13 | data = json.loads(rv.data) 14 | assert "body" in data 15 | assert data["body"] == "ok" 16 | -------------------------------------------------------------------------------- /test/serverless-flask-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from 'aws-cdk-lib/assertions'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as ServerlessFlask from '../lib/serverless-flask-stack'; 4 | 5 | // super basic snapshot testing 6 | // See https://docs.aws.amazon.com/cdk/latest/guide/testing.html 7 | test('SnapshotTest', () => { 8 | const app = new cdk.App({context: { 9 | "stage": "prod" 10 | }}); 11 | // WHEN 12 | const stack = new ServerlessFlask.ServerlessFlaskStack(app, 'MyTestStack'); 13 | // THEN 14 | const template = Template.fromStack(stack); 15 | expect(template.toJSON()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from serverless_flask import create_app 3 | from flask import Flask 4 | from flask.testing import FlaskClient 5 | import pytest 6 | 7 | @pytest.fixture 8 | def test_app() -> Flask: 9 | app = create_app({ 10 | "DEBUG": False, 11 | "UNITTEST": True, 12 | "SEND_FILE_MAX_AGE_DEFAULT": 300, 13 | "PERMANENT_SESSION_LIFETIME": 86400}) 14 | 15 | return app 16 | 17 | @pytest.fixture 18 | def test_client(test_app: Flask) -> FlaskClient: 19 | test_app.config["SERVER_NAME"] = "unittest.example.com" 20 | with test_app.test_client() as c: 21 | yield c 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-flask-on-aws", 3 | "version": "0.2.0", 4 | "bin": { 5 | "serverless-flask-on-aws": "bin/serverless-flask-on-aws.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^26.0.24", 15 | "@types/node": "^10.17.60", 16 | "aws-cdk": "^1.190.0", 17 | "aws-cdk-lib": "^2.62.2", 18 | "constructs": "^10.1.235", 19 | "jest": "^26.6.3", 20 | "ts-jest": "^26.5.6", 21 | "ts-node": "^10.0.0", 22 | "typescript": "^4.0.0" 23 | }, 24 | "dependencies": { 25 | "source-map-support": "^0.5.21" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /serverless_flask/pages/index.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, current_app, make_response 2 | import time 3 | 4 | app = Blueprint("index", __name__, template_folder="../templates") 5 | 6 | 7 | @app.route("/") 8 | def index(): 9 | return render_template("index.html", app=current_app, time=round(time.time())) 10 | 11 | 12 | @app.route("/example_json_api") 13 | def example_json_api(): 14 | resp = make_response({"body": "ok", "time": round(time.time())}) 15 | resp.headers["Content-Type"] = "application/json" 16 | return resp 17 | 18 | 19 | @app.route("/example_json_api_no_cache") 20 | def example_json_api_no_cache(): 21 | resp = make_response({"body": "ok", "time": round(time.time())}) 22 | resp.headers["Content-Type"] = "application/json" 23 | resp.headers["Cache-Control"] = "no-store, max-age=0" 24 | return resp 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Woongbin Kang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bin/serverless-flask.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { ServerlessFlaskStack } from '../lib/serverless-flask-stack'; 5 | 6 | const app = new cdk.App(); 7 | const stackName = ({ 8 | "dev": "serverless-flask-dev", 9 | "staging": "serverless-flask-staging", 10 | "prod": "serverless-flask-prod" 11 | }as Record)[app.node.tryGetContext("stage") as string]; 12 | new ServerlessFlaskStack(app, 'ServerlessFlask', { 13 | stackName: stackName, 14 | /* If you don't specify 'env', this stack will be environment-agnostic. 15 | * Account/Region-dependent features and context lookups will not work, 16 | * but a single synthesized template can be deployed anywhere. */ 17 | 18 | /* Uncomment the next line to specialize this stack for the AWS Account 19 | * and Region that are implied by the current CLI configuration. */ 20 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 21 | 22 | /* Uncomment the next line if you know exactly what Account and Region you 23 | * want to deploy the stack to. */ 24 | // env: { account: '123456789012', region: 'us-east-1' }, 25 | 26 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 27 | }); 28 | -------------------------------------------------------------------------------- /serverless_flask/lambda.py: -------------------------------------------------------------------------------- 1 | import os 2 | from apig_wsgi import make_lambda_handler 3 | from serverless_flask import create_app 4 | 5 | 6 | def configure_logger(): 7 | from logging.config import dictConfig 8 | 9 | dictConfig( 10 | { 11 | "version": 1, 12 | "formatters": { 13 | "default": { 14 | "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s", 15 | } 16 | }, 17 | "handlers": { 18 | "wsgi": { 19 | "class": "logging.StreamHandler", 20 | "stream": "ext://flask.logging.wsgi_errors_stream", 21 | "formatter": "default", 22 | } 23 | }, 24 | "root": {"level": os.environ.get("ROOT_LOG_LEVEL", "INFO"), "handlers": ["wsgi"]}, 25 | } 26 | ) 27 | 28 | 29 | # entry point for Lambda 30 | 31 | configure_logger() 32 | app = create_app() 33 | 34 | inner_handler = make_lambda_handler(app, binary_support=True) 35 | 36 | 37 | def lambda_handler(event, context): 38 | try: 39 | app.logger.debug(event) 40 | headers = event["headers"] 41 | cf_host = headers.pop("X-Forwarded-Host", None) 42 | if cf_host: 43 | app.config["SERVER_NAME"] = cf_host 44 | # patch host header 45 | headers["Host"] = cf_host 46 | event["multiValueHeaders"]["Host"] = [cf_host] 47 | app.logger.info(f"Host header is successfully patched to {cf_host}") 48 | 49 | return inner_handler(event, context) 50 | except: # noqa 51 | app.logger.exception("Exception handling lambda") 52 | raise 53 | -------------------------------------------------------------------------------- /serverless_flask/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import os 3 | import json 4 | import logging 5 | 6 | DEFAULT_CACHE_DURATION = 300 7 | 8 | 9 | def ensure_secret_key_exists(app: Flask): 10 | import boto3 11 | import botocore 12 | import secrets 13 | 14 | # this is where the session secret goes 15 | key = "SECRET_KEY" 16 | s3 = boto3.resource("s3") 17 | bucket = app.config["S3_BUCKET"] 18 | obj = s3.Object(bucket, key) 19 | 20 | try: 21 | app.logger.debug(f"ensure_secret_key_exists: Checking for s3 bucket {bucket}") 22 | resp = obj.get() 23 | # the key already exists, so use that instead. 24 | app.config["SECRET_KEY"] = resp["Body"].read().decode("utf-8") 25 | except botocore.exceptions.ClientError as ex: 26 | if ex.response["Error"]["Code"] == "NoSuchKey": 27 | app.config["SECRET_KEY"] = secrets.token_hex() 28 | obj.put(Body=app.config["SECRET_KEY"].encode("utf-8")) 29 | app.logger.info("Created a new SECRET_KEY") 30 | else: 31 | raise 32 | 33 | 34 | def create_app(config_overrides={}) -> Flask: 35 | app = Flask("serverless-flask") 36 | # Apply a JSON config override from env var if exists 37 | if os.environ.get("JSON_CONFIG_OVERRIDE"): 38 | app.config.update(json.loads(os.environ.get("JSON_CONFIG_OVERRIDE"))) 39 | 40 | if os.environ.get("DEBUG", False): 41 | app.logger.setLevel(logging.DEBUG) 42 | 43 | app.config.update(config_overrides) 44 | 45 | import serverless_flask.pages.index 46 | 47 | app.register_blueprint(serverless_flask.pages.index.app) 48 | 49 | app.logger.debug("Config is: %r" % app.config) 50 | 51 | if not app.config.get("UNITTEST", False): 52 | ensure_secret_key_exists(app) 53 | 54 | cacheable_methods = set(["GET", "HEAD"]) 55 | 56 | @app.after_request 57 | def after_request(response): 58 | response.headers["X-Frame-Options"] = "SAMEORIGIN" 59 | response.headers["Content-Security-Policy"] = "frame-ancestors self" 60 | if request.method not in cacheable_methods: 61 | # don't cache if logged in or not cacheable 62 | response.headers["Cache-Control"] = "no-store" 63 | elif not response.headers.get("Cache-Control", False): 64 | # cache for 5 minutes by default, unless otherwise specified. 65 | response.headers["Cache-Control"] = "public, max-age=300" 66 | app.logger.info( 67 | "[from:%s|%s %s]+[%s]=>[%d|%dbytes]" 68 | % ( 69 | request.remote_addr, 70 | request.method, 71 | request.url, 72 | request.data, 73 | response.status_code, 74 | response.content_length, 75 | ) 76 | ) 77 | return response 78 | 79 | return app 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | *.js 131 | !jest.config.js 132 | *.d.ts 133 | node_modules 134 | 135 | # CDK asset staging directory 136 | .cdk.staging 137 | cdk.out 138 | 139 | # this is where temporary python build artifacts go 140 | build-python/ 141 | build-python-cdk/ 142 | 143 | # temp files 144 | requirements.txt 145 | .pipenv 146 | .deploy-dev-once 147 | sam-params.json 148 | 149 | # ide 150 | .vscode/ 151 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | release: node_modules .pipenv lint test bundle-python 2 | 3 | lint: 4 | pipenv run black --line-length 120 serverless_flask 5 | pipenv run flake8 serverless_flask 6 | 7 | clean: 8 | find -name '*.pyc' | xargs rm -f 9 | find -name '__pycache__' | xargs rm -rf 10 | rm -rf cdk.out/ 11 | rm -rf build-python/ 12 | rm -rf .aws-sam/ 13 | rm -f requirements.txt 14 | rm -f .deploy-dev-once 15 | rm -rf node_modules 16 | rm -f sam-params.json 17 | rm -f test/*.js 18 | rm -f .pipenv && pipenv --rm || true 19 | echo "Clean is finished" 20 | 21 | .pipenv: 22 | pipenv sync -d 23 | touch "$@" 24 | 25 | run-flask: .pipenv .deploy-dev-once 26 | # Reads the same environment variable that Lambda will use. 27 | FLASK_APP=serverless_flask:create_app \ 28 | JSON_CONFIG_OVERRIDE=`jq -r '."serverless-flask-dev".LambdaEnv' cdk.out/dev-stage-output.json` \ 29 | AWS_PROFILE=serverless-flask-dev \ 30 | FLASK_ENV=dev \ 31 | FLASK_DEBUG=1 \ 32 | pipenv run flask run --cert adhoc -h localhost -p 5000 33 | 34 | update-deps: clean 35 | pipenv update 36 | 37 | build-python: .pipenv 38 | mkdir -p "$@"/ 39 | echo "Building in $@/" 40 | pipenv lock -r > requirements.txt 41 | pip3 install -r requirements.txt -t "$@/" 42 | # prune botocore and boto3 because they come with Lambda runtime 43 | rm -rf "$@/boto3" 44 | rm -rf "$@/botocore" 45 | # prune other trash 46 | find "$@/" -name "__pycache__" -type d | xargs rm -rf 47 | find "$@/" -name "*.pyc" -type d | xargs rm -rf 48 | rm -rf "$@/{_pytest}" 49 | 50 | bundle-python: build-python 51 | echo "Copying local Python files" 52 | rsync -ah --exclude '*.pyc' serverless_flask "build-python/" 53 | echo "The Python bundle's size: $$(du -sh "build-python/")" 54 | 55 | pytest: 56 | pipenv run pytest -x 57 | 58 | npmtest: build-ts bundle-python 59 | npm run test 60 | 61 | test: pytest npmtest 62 | 63 | build-ts: 64 | npm run build 65 | 66 | node_modules: 67 | npm install 68 | 69 | 70 | deploy-dev: node_modules bundle-python 71 | cdk deploy -c stage=dev --outputs-file cdk.out/dev-stage-output.json 72 | 73 | .deploy-dev-once: node_modules 74 | cdk deploy -c stage=dev --outputs-file cdk.out/dev-stage-output.json 75 | touch $@ 76 | 77 | deploy-staging: release 78 | cdk deploy -c stage=staging 79 | 80 | deploy-prod: release 81 | cdk deploy -c stage=prod 82 | 83 | synth-dev: node_modules 84 | cdk synth -c stage=dev 85 | 86 | sam-params.json: 87 | jq -r '{"Parameters":{"JSON_CONFIG_OVERRIDE": ."serverless-flask-dev".LambdaEnv}}' cdk.out/dev-stage-output.json > "$@" 88 | 89 | sam-local: .deploy-dev-once bundle-python synth-dev sam-params.json 90 | sam local start-api -p 5000 -t cdk.out/ServerlessFlask.template.json \ 91 | -n sam-params.json 92 | rm -f sam-params.json 93 | 94 | .PHONY = clean run-flask bundle-python build-ts test pytest npmtest \ 95 | sam-local deploy-dev deploy-staging deploy-prod release \ 96 | update-deps synth-dev npm-install lint 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Flask Starter Kit 2 | 3 | This is a starter kit for hosting Flask on AWS using Serverless components. Find the more detailed explanations at [my blog](https://wbk.one/). 4 | 5 | ## Prerequisite 6 | 7 | ### AWS CLI 8 | 9 | Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html). 10 | 11 | ### AWS Account Setup 12 | 13 | You need an AWS account. 14 | 15 | You need to create an IAM user with Administrator access. Follow [this guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started_create-admin-group.html) to setup one. 16 | 17 | Create an access key, and configure your AWS CLI to use it by default: 18 | 19 | ```aws configure``` 20 | 21 | Alternatively, you can create a named profile and export AWS_PROFILE variable to point to that profile. 22 | 23 | ### Python 3.9+ 24 | 25 | You will need a recent Python version greater than 3.9. If you don't have it, you can use [pyenv](https://github.com/pyenv/pyenv) to install it. 26 | 27 | ### pipenv 28 | 29 | We will use pipenv to manage Python package dependencies. You can install it like this: 30 | 31 | `python3 -m pip install --user pipenv` 32 | 33 | Make sure to add the user package installation location to your PATH. Example: 34 | 35 | `export PATH="$(python3 -c 'import site; print(site.USER_BASE)')/bin:${PATH}"` 36 | 37 | ### NodeJS 38 | 39 | We need to [install NodeJS](https://nodejs.org/en/download/) so we can use [CDK v2](https://docs.aws.amazon.com/cdk/latest/guide/home.html). 40 | 41 | ### CDK 42 | 43 | Install the [CDK toolkit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html). 44 | 45 | ### Docker 46 | 47 | Install [docker](https://docs.docker.com/get-docker/). We need this for SAM. If you don't plan to run SAM you don't need it. 48 | 49 | ### SAM CLI 50 | 51 | Install [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). If you don't plan to run SAM you don't need it. 52 | 53 | ### Other tools 54 | 55 | You will need need `jq` 56 | 57 | ## Local Development 58 | 59 | ### IDE Recommendation 60 | 61 | If you open the folder with [Visual Studio Code](https://code.visualstudio.com/), everything should work out of the box. 62 | 63 | ### Entering the pipenv shell 64 | 65 | Run `pipenv sync -d` to install the dependencies. 66 | 67 | Run `pipenv shell` to enter the environment for the Python code. 68 | 69 | 70 | ### Install npm dependencies for CDK 71 | 72 | Run `npm install` to install all CDK dependencies. 73 | 74 | ### CDK Bootstrap 75 | 76 | CDK Bootstrap needs to be done once per region: `cdk bootstrap` 77 | 78 | ### Dev Stack Deployment 79 | 80 | The sample CDK app takes a context variable named `stage` which can be `dev`, `staging`, or `prod`. We need to deploy the `dev` stage stack once and create an IAM user to simulate the same permission as the lambda. 81 | 82 | The following snippet will auto-create a profile named `serverless-flask-dev`, which we can use to test the application using the same permissions as the lambda. 83 | 84 | ``` 85 | make deploy-dev 86 | profile=serverless-flask-dev 87 | creds=$(aws iam create-access-key --user-name $(jq -r '."serverless-flask-dev".devIamUser' cdk.out/dev-stage-output.json) --output json) 88 | aws configure set aws_access_key_id $(echo "$creds" | jq -r '.AccessKey.AccessKeyId') --profile $profile 89 | aws configure set aws_secret_access_key $(echo "$creds" | jq -r '.AccessKey.SecretAccessKey') --profile $profile 90 | aws configure set output json --profile $profile 91 | ``` 92 | 93 | ### Deploying the dev stage Stack 94 | 95 | Deploy the dev stack once. 96 | 97 | ``` 98 | make deploy-dev 99 | ``` 100 | 101 | If you want to rebuild your Python/NPM dependencies, just do `make clean`. 102 | 103 | ### Running the unit test 104 | 105 | Under pipenv shell: 106 | ``` 107 | make test 108 | ``` 109 | See `conftest.py` for the fixtures. See [pytest](https://pytest.org/) for more documentation. 110 | 111 | ### Running the local Flask server 112 | 113 | There is a convenient launcher to start the local Flask server with the same environment variable as the Lambda: 114 | 115 | ``` 116 | make run-flask 117 | ``` 118 | 119 | Your server will be running at https://localhost:5000/ 120 | 121 | Deploy to the sam-local stack which emulates a locally-running lambda more faithfully: 122 | 123 | ``` 124 | make sam-local 125 | ``` 126 | 127 | ### Run all tests 128 | 129 | `make` 130 | 131 | For CDK, there is only a very basic snapshot test. See [snapshot tests](https://docs.aws.amazon.com/cdk/latest/guide/testing.html#testing_snapshot). 132 | 133 | 134 | ## Deployment 135 | 136 | You can deploy either using the `staging` stage or `prod` stage. 137 | 138 | `staging` stage merely exists so you can have an environment that mirrors prod exactly. 139 | 140 | ### Deploy to Staging 141 | 142 | ``` 143 | make deploy-staging 144 | ``` 145 | 146 | Use the output value `ServerlessFlask.CDNDomain` to access the website. 147 | 148 | ### Deploy to Prod 149 | 150 | ``` 151 | make deploy-prod 152 | ``` 153 | 154 | Use the output value `ServerlessFlask.CDNDomain` to access the website. 155 | 156 | ### Upgrade Python Dependencies 157 | 158 | `pipenv update --outdated` 159 | 160 | ## CDK-inherited README.md 161 | 162 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 163 | 164 | ## Useful commands 165 | 166 | * `npm run build` compile typescript to js 167 | * `npm run watch` watch for changes and compile 168 | * `npm run test` perform the jest unit tests 169 | * `cdk deploy` deploy this stack to your default AWS account/region 170 | * `cdk diff` compare deployed stack with current state 171 | * `cdk synth` emits the synthesized CloudFormation template 172 | -------------------------------------------------------------------------------- /lib/serverless-flask-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; 3 | import * as agw from 'aws-cdk-lib/aws-apigateway'; 4 | import * as iam from 'aws-cdk-lib/aws-iam'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as eventTargets from "aws-cdk-lib/aws-events-targets"; 7 | import * as s3 from 'aws-cdk-lib/aws-s3'; 8 | import * as events from "aws-cdk-lib/aws-events"; 9 | import { RuleTargetInput } from 'aws-cdk-lib/aws-events'; 10 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 11 | import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; 12 | import * as logs from 'aws-cdk-lib/aws-logs'; 13 | import { BlockPublicAccess, BucketEncryption } from 'aws-cdk-lib/aws-s3'; 14 | import { Construct } from 'constructs'; 15 | 16 | const LAMBDA_CONFIG_ENV : {[key:string]: {[key:string]:any}} = { 17 | "dev": { 18 | "SESSION_COOKIE_SECURE": false, 19 | "DEBUG": true, 20 | "TEMPLATES_AUTO_RELOAD": true, 21 | "SEND_FILE_MAX_AGE_DEFAULT": 300, 22 | "PERMANENT_SESSION_LIFETIME": 86400, // 1 day 23 | "SERVER_NAME": "localhost:5000", 24 | "ROOT_LOG_LEVEL": "DEBUG" 25 | }, 26 | 'staging': { 27 | "SESSION_COOKIE_SECURE": true, 28 | "DEBUG": false, 29 | "TEMPLATES_AUTO_RELOAD": false, 30 | "SEND_FILE_MAX_AGE_DEFAULT": 300, 31 | "PERMANENT_SESSION_LIFETIME": 86400, // 1 day, 32 | "ROOT_LOG_LEVEL": "DEBUG" 33 | }, 34 | "prod": { 35 | "SESSION_COOKIE_SECURE": true, 36 | "DEBUG": false, 37 | "TEMPLATES_AUTO_RELOAD": false, 38 | "SEND_FILE_MAX_AGE_DEFAULT": 300, 39 | "PERMANENT_SESSION_LIFETIME": 86400, // 1 day 40 | "ROOT_LOG_LEVEL": "INFO" 41 | } 42 | }; 43 | 44 | const MAX_RPS = 100; 45 | const MAX_RPS_BUCKET_SIZE = 1000; 46 | 47 | export class ServerlessFlaskStack extends cdk.Stack { 48 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 49 | super(scope, id, {...props, analyticsReporting: false}); 50 | 51 | const stageName = this.node.tryGetContext("stage") as string; 52 | 53 | let appStore = new s3.Bucket(this, "S3Storage", { 54 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 55 | removalPolicy:RemovalPolicy.RETAIN, 56 | encryption: BucketEncryption.S3_MANAGED, 57 | }); 58 | 59 | 60 | // this is the lambda role 61 | let lambdaRole = new iam.Role(this, "LambdaRole", { 62 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 63 | inlinePolicies: { 64 | "lambda-executor": new iam.PolicyDocument({ 65 | assignSids: true, 66 | statements: [ 67 | new iam.PolicyStatement({ 68 | effect: iam.Effect.ALLOW, 69 | actions: ["ec2:DescribeTags", 70 | "cloudwatch:GetMetricStatistics", 71 | "cloudwatch:ListMetrics", 72 | "logs:CreateLogGroup", 73 | "logs:CreateLogStream", 74 | "logs:PutLogEvents", 75 | "logs:DescribeLogStreams"], 76 | resources: ["*"] 77 | }), 78 | new iam.PolicyStatement({ 79 | effect: iam.Effect.ALLOW, 80 | actions: ["lambda:InvokeFunction"], 81 | resources: ["*"] 82 | }) 83 | ] 84 | }) 85 | } 86 | }); 87 | 88 | 89 | let lambdaEnv = LAMBDA_CONFIG_ENV[stageName]; 90 | lambdaEnv["S3_BUCKET"] = appStore.bucketName; 91 | 92 | let webappLambda = new lambda.Function(this, "ServerlessFlaskLambda", { 93 | functionName: `serverless-flask-lambda-${stageName}`, 94 | code: lambda.Code.fromAsset(__dirname + "/../build-python/",), 95 | runtime: lambda.Runtime.PYTHON_3_9, 96 | handler: "serverless_flask.lambda.lambda_handler", 97 | role: lambdaRole, 98 | timeout: Duration.seconds(30), 99 | memorySize: 256, 100 | environment: {"JSON_CONFIG_OVERRIDE": JSON.stringify(lambdaEnv)}, 101 | // default is infinite, and you probably don't want it 102 | logRetention: logs.RetentionDays.SIX_MONTHS, 103 | }); 104 | 105 | 106 | let restApi = new agw.LambdaRestApi(this, "FlaskLambdaRestApi", { 107 | restApiName: `serverless-flask-api-${stageName}`, 108 | handler: webappLambda, 109 | binaryMediaTypes: ["*/*"], 110 | deployOptions: { 111 | throttlingBurstLimit: MAX_RPS_BUCKET_SIZE, 112 | throttlingRateLimit: MAX_RPS 113 | } 114 | }); 115 | const restApiUrl = `${restApi.restApiId}.execute-api.${this.region}.amazonaws.com`; 116 | 117 | if (this.node.tryGetContext("stage") !== "dev") { 118 | let cdn = new cloudfront.Distribution(this, "CDN", { 119 | defaultBehavior: { 120 | functionAssociations: [{ 121 | eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, 122 | function: new cloudfront.Function(this, "RewriteCdnHost", { 123 | functionName: `${this.account}${this.stackName}FixCdnHostFunction${stageName}`, 124 | // documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-example 125 | code: cloudfront.FunctionCode.fromInline(` 126 | function handler(event) { 127 | var req = event.request; 128 | if (req.headers['host']) { 129 | req.headers['x-forwarded-host'] = { 130 | value: req.headers['host'].value 131 | }; 132 | } 133 | return req; 134 | } 135 | `) 136 | }) 137 | }], 138 | origin: new origins.HttpOrigin(restApiUrl, { 139 | originPath: "/prod", 140 | protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY, 141 | connectionAttempts: 3, 142 | connectionTimeout: Duration.seconds(10), 143 | httpsPort: 443, 144 | }), 145 | smoothStreaming: false, 146 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 147 | cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, 148 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 149 | compress: true, 150 | cachePolicy: new cloudfront.CachePolicy(this, 'DefaultCachePolicy', { 151 | // need to be overriden because the names are not automatically randomized across stages 152 | cachePolicyName: `CachePolicy-${stageName}`, 153 | headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList("x-forwarded-host"), 154 | // allow Flask session variable 155 | cookieBehavior: cloudfront.CacheCookieBehavior.allowList("session"), 156 | queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(), 157 | maxTtl: Duration.hours(1), 158 | defaultTtl: Duration.minutes(5), 159 | enableAcceptEncodingGzip: true, 160 | enableAcceptEncodingBrotli: true 161 | }), 162 | }, 163 | priceClass: cloudfront.PriceClass.PRICE_CLASS_200, 164 | enabled: true, 165 | httpVersion: cloudfront.HttpVersion.HTTP2, 166 | }); 167 | new CfnOutput(this, "CDNDomain", { 168 | value: "https://" + cdn.distributionDomainName 169 | }); 170 | } 171 | 172 | const grantLambdaResourcePermissions = (entity: iam.IGrantable) => { 173 | appStore.grantReadWrite(entity); 174 | }; 175 | grantLambdaResourcePermissions(lambdaRole); 176 | 177 | // create dev user - not applicable for anything other than dev stage 178 | if (this.node.tryGetContext("stage") === "dev") { 179 | let localDevUser = new iam.User(this, "serverless-flask-local-dev"); 180 | new CfnOutput(this, "devIamUser", { 181 | value: localDevUser.userName 182 | }); 183 | grantLambdaResourcePermissions(localDevUser); 184 | 185 | // export the lambda variables for later use 186 | new CfnOutput(this, "LambdaEnv", { 187 | value: JSON.stringify(lambdaEnv) 188 | }); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "2ef0fbadf5e6a0f567055a25daebb89cabae9738655c31dc046b8641923f89fb" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "apig-wsgi": { 20 | "hashes": [ 21 | "sha256:642f79125c710ec02480fd0e1f25c6889ff5dc57db629b394698141545e11ad8", 22 | "sha256:d0df652689ddba9cb54e9b1ebaf9f2999c5af07f0308a1488f6d308faa707c5b" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.15.0" 26 | }, 27 | "boto3": { 28 | "hashes": [ 29 | "sha256:7ab7bb335b726e2f472b5c050028198d16338560c83c40b2bd2bd4e4018ec802", 30 | "sha256:d97176a7ffb37539bc53671cb0bf1c5b304f1c78bbd748553df549a9d4f92a9e" 31 | ], 32 | "index": "pypi", 33 | "version": "==1.26.84" 34 | }, 35 | "botocore": { 36 | "hashes": [ 37 | "sha256:0f976427ad0a2602624ba784b5db328a865c2e9e0cc1bb6d8cffb6c0a2d177e1", 38 | "sha256:a36f7f6f8eae5dbd4a1cc8cb6fc747f6315500541181eff2093ee0529fc8e4bc" 39 | ], 40 | "markers": "python_version >= '3.7'", 41 | "version": "==1.29.84" 42 | }, 43 | "certifi": { 44 | "hashes": [ 45 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 46 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 47 | ], 48 | "index": "pypi", 49 | "version": "==2022.12.7" 50 | }, 51 | "cffi": { 52 | "hashes": [ 53 | "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", 54 | "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", 55 | "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", 56 | "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", 57 | "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", 58 | "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", 59 | "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", 60 | "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", 61 | "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", 62 | "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", 63 | "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", 64 | "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", 65 | "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", 66 | "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", 67 | "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", 68 | "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", 69 | "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", 70 | "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", 71 | "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", 72 | "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", 73 | "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", 74 | "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", 75 | "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", 76 | "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", 77 | "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", 78 | "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", 79 | "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", 80 | "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", 81 | "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", 82 | "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", 83 | "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", 84 | "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", 85 | "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", 86 | "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", 87 | "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", 88 | "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", 89 | "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", 90 | "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", 91 | "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", 92 | "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", 93 | "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", 94 | "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", 95 | "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", 96 | "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", 97 | "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", 98 | "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", 99 | "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", 100 | "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", 101 | "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", 102 | "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", 103 | "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", 104 | "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", 105 | "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", 106 | "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", 107 | "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", 108 | "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", 109 | "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", 110 | "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", 111 | "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", 112 | "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", 113 | "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", 114 | "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", 115 | "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", 116 | "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" 117 | ], 118 | "version": "==1.15.1" 119 | }, 120 | "click": { 121 | "hashes": [ 122 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 123 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 124 | ], 125 | "markers": "python_version >= '3.7'", 126 | "version": "==8.1.3" 127 | }, 128 | "cryptography": { 129 | "hashes": [ 130 | "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", 131 | "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", 132 | "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", 133 | "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", 134 | "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", 135 | "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", 136 | "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", 137 | "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", 138 | "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", 139 | "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", 140 | "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", 141 | "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", 142 | "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", 143 | "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", 144 | "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", 145 | "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", 146 | "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", 147 | "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", 148 | "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", 149 | "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", 150 | "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", 151 | "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", 152 | "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" 153 | ], 154 | "index": "pypi", 155 | "version": "==39.0.2" 156 | }, 157 | "flask": { 158 | "hashes": [ 159 | "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", 160 | "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d" 161 | ], 162 | "index": "pypi", 163 | "version": "==2.2.3" 164 | }, 165 | "importlib-metadata": { 166 | "hashes": [ 167 | "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", 168 | "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" 169 | ], 170 | "markers": "python_version < '3.10'", 171 | "version": "==6.0.0" 172 | }, 173 | "itsdangerous": { 174 | "hashes": [ 175 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", 176 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" 177 | ], 178 | "markers": "python_version >= '3.7'", 179 | "version": "==2.1.2" 180 | }, 181 | "jinja2": { 182 | "hashes": [ 183 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 184 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 185 | ], 186 | "markers": "python_version >= '3.7'", 187 | "version": "==3.1.2" 188 | }, 189 | "jmespath": { 190 | "hashes": [ 191 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 192 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 193 | ], 194 | "markers": "python_version >= '3.7'", 195 | "version": "==1.0.1" 196 | }, 197 | "markupsafe": { 198 | "hashes": [ 199 | "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", 200 | "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", 201 | "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", 202 | "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", 203 | "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", 204 | "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", 205 | "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", 206 | "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", 207 | "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", 208 | "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", 209 | "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", 210 | "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", 211 | "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", 212 | "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", 213 | "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", 214 | "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", 215 | "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", 216 | "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", 217 | "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", 218 | "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", 219 | "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", 220 | "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", 221 | "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", 222 | "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", 223 | "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", 224 | "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", 225 | "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", 226 | "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", 227 | "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", 228 | "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", 229 | "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", 230 | "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", 231 | "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", 232 | "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", 233 | "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", 234 | "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", 235 | "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", 236 | "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", 237 | "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", 238 | "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", 239 | "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", 240 | "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", 241 | "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", 242 | "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", 243 | "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", 244 | "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", 245 | "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", 246 | "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", 247 | "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", 248 | "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" 249 | ], 250 | "markers": "python_version >= '3.7'", 251 | "version": "==2.1.2" 252 | }, 253 | "pycparser": { 254 | "hashes": [ 255 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 256 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 257 | ], 258 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 259 | "version": "==2.21" 260 | }, 261 | "python-dateutil": { 262 | "hashes": [ 263 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 264 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 265 | ], 266 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 267 | "version": "==2.8.2" 268 | }, 269 | "s3transfer": { 270 | "hashes": [ 271 | "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", 272 | "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" 273 | ], 274 | "markers": "python_version >= '3.7'", 275 | "version": "==0.6.0" 276 | }, 277 | "six": { 278 | "hashes": [ 279 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 280 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 281 | ], 282 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 283 | "version": "==1.16.0" 284 | }, 285 | "urllib3": { 286 | "hashes": [ 287 | "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", 288 | "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" 289 | ], 290 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 291 | "version": "==1.26.14" 292 | }, 293 | "werkzeug": { 294 | "hashes": [ 295 | "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", 296 | "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" 297 | ], 298 | "markers": "python_version >= '3.7'", 299 | "version": "==2.2.3" 300 | }, 301 | "zipp": { 302 | "hashes": [ 303 | "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", 304 | "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" 305 | ], 306 | "markers": "python_version >= '3.7'", 307 | "version": "==3.15.0" 308 | } 309 | }, 310 | "develop": { 311 | "attrs": { 312 | "hashes": [ 313 | "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", 314 | "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" 315 | ], 316 | "markers": "python_version >= '3.6'", 317 | "version": "==22.2.0" 318 | }, 319 | "black": { 320 | "hashes": [ 321 | "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd", 322 | "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555", 323 | "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481", 324 | "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468", 325 | "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9", 326 | "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a", 327 | "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958", 328 | "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580", 329 | "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26", 330 | "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32", 331 | "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8", 332 | "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753", 333 | "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b", 334 | "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074", 335 | "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651", 336 | "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24", 337 | "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6", 338 | "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad", 339 | "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac", 340 | "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221", 341 | "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06", 342 | "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27", 343 | "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648", 344 | "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739", 345 | "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104" 346 | ], 347 | "index": "pypi", 348 | "version": "==23.1.0" 349 | }, 350 | "click": { 351 | "hashes": [ 352 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 353 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 354 | ], 355 | "markers": "python_version >= '3.7'", 356 | "version": "==8.1.3" 357 | }, 358 | "exceptiongroup": { 359 | "hashes": [ 360 | "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e", 361 | "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23" 362 | ], 363 | "markers": "python_version < '3.11'", 364 | "version": "==1.1.0" 365 | }, 366 | "flake8": { 367 | "hashes": [ 368 | "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", 369 | "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" 370 | ], 371 | "index": "pypi", 372 | "version": "==6.0.0" 373 | }, 374 | "iniconfig": { 375 | "hashes": [ 376 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 377 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 378 | ], 379 | "markers": "python_version >= '3.7'", 380 | "version": "==2.0.0" 381 | }, 382 | "mccabe": { 383 | "hashes": [ 384 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 385 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 386 | ], 387 | "markers": "python_version >= '3.6'", 388 | "version": "==0.7.0" 389 | }, 390 | "mypy-extensions": { 391 | "hashes": [ 392 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 393 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 394 | ], 395 | "markers": "python_version >= '3.5'", 396 | "version": "==1.0.0" 397 | }, 398 | "packaging": { 399 | "hashes": [ 400 | "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", 401 | "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" 402 | ], 403 | "markers": "python_version >= '3.7'", 404 | "version": "==23.0" 405 | }, 406 | "pathspec": { 407 | "hashes": [ 408 | "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229", 409 | "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc" 410 | ], 411 | "markers": "python_version >= '3.7'", 412 | "version": "==0.11.0" 413 | }, 414 | "platformdirs": { 415 | "hashes": [ 416 | "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a", 417 | "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef" 418 | ], 419 | "markers": "python_version >= '3.7'", 420 | "version": "==3.1.0" 421 | }, 422 | "pluggy": { 423 | "hashes": [ 424 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 425 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 426 | ], 427 | "markers": "python_version >= '3.6'", 428 | "version": "==1.0.0" 429 | }, 430 | "pycodestyle": { 431 | "hashes": [ 432 | "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", 433 | "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" 434 | ], 435 | "markers": "python_version >= '3.6'", 436 | "version": "==2.10.0" 437 | }, 438 | "pyflakes": { 439 | "hashes": [ 440 | "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", 441 | "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" 442 | ], 443 | "markers": "python_version >= '3.6'", 444 | "version": "==3.0.1" 445 | }, 446 | "pytest": { 447 | "hashes": [ 448 | "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", 449 | "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4" 450 | ], 451 | "index": "pypi", 452 | "version": "==7.2.2" 453 | }, 454 | "tomli": { 455 | "hashes": [ 456 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 457 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 458 | ], 459 | "markers": "python_version < '3.11'", 460 | "version": "==2.0.1" 461 | }, 462 | "typing-extensions": { 463 | "hashes": [ 464 | "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", 465 | "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" 466 | ], 467 | "markers": "python_version < '3.10'", 468 | "version": "==4.5.0" 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /test/__snapshots__/serverless-flask-stack.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SnapshotTest 1`] = ` 4 | Object { 5 | "Outputs": Object { 6 | "CDNDomain": Object { 7 | "Value": Object { 8 | "Fn::Join": Array [ 9 | "", 10 | Array [ 11 | "https://", 12 | Object { 13 | "Fn::GetAtt": Array [ 14 | "CDN2330F4C0", 15 | "DomainName", 16 | ], 17 | }, 18 | ], 19 | ], 20 | }, 21 | }, 22 | "FlaskLambdaRestApiEndpoint2BF2C749": Object { 23 | "Value": Object { 24 | "Fn::Join": Array [ 25 | "", 26 | Array [ 27 | "https://", 28 | Object { 29 | "Ref": "FlaskLambdaRestApi4883B802", 30 | }, 31 | ".execute-api.", 32 | Object { 33 | "Ref": "AWS::Region", 34 | }, 35 | ".", 36 | Object { 37 | "Ref": "AWS::URLSuffix", 38 | }, 39 | "/", 40 | Object { 41 | "Ref": "FlaskLambdaRestApiDeploymentStageprodAA802E6B", 42 | }, 43 | "/", 44 | ], 45 | ], 46 | }, 47 | }, 48 | }, 49 | "Parameters": Object { 50 | "BootstrapVersion": Object { 51 | "Default": "/cdk-bootstrap/hnb659fds/version", 52 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 53 | "Type": "AWS::SSM::Parameter::Value", 54 | }, 55 | }, 56 | "Resources": Object { 57 | "CDN2330F4C0": Object { 58 | "Properties": Object { 59 | "DistributionConfig": Object { 60 | "DefaultCacheBehavior": Object { 61 | "AllowedMethods": Array [ 62 | "GET", 63 | "HEAD", 64 | "OPTIONS", 65 | "PUT", 66 | "PATCH", 67 | "POST", 68 | "DELETE", 69 | ], 70 | "CachePolicyId": Object { 71 | "Ref": "DefaultCachePolicyDDFA5BDC", 72 | }, 73 | "CachedMethods": Array [ 74 | "GET", 75 | "HEAD", 76 | "OPTIONS", 77 | ], 78 | "Compress": true, 79 | "FunctionAssociations": Array [ 80 | Object { 81 | "EventType": "viewer-request", 82 | "FunctionARN": Object { 83 | "Fn::GetAtt": Array [ 84 | "RewriteCdnHost7906DE10", 85 | "FunctionARN", 86 | ], 87 | }, 88 | }, 89 | ], 90 | "SmoothStreaming": false, 91 | "TargetOriginId": "MyTestStackCDNOrigin1581165C2", 92 | "ViewerProtocolPolicy": "redirect-to-https", 93 | }, 94 | "Enabled": true, 95 | "HttpVersion": "http2", 96 | "IPV6Enabled": true, 97 | "Origins": Array [ 98 | Object { 99 | "ConnectionAttempts": 3, 100 | "ConnectionTimeout": 10, 101 | "CustomOriginConfig": Object { 102 | "HTTPSPort": 443, 103 | "OriginProtocolPolicy": "https-only", 104 | "OriginSSLProtocols": Array [ 105 | "TLSv1.2", 106 | ], 107 | }, 108 | "DomainName": Object { 109 | "Fn::Join": Array [ 110 | "", 111 | Array [ 112 | Object { 113 | "Ref": "FlaskLambdaRestApi4883B802", 114 | }, 115 | ".execute-api.", 116 | Object { 117 | "Ref": "AWS::Region", 118 | }, 119 | ".amazonaws.com", 120 | ], 121 | ], 122 | }, 123 | "Id": "MyTestStackCDNOrigin1581165C2", 124 | "OriginPath": "/prod", 125 | }, 126 | ], 127 | "PriceClass": "PriceClass_200", 128 | }, 129 | }, 130 | "Type": "AWS::CloudFront::Distribution", 131 | }, 132 | "DefaultCachePolicyDDFA5BDC": Object { 133 | "Properties": Object { 134 | "CachePolicyConfig": Object { 135 | "DefaultTTL": 300, 136 | "MaxTTL": 3600, 137 | "MinTTL": 0, 138 | "Name": "CachePolicy-prod", 139 | "ParametersInCacheKeyAndForwardedToOrigin": Object { 140 | "CookiesConfig": Object { 141 | "CookieBehavior": "whitelist", 142 | "Cookies": Array [ 143 | "session", 144 | ], 145 | }, 146 | "EnableAcceptEncodingBrotli": true, 147 | "EnableAcceptEncodingGzip": true, 148 | "HeadersConfig": Object { 149 | "HeaderBehavior": "whitelist", 150 | "Headers": Array [ 151 | "x-forwarded-host", 152 | ], 153 | }, 154 | "QueryStringsConfig": Object { 155 | "QueryStringBehavior": "all", 156 | }, 157 | }, 158 | }, 159 | }, 160 | "Type": "AWS::CloudFront::CachePolicy", 161 | }, 162 | "FlaskLambdaRestApi4883B802": Object { 163 | "Properties": Object { 164 | "BinaryMediaTypes": Array [ 165 | "*/*", 166 | ], 167 | "Name": "serverless-flask-api-prod", 168 | }, 169 | "Type": "AWS::ApiGateway::RestApi", 170 | }, 171 | "FlaskLambdaRestApiANYApiPermissionMyTestStackFlaskLambdaRestApi58E05B2CANYA0684099": Object { 172 | "Properties": Object { 173 | "Action": "lambda:InvokeFunction", 174 | "FunctionName": Object { 175 | "Fn::GetAtt": Array [ 176 | "ServerlessFlaskLambda90920255", 177 | "Arn", 178 | ], 179 | }, 180 | "Principal": "apigateway.amazonaws.com", 181 | "SourceArn": Object { 182 | "Fn::Join": Array [ 183 | "", 184 | Array [ 185 | "arn:", 186 | Object { 187 | "Ref": "AWS::Partition", 188 | }, 189 | ":execute-api:", 190 | Object { 191 | "Ref": "AWS::Region", 192 | }, 193 | ":", 194 | Object { 195 | "Ref": "AWS::AccountId", 196 | }, 197 | ":", 198 | Object { 199 | "Ref": "FlaskLambdaRestApi4883B802", 200 | }, 201 | "/", 202 | Object { 203 | "Ref": "FlaskLambdaRestApiDeploymentStageprodAA802E6B", 204 | }, 205 | "/*/", 206 | ], 207 | ], 208 | }, 209 | }, 210 | "Type": "AWS::Lambda::Permission", 211 | }, 212 | "FlaskLambdaRestApiANYApiPermissionTestMyTestStackFlaskLambdaRestApi58E05B2CANY448E67D4": Object { 213 | "Properties": Object { 214 | "Action": "lambda:InvokeFunction", 215 | "FunctionName": Object { 216 | "Fn::GetAtt": Array [ 217 | "ServerlessFlaskLambda90920255", 218 | "Arn", 219 | ], 220 | }, 221 | "Principal": "apigateway.amazonaws.com", 222 | "SourceArn": Object { 223 | "Fn::Join": Array [ 224 | "", 225 | Array [ 226 | "arn:", 227 | Object { 228 | "Ref": "AWS::Partition", 229 | }, 230 | ":execute-api:", 231 | Object { 232 | "Ref": "AWS::Region", 233 | }, 234 | ":", 235 | Object { 236 | "Ref": "AWS::AccountId", 237 | }, 238 | ":", 239 | Object { 240 | "Ref": "FlaskLambdaRestApi4883B802", 241 | }, 242 | "/test-invoke-stage/*/", 243 | ], 244 | ], 245 | }, 246 | }, 247 | "Type": "AWS::Lambda::Permission", 248 | }, 249 | "FlaskLambdaRestApiANYC4308976": Object { 250 | "Properties": Object { 251 | "AuthorizationType": "NONE", 252 | "HttpMethod": "ANY", 253 | "Integration": Object { 254 | "IntegrationHttpMethod": "POST", 255 | "Type": "AWS_PROXY", 256 | "Uri": Object { 257 | "Fn::Join": Array [ 258 | "", 259 | Array [ 260 | "arn:", 261 | Object { 262 | "Ref": "AWS::Partition", 263 | }, 264 | ":apigateway:", 265 | Object { 266 | "Ref": "AWS::Region", 267 | }, 268 | ":lambda:path/2015-03-31/functions/", 269 | Object { 270 | "Fn::GetAtt": Array [ 271 | "ServerlessFlaskLambda90920255", 272 | "Arn", 273 | ], 274 | }, 275 | "/invocations", 276 | ], 277 | ], 278 | }, 279 | }, 280 | "ResourceId": Object { 281 | "Fn::GetAtt": Array [ 282 | "FlaskLambdaRestApi4883B802", 283 | "RootResourceId", 284 | ], 285 | }, 286 | "RestApiId": Object { 287 | "Ref": "FlaskLambdaRestApi4883B802", 288 | }, 289 | }, 290 | "Type": "AWS::ApiGateway::Method", 291 | }, 292 | "FlaskLambdaRestApiAccount566D8BF7": Object { 293 | "DeletionPolicy": "Retain", 294 | "DependsOn": Array [ 295 | "FlaskLambdaRestApi4883B802", 296 | ], 297 | "Properties": Object { 298 | "CloudWatchRoleArn": Object { 299 | "Fn::GetAtt": Array [ 300 | "FlaskLambdaRestApiCloudWatchRole259FF17C", 301 | "Arn", 302 | ], 303 | }, 304 | }, 305 | "Type": "AWS::ApiGateway::Account", 306 | "UpdateReplacePolicy": "Retain", 307 | }, 308 | "FlaskLambdaRestApiCloudWatchRole259FF17C": Object { 309 | "DeletionPolicy": "Retain", 310 | "Properties": Object { 311 | "AssumeRolePolicyDocument": Object { 312 | "Statement": Array [ 313 | Object { 314 | "Action": "sts:AssumeRole", 315 | "Effect": "Allow", 316 | "Principal": Object { 317 | "Service": "apigateway.amazonaws.com", 318 | }, 319 | }, 320 | ], 321 | "Version": "2012-10-17", 322 | }, 323 | "ManagedPolicyArns": Array [ 324 | Object { 325 | "Fn::Join": Array [ 326 | "", 327 | Array [ 328 | "arn:", 329 | Object { 330 | "Ref": "AWS::Partition", 331 | }, 332 | ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", 333 | ], 334 | ], 335 | }, 336 | ], 337 | }, 338 | "Type": "AWS::IAM::Role", 339 | "UpdateReplacePolicy": "Retain", 340 | }, 341 | "FlaskLambdaRestApiDeployment354FB1C3336497a99a910cbb43294351d7f04d34": Object { 342 | "DependsOn": Array [ 343 | "FlaskLambdaRestApiproxyANY9442EDC4", 344 | "FlaskLambdaRestApiproxyF0AA8E0C", 345 | "FlaskLambdaRestApiANYC4308976", 346 | ], 347 | "Properties": Object { 348 | "Description": "Automatically created by the RestApi construct", 349 | "RestApiId": Object { 350 | "Ref": "FlaskLambdaRestApi4883B802", 351 | }, 352 | }, 353 | "Type": "AWS::ApiGateway::Deployment", 354 | }, 355 | "FlaskLambdaRestApiDeploymentStageprodAA802E6B": Object { 356 | "DependsOn": Array [ 357 | "FlaskLambdaRestApiAccount566D8BF7", 358 | ], 359 | "Properties": Object { 360 | "DeploymentId": Object { 361 | "Ref": "FlaskLambdaRestApiDeployment354FB1C3336497a99a910cbb43294351d7f04d34", 362 | }, 363 | "MethodSettings": Array [ 364 | Object { 365 | "DataTraceEnabled": false, 366 | "HttpMethod": "*", 367 | "ResourcePath": "/*", 368 | "ThrottlingBurstLimit": 1000, 369 | "ThrottlingRateLimit": 100, 370 | }, 371 | ], 372 | "RestApiId": Object { 373 | "Ref": "FlaskLambdaRestApi4883B802", 374 | }, 375 | "StageName": "prod", 376 | }, 377 | "Type": "AWS::ApiGateway::Stage", 378 | }, 379 | "FlaskLambdaRestApiproxyANY9442EDC4": Object { 380 | "Properties": Object { 381 | "AuthorizationType": "NONE", 382 | "HttpMethod": "ANY", 383 | "Integration": Object { 384 | "IntegrationHttpMethod": "POST", 385 | "Type": "AWS_PROXY", 386 | "Uri": Object { 387 | "Fn::Join": Array [ 388 | "", 389 | Array [ 390 | "arn:", 391 | Object { 392 | "Ref": "AWS::Partition", 393 | }, 394 | ":apigateway:", 395 | Object { 396 | "Ref": "AWS::Region", 397 | }, 398 | ":lambda:path/2015-03-31/functions/", 399 | Object { 400 | "Fn::GetAtt": Array [ 401 | "ServerlessFlaskLambda90920255", 402 | "Arn", 403 | ], 404 | }, 405 | "/invocations", 406 | ], 407 | ], 408 | }, 409 | }, 410 | "ResourceId": Object { 411 | "Ref": "FlaskLambdaRestApiproxyF0AA8E0C", 412 | }, 413 | "RestApiId": Object { 414 | "Ref": "FlaskLambdaRestApi4883B802", 415 | }, 416 | }, 417 | "Type": "AWS::ApiGateway::Method", 418 | }, 419 | "FlaskLambdaRestApiproxyANYApiPermissionMyTestStackFlaskLambdaRestApi58E05B2CANYproxy7E790FF1": Object { 420 | "Properties": Object { 421 | "Action": "lambda:InvokeFunction", 422 | "FunctionName": Object { 423 | "Fn::GetAtt": Array [ 424 | "ServerlessFlaskLambda90920255", 425 | "Arn", 426 | ], 427 | }, 428 | "Principal": "apigateway.amazonaws.com", 429 | "SourceArn": Object { 430 | "Fn::Join": Array [ 431 | "", 432 | Array [ 433 | "arn:", 434 | Object { 435 | "Ref": "AWS::Partition", 436 | }, 437 | ":execute-api:", 438 | Object { 439 | "Ref": "AWS::Region", 440 | }, 441 | ":", 442 | Object { 443 | "Ref": "AWS::AccountId", 444 | }, 445 | ":", 446 | Object { 447 | "Ref": "FlaskLambdaRestApi4883B802", 448 | }, 449 | "/", 450 | Object { 451 | "Ref": "FlaskLambdaRestApiDeploymentStageprodAA802E6B", 452 | }, 453 | "/*/*", 454 | ], 455 | ], 456 | }, 457 | }, 458 | "Type": "AWS::Lambda::Permission", 459 | }, 460 | "FlaskLambdaRestApiproxyANYApiPermissionTestMyTestStackFlaskLambdaRestApi58E05B2CANYproxy3ECD2019": Object { 461 | "Properties": Object { 462 | "Action": "lambda:InvokeFunction", 463 | "FunctionName": Object { 464 | "Fn::GetAtt": Array [ 465 | "ServerlessFlaskLambda90920255", 466 | "Arn", 467 | ], 468 | }, 469 | "Principal": "apigateway.amazonaws.com", 470 | "SourceArn": Object { 471 | "Fn::Join": Array [ 472 | "", 473 | Array [ 474 | "arn:", 475 | Object { 476 | "Ref": "AWS::Partition", 477 | }, 478 | ":execute-api:", 479 | Object { 480 | "Ref": "AWS::Region", 481 | }, 482 | ":", 483 | Object { 484 | "Ref": "AWS::AccountId", 485 | }, 486 | ":", 487 | Object { 488 | "Ref": "FlaskLambdaRestApi4883B802", 489 | }, 490 | "/test-invoke-stage/*/*", 491 | ], 492 | ], 493 | }, 494 | }, 495 | "Type": "AWS::Lambda::Permission", 496 | }, 497 | "FlaskLambdaRestApiproxyF0AA8E0C": Object { 498 | "Properties": Object { 499 | "ParentId": Object { 500 | "Fn::GetAtt": Array [ 501 | "FlaskLambdaRestApi4883B802", 502 | "RootResourceId", 503 | ], 504 | }, 505 | "PathPart": "{proxy+}", 506 | "RestApiId": Object { 507 | "Ref": "FlaskLambdaRestApi4883B802", 508 | }, 509 | }, 510 | "Type": "AWS::ApiGateway::Resource", 511 | }, 512 | "LambdaRole3A44B857": Object { 513 | "Properties": Object { 514 | "AssumeRolePolicyDocument": Object { 515 | "Statement": Array [ 516 | Object { 517 | "Action": "sts:AssumeRole", 518 | "Effect": "Allow", 519 | "Principal": Object { 520 | "Service": "lambda.amazonaws.com", 521 | }, 522 | }, 523 | ], 524 | "Version": "2012-10-17", 525 | }, 526 | "Policies": Array [ 527 | Object { 528 | "PolicyDocument": Object { 529 | "Statement": Array [ 530 | Object { 531 | "Action": Array [ 532 | "ec2:DescribeTags", 533 | "cloudwatch:GetMetricStatistics", 534 | "cloudwatch:ListMetrics", 535 | "logs:CreateLogGroup", 536 | "logs:CreateLogStream", 537 | "logs:PutLogEvents", 538 | "logs:DescribeLogStreams", 539 | ], 540 | "Effect": "Allow", 541 | "Resource": "*", 542 | "Sid": "0", 543 | }, 544 | Object { 545 | "Action": "lambda:InvokeFunction", 546 | "Effect": "Allow", 547 | "Resource": "*", 548 | "Sid": "1", 549 | }, 550 | ], 551 | "Version": "2012-10-17", 552 | }, 553 | "PolicyName": "lambda-executor", 554 | }, 555 | ], 556 | }, 557 | "Type": "AWS::IAM::Role", 558 | }, 559 | "LambdaRoleDefaultPolicy75625A82": Object { 560 | "Properties": Object { 561 | "PolicyDocument": Object { 562 | "Statement": Array [ 563 | Object { 564 | "Action": Array [ 565 | "s3:GetObject*", 566 | "s3:GetBucket*", 567 | "s3:List*", 568 | "s3:DeleteObject*", 569 | "s3:PutObject", 570 | "s3:PutObjectLegalHold", 571 | "s3:PutObjectRetention", 572 | "s3:PutObjectTagging", 573 | "s3:PutObjectVersionTagging", 574 | "s3:Abort*", 575 | ], 576 | "Effect": "Allow", 577 | "Resource": Array [ 578 | Object { 579 | "Fn::GetAtt": Array [ 580 | "S3StorageA4C3DCD4", 581 | "Arn", 582 | ], 583 | }, 584 | Object { 585 | "Fn::Join": Array [ 586 | "", 587 | Array [ 588 | Object { 589 | "Fn::GetAtt": Array [ 590 | "S3StorageA4C3DCD4", 591 | "Arn", 592 | ], 593 | }, 594 | "/*", 595 | ], 596 | ], 597 | }, 598 | ], 599 | }, 600 | ], 601 | "Version": "2012-10-17", 602 | }, 603 | "PolicyName": "LambdaRoleDefaultPolicy75625A82", 604 | "Roles": Array [ 605 | Object { 606 | "Ref": "LambdaRole3A44B857", 607 | }, 608 | ], 609 | }, 610 | "Type": "AWS::IAM::Policy", 611 | }, 612 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": Object { 613 | "DependsOn": Array [ 614 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", 615 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", 616 | ], 617 | "Properties": Object { 618 | "Code": Object { 619 | "S3Bucket": Object { 620 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 621 | }, 622 | "S3Key": "e45ee2082d227db1b6f0292696ce5ce2b061c105d15efb341925ca040d1feb68.zip", 623 | }, 624 | "Handler": "index.handler", 625 | "Role": Object { 626 | "Fn::GetAtt": Array [ 627 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", 628 | "Arn", 629 | ], 630 | }, 631 | "Runtime": "nodejs14.x", 632 | }, 633 | "Type": "AWS::Lambda::Function", 634 | }, 635 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": Object { 636 | "Properties": Object { 637 | "AssumeRolePolicyDocument": Object { 638 | "Statement": Array [ 639 | Object { 640 | "Action": "sts:AssumeRole", 641 | "Effect": "Allow", 642 | "Principal": Object { 643 | "Service": "lambda.amazonaws.com", 644 | }, 645 | }, 646 | ], 647 | "Version": "2012-10-17", 648 | }, 649 | "ManagedPolicyArns": Array [ 650 | Object { 651 | "Fn::Join": Array [ 652 | "", 653 | Array [ 654 | "arn:", 655 | Object { 656 | "Ref": "AWS::Partition", 657 | }, 658 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 659 | ], 660 | ], 661 | }, 662 | ], 663 | }, 664 | "Type": "AWS::IAM::Role", 665 | }, 666 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": Object { 667 | "Properties": Object { 668 | "PolicyDocument": Object { 669 | "Statement": Array [ 670 | Object { 671 | "Action": Array [ 672 | "logs:PutRetentionPolicy", 673 | "logs:DeleteRetentionPolicy", 674 | ], 675 | "Effect": "Allow", 676 | "Resource": "*", 677 | }, 678 | ], 679 | "Version": "2012-10-17", 680 | }, 681 | "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", 682 | "Roles": Array [ 683 | Object { 684 | "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", 685 | }, 686 | ], 687 | }, 688 | "Type": "AWS::IAM::Policy", 689 | }, 690 | "RewriteCdnHost7906DE10": Object { 691 | "Properties": Object { 692 | "AutoPublish": true, 693 | "FunctionCode": " 694 | function handler(event) { 695 | var req = event.request; 696 | if (req.headers['host']) { 697 | req.headers['x-forwarded-host'] = { 698 | value: req.headers['host'].value 699 | }; 700 | } 701 | return req; 702 | } 703 | ", 704 | "FunctionConfig": Object { 705 | "Comment": Object { 706 | "Fn::Join": Array [ 707 | "", 708 | Array [ 709 | Object { 710 | "Ref": "AWS::AccountId", 711 | }, 712 | "MyTestStackFixCdnHostFunctionprod", 713 | ], 714 | ], 715 | }, 716 | "Runtime": "cloudfront-js-1.0", 717 | }, 718 | "Name": Object { 719 | "Fn::Join": Array [ 720 | "", 721 | Array [ 722 | Object { 723 | "Ref": "AWS::AccountId", 724 | }, 725 | "MyTestStackFixCdnHostFunctionprod", 726 | ], 727 | ], 728 | }, 729 | }, 730 | "Type": "AWS::CloudFront::Function", 731 | }, 732 | "S3StorageA4C3DCD4": Object { 733 | "DeletionPolicy": "Retain", 734 | "Properties": Object { 735 | "BucketEncryption": Object { 736 | "ServerSideEncryptionConfiguration": Array [ 737 | Object { 738 | "ServerSideEncryptionByDefault": Object { 739 | "SSEAlgorithm": "AES256", 740 | }, 741 | }, 742 | ], 743 | }, 744 | "PublicAccessBlockConfiguration": Object { 745 | "BlockPublicAcls": true, 746 | "BlockPublicPolicy": true, 747 | "IgnorePublicAcls": true, 748 | "RestrictPublicBuckets": true, 749 | }, 750 | }, 751 | "Type": "AWS::S3::Bucket", 752 | "UpdateReplacePolicy": "Retain", 753 | }, 754 | "ServerlessFlaskLambda90920255": Object { 755 | "DependsOn": Array [ 756 | "LambdaRoleDefaultPolicy75625A82", 757 | "LambdaRole3A44B857", 758 | ], 759 | "Properties": Object { 760 | "Code": Object { 761 | "S3Bucket": Object { 762 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 763 | }, 764 | "S3Key": "0566ca4caab8da098f87d22c688cd37b85719bce0b9b55f6d20c6e06355e134a.zip", 765 | }, 766 | "Environment": Object { 767 | "Variables": Object { 768 | "JSON_CONFIG_OVERRIDE": Object { 769 | "Fn::Join": Array [ 770 | "", 771 | Array [ 772 | "{\\"SESSION_COOKIE_SECURE\\":true,\\"DEBUG\\":false,\\"TEMPLATES_AUTO_RELOAD\\":false,\\"SEND_FILE_MAX_AGE_DEFAULT\\":300,\\"PERMANENT_SESSION_LIFETIME\\":86400,\\"ROOT_LOG_LEVEL\\":\\"INFO\\",\\"S3_BUCKET\\":\\"", 773 | Object { 774 | "Ref": "S3StorageA4C3DCD4", 775 | }, 776 | "\\"}", 777 | ], 778 | ], 779 | }, 780 | }, 781 | }, 782 | "FunctionName": "serverless-flask-lambda-prod", 783 | "Handler": "serverless_flask.lambda.lambda_handler", 784 | "MemorySize": 256, 785 | "Role": Object { 786 | "Fn::GetAtt": Array [ 787 | "LambdaRole3A44B857", 788 | "Arn", 789 | ], 790 | }, 791 | "Runtime": "python3.9", 792 | "Timeout": 30, 793 | }, 794 | "Type": "AWS::Lambda::Function", 795 | }, 796 | "ServerlessFlaskLambdaLogRetention92047C19": Object { 797 | "Properties": Object { 798 | "LogGroupName": Object { 799 | "Fn::Join": Array [ 800 | "", 801 | Array [ 802 | "/aws/lambda/", 803 | Object { 804 | "Ref": "ServerlessFlaskLambda90920255", 805 | }, 806 | ], 807 | ], 808 | }, 809 | "RetentionInDays": 180, 810 | "ServiceToken": Object { 811 | "Fn::GetAtt": Array [ 812 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", 813 | "Arn", 814 | ], 815 | }, 816 | }, 817 | "Type": "Custom::LogRetention", 818 | }, 819 | }, 820 | "Rules": Object { 821 | "CheckBootstrapVersion": Object { 822 | "Assertions": Array [ 823 | Object { 824 | "Assert": Object { 825 | "Fn::Not": Array [ 826 | Object { 827 | "Fn::Contains": Array [ 828 | Array [ 829 | "1", 830 | "2", 831 | "3", 832 | "4", 833 | "5", 834 | ], 835 | Object { 836 | "Ref": "BootstrapVersion", 837 | }, 838 | ], 839 | }, 840 | ], 841 | }, 842 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 843 | }, 844 | ], 845 | }, 846 | }, 847 | } 848 | `; 849 | --------------------------------------------------------------------------------