diff --git a/.github/workflows/run-ci-cd.yaml b/.github/workflows/run-ci-cd.yaml index adef9776f3..e5caeec3fa 100644 --- a/.github/workflows/run-ci-cd.yaml +++ b/.github/workflows/run-ci-cd.yaml @@ -55,6 +55,11 @@ jobs: restore-keys: | pre-commit-${{ runner.os }}- + - name: Setup TFLint + uses: terraform-linters/setup-tflint@dfcb455ed8d56b55edf6447a37379108181a6707 + with: + tflint_version: v0.59.1 + - name: Run pre-commit uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd diff --git a/.gitignore b/.gitignore index c9ddb28ad8..4c69638274 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,16 @@ __pycache__ *.log *.pdf *.pem +*.tfstate +*.tfstate.* +**/.terraform/ +backend/*nest-backend-dev*.tar.gz +backend/*nest-backend-dev*.zip +backend/*nest-backend-staging*.tar.gz +backend/*nest-backend-staging*.zip backend/data/backup* backend/staticfiles +backend/zappa_settings.json design/ frontend/blob-report/ frontend/coverage @@ -37,6 +45,7 @@ frontend/pnpm-debug.log* frontend/test-results/ frontend/yarn-debug.log* frontend/yarn-error.log* +infrastructure/terraform.tfvars logs node_modules/ TODO diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a19ae50a4..918d8b82be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,14 @@ repos: - --strict exclude: (.github|pnpm-lock.yaml) + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.92.0 + hooks: + - id: terraform_fmt + files: ^infrastructure/.*\.tf$ + - id: terraform_tflint + files: ^infrastructure/.*\.tf$ + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.2 hooks: diff --git a/backend/poetry.lock b/backend/poetry.lock index 2be96c04e9..2678355a93 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -235,6 +235,21 @@ sniffio = ">=1.1" [package.extras] trio = ["trio (>=0.31.0)"] +[[package]] +name = "argcomplete" +version = "3.6.3" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"}, + {file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + [[package]] name = "asgiref" version = "3.10.0" @@ -276,18 +291,18 @@ files = [ [[package]] name = "boto3" -version = "1.40.59" +version = "1.40.57" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.40.59-py3-none-any.whl", hash = "sha256:75752e7dc445131700a58926a50ca705794232f0f47d0e21edb59fbf1898db95"}, - {file = "boto3-1.40.59.tar.gz", hash = "sha256:b1a5a203511e594872b39a129365f02eb5846eea990629e8daf47a3c01e7fd49"}, + {file = "boto3-1.40.57-py3-none-any.whl", hash = "sha256:4ceeac741b04cd5d9193c85d1707597a30f7482682733437454408ea755ee151"}, + {file = "boto3-1.40.57.tar.gz", hash = "sha256:717605170cb167e07462b7f033b26bc9c0fee34b78b5eac52edcd6149915e23f"}, ] [package.dependencies] -botocore = ">=1.40.59,<1.41.0" +botocore = ">=1.40.57,<1.41.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.14.0,<0.15.0" @@ -296,14 +311,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.40.59" +version = "1.40.57" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.40.59-py3-none-any.whl", hash = "sha256:042dd844ca82155ca1ab9608b9bef36d517515c775d075f57b89257108ae843b"}, - {file = "botocore-1.40.59.tar.gz", hash = "sha256:842a466d8735272a30fe5b7f97df559d9e211a18e412f62a17ed249fd62f85fe"}, + {file = "botocore-1.40.57-py3-none-any.whl", hash = "sha256:95dfd35e0863c3e33d458b0e74eb5a82d73521347c25651e1a0e18cf921ee010"}, + {file = "botocore-1.40.57.tar.gz", hash = "sha256:39bb0570e10eb7a5d518974865aeebafe275498c8f132b23e3021b957babaf8a"}, ] [package.dependencies] @@ -436,6 +451,23 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "cfn-flip" +version = "1.3.0" +description = "Convert AWS CloudFormation templates between JSON and YAML formats" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "cfn_flip-1.3.0-py3-none-any.whl", hash = "sha256:faca8e77f0d32fb84cce1db1ef4c18b14a325d31125dae73c13bcc01947d2722"}, + {file = "cfn_flip-1.3.0.tar.gz", hash = "sha256:003e02a089c35e1230ffd0e1bcfbbc4b12cc7d2deb2fcc6c4228ac9819307362"}, +] + +[package.dependencies] +Click = "*" +PyYAML = ">=4.1" +six = "*" + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -565,7 +597,7 @@ version = "8.3.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, @@ -993,6 +1025,18 @@ pyyaml = ">=6" regex = ">=2023" tqdm = ">=4.62.2" +[[package]] +name = "durationpy" +version = "0.10" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, + {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, +] + [[package]] name = "editorconfig" version = "0.17.1" @@ -1341,6 +1385,18 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "hjson" +version = "3.1.0" +description = "Hjson, a user interface for JSON." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89"}, + {file = "hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75"}, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1694,6 +1750,24 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "kappa" +version = "0.6.0" +description = "A CLI tool for AWS Lambda developers" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "kappa-0.6.0-py2-none-any.whl", hash = "sha256:4d6b7b3accce4a0aaaac92b36237a6304f0f2fffbbe3caea3f7c9f52d12c9989"}, + {file = "kappa-0.6.0.tar.gz", hash = "sha256:4b5b372872f25d619e427e04282551048dc975a107385b076b3ffc6406a15833"}, +] + +[package.dependencies] +boto3 = ">=1.2.3" +click = ">=5.1" +placebo = ">=0.8.1" +PyYAML = ">=3.11" + [[package]] name = "langchain" version = "0.3.27" @@ -1798,14 +1872,14 @@ langchain-core = ">=0.3.75,<2.0.0" [[package]] name = "langsmith" -version = "0.4.38" -description = "Client library to connect to the LangSmith Observability and Evaluation Platform." +version = "0.4.37" +description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "langsmith-0.4.38-py3-none-any.whl", hash = "sha256:326232a24b1c6dd308a3188557cc023adf8fb14144263b2982c115a6be5141e7"}, - {file = "langsmith-0.4.38.tar.gz", hash = "sha256:3aa57f9c16a5880256cd1eab0452533c1fb5ee14ec5250e23ed919cc2b07f6d3"}, + {file = "langsmith-0.4.37-py3-none-any.whl", hash = "sha256:e34a94ce7277646299e4703a0f6e2d2c43647a28e8b800bb7ef82fd87a0ec766"}, + {file = "langsmith-0.4.37.tar.gz", hash = "sha256:d9a0eb6dd93f89843ac982c9f92be93cf2bcabbe19957f362c547766c7366c71"}, ] [package.dependencies] @@ -2397,14 +2471,14 @@ files = [ [[package]] name = "openai" -version = "2.6.1" +version = "2.6.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-2.6.1-py3-none-any.whl", hash = "sha256:904e4b5254a8416746a2f05649594fa41b19d799843cd134dac86167e094edef"}, - {file = "openai-2.6.1.tar.gz", hash = "sha256:27ae704d190615fca0c0fc2b796a38f8b5879645a3a52c9c453b23f97141bb49"}, + {file = "openai-2.6.0-py3-none-any.whl", hash = "sha256:f33fa12070fe347b5787a7861c8dd397786a4a17e1c3186e239338dac7e2e743"}, + {file = "openai-2.6.0.tar.gz", hash = "sha256:f119faf7fc07d7e558c1e7c32c873e241439b01bd7480418234291ee8c8f4b9d"}, ] [package.dependencies] @@ -2425,112 +2499,108 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "orjson" -version = "3.11.4" +version = "3.11.3" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1"}, - {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44"}, - {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c"}, - {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23"}, - {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea"}, - {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba"}, - {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff"}, - {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac"}, - {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79"}, - {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827"}, - {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b"}, - {file = "orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3"}, - {file = "orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc"}, - {file = "orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39"}, - {file = "orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d"}, - {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175"}, - {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040"}, - {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63"}, - {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9"}, - {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a"}, - {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be"}, - {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7"}, - {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549"}, - {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905"}, - {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907"}, - {file = "orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c"}, - {file = "orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a"}, - {file = "orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045"}, - {file = "orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50"}, - {file = "orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853"}, - {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938"}, - {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415"}, - {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44"}, - {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2"}, - {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708"}, - {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210"}, - {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241"}, - {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b"}, - {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c"}, - {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9"}, - {file = "orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa"}, - {file = "orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140"}, - {file = "orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e"}, - {file = "orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534"}, - {file = "orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff"}, - {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad"}, - {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5"}, - {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a"}, - {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436"}, - {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9"}, - {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73"}, - {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0"}, - {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196"}, - {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a"}, - {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6"}, - {file = "orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839"}, - {file = "orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a"}, - {file = "orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de"}, - {file = "orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803"}, - {file = "orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54"}, - {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e"}, - {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316"}, - {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1"}, - {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc"}, - {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f"}, - {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf"}, - {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606"}, - {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780"}, - {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23"}, - {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155"}, - {file = "orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394"}, - {file = "orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1"}, - {file = "orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d"}, - {file = "orjson-3.11.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:405261b0a8c62bcbd8e2931c26fdc08714faf7025f45531541e2b29e544b545b"}, - {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af02ff34059ee9199a3546f123a6ab4c86caf1708c79042caf0820dc290a6d4f"}, - {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b2eba969ea4203c177c7b38b36c69519e6067ee68c34dc37081fac74c796e10"}, - {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0baa0ea43cfa5b008a28d3c07705cf3ada40e5d347f0f44994a64b1b7b4b5350"}, - {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80fd082f5dcc0e94657c144f1b2a3a6479c44ad50be216cf0c244e567f5eae19"}, - {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3704d35e47d5bee811fb1cbd8599f0b4009b14d451c4c57be5a7e25eb89a13"}, - {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa447f2b5356779d914658519c874cf3b7629e99e63391ed519c28c8aea4919"}, - {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bba5118143373a86f91dadb8df41d9457498226698ebdf8e11cbb54d5b0e802d"}, - {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:622463ab81d19ef3e06868b576551587de8e4d518892d1afab71e0fbc1f9cffc"}, - {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3e0a700c4b82144b72946b6629968df9762552ee1344bfdb767fecdd634fbd5a"}, - {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e18a5c15e764e5f3fc569b47872450b4bcea24f2a6354c0a0e95ad21045d5a9"}, - {file = "orjson-3.11.4-cp39-cp39-win32.whl", hash = "sha256:fb1c37c71cad991ef4d89c7a634b5ffb4447dbd7ae3ae13e8f5ee7f1775e7ab1"}, - {file = "orjson-3.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:e2985ce8b8c42d00492d0ed79f2bd2b6460d00f2fa671dfde4bf2e02f49bf5c6"}, - {file = "orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d"}, + {file = "orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6"}, + {file = "orjson-3.11.3-cp310-cp310-win32.whl", hash = "sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc"}, + {file = "orjson-3.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770"}, + {file = "orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f"}, + {file = "orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f"}, + {file = "orjson-3.11.3-cp311-cp311-win32.whl", hash = "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204"}, + {file = "orjson-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b"}, + {file = "orjson-3.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e"}, + {file = "orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b"}, + {file = "orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049"}, + {file = "orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca"}, + {file = "orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1"}, + {file = "orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710"}, + {file = "orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810"}, + {file = "orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633"}, + {file = "orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b"}, + {file = "orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae"}, + {file = "orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce"}, + {file = "orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4"}, + {file = "orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e"}, + {file = "orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d"}, + {file = "orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872"}, + {file = "orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d"}, + {file = "orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804"}, + {file = "orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc"}, + {file = "orjson-3.11.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7"}, + {file = "orjson-3.11.3-cp39-cp39-win32.whl", hash = "sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225"}, + {file = "orjson-3.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb"}, + {file = "orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a"}, ] [[package]] name = "owasp-schema" -version = "0.1.39" +version = "0.1.38" description = "A collection of OWASP schemas" optional = false python-versions = "<4.0,>=3.13" groups = ["main"] files = [ - {file = "owasp_schema-0.1.39-py3-none-any.whl", hash = "sha256:76ddc5464a54e1410a5bd7ebd8e109d5a7c313a8fd372783b84a37990eb5be97"}, - {file = "owasp_schema-0.1.39.tar.gz", hash = "sha256:c97537f2dccdeaf2d582888949b6c60dec108c415c546c86469dad9d4304e134"}, + {file = "owasp_schema-0.1.38-py3-none-any.whl", hash = "sha256:4439bb55eee6cda5a151cf3ac6fefcc435de4f387196321938a626770f5bd7bf"}, + {file = "owasp_schema-0.1.38.tar.gz", hash = "sha256:60e281bba38d7acecc63fb07abcff2406f97ceb8a9aff95bf36a51668bab6d49"}, ] [package.dependencies] @@ -2686,6 +2756,29 @@ test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] +[[package]] +name = "pip" +version = "25.2" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717"}, + {file = "pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2"}, +] + +[[package]] +name = "placebo" +version = "0.9.0" +description = "Make boto3 calls that look real but have no effect" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "placebo-0.9.0.tar.gz", hash = "sha256:03157f8527bbc2965b71b88f4a139ef8038618b346787f20d63e3c5da541b047"}, +] + [[package]] name = "platformdirs" version = "4.5.0" @@ -3371,6 +3464,24 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] name = "pyyaml" version = "6.0.3" @@ -4011,6 +4122,27 @@ statsig = ["statsig (>=0.55.3)"] tornado = ["tornado (>=6)"] unleash = ["UnleashClient (>=6.0.1)"] +[[package]] +name = "setuptools" +version = "79.0.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51"}, + {file = "setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + [[package]] name = "six" version = "1.17.0" @@ -4253,6 +4385,18 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "thefuzz" version = "0.22.1" @@ -4268,6 +4412,18 @@ files = [ [package.dependencies] rapidfuzz = ">=3.0.0,<4.0.0" +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -4290,6 +4446,24 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "troposphere" +version = "4.9.4" +description = "AWS CloudFormation creation library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "troposphere-4.9.4-py3-none-any.whl", hash = "sha256:45d1b600e5a0d0678416eaf5a48ee66f68d771e0c0e7ee695b6ebc8c93fb0e02"}, + {file = "troposphere-4.9.4.tar.gz", hash = "sha256:55af51da7a634960193ed054146cfa8656f5a8a7b0027aa7f200506e25058b08"}, +] + +[package.dependencies] +cfn_flip = ">=1.0.2" + +[package.extras] +policy = ["awacs (>=2.0.0)"] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -4400,6 +4574,39 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wheel" +version = "0.45.1" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + [[package]] name = "yarl" version = "1.22.0" @@ -4545,6 +4752,38 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.1" +[[package]] +name = "zappa" +version = "0.60.2" +description = "Server-less Python Web Services for AWS Lambda and API Gateway" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zappa-0.60.2-py3-none-any.whl", hash = "sha256:876b81210cc152b617b3ddd988a9376023d0b00e47a195d285d646198ef45563"}, + {file = "zappa-0.60.2.tar.gz", hash = "sha256:f20a07e8e447c954c50adafe93dd10c0c8261d70e7818bb2d4ba6023c6114542"}, +] + +[package.dependencies] +argcomplete = "*" +boto3 = ">=1.17.28" +durationpy = "*" +hjson = "*" +jmespath = "*" +kappa = "0.6.0" +pip = ">=24.0.0" +placebo = "<0.10" +python-dateutil = "*" +python-slugify = "*" +pyyaml = "*" +requests = ">=2.32.0" +setuptools = "<80.0.0" +toml = "*" +tqdm = ">=4.66.3" +troposphere = ">=3.0" +werkzeug = "*" +wheel = "*" + [[package]] name = "zstandard" version = "0.25.0" @@ -4660,4 +4899,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "ff73d1805d6c44e69d3329dc59518714a20bc2f54c717575a60b73c1eadb4d91" +content-hash = "f2b7451897ee6cb9b9a7d84bc74e00fb03060d025104ea7d8bba35c70138f5f9" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e5e7697fba..39e70696c0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -48,6 +48,7 @@ strawberry-graphql = { extras = [ "django" ], version = "^0.284.1" } strawberry-graphql-django = "^0.67.0" thefuzz = "^0.22.1" pyparsing = "^3.2.3" +zappa = "^0.60.2" [tool.poetry.group.dev.dependencies] djlint = "^1.36.4" diff --git a/backend/zappa_settings.example.json b/backend/zappa_settings.example.json new file mode 100644 index 0000000000..7e35e81c4b --- /dev/null +++ b/backend/zappa_settings.example.json @@ -0,0 +1,39 @@ +{ + "staging": { + "app_function": "wsgi.application", + "django_settings": "settings.staging", + "environment_variables": { + "DJANGO_ALGOLIA_APPLICATION_ID": "${DJANGO_ALGOLIA_APPLICATION_ID}", + "DJANGO_ALGOLIA_WRITE_API_KEY": "${DJANGO_ALGOLIA_WRITE_API_KEY}", + "DJANGO_ALLOWED_HOSTS": "${DJANGO_ALLOWED_HOSTS}", + "DJANGO_AWS_ACCESS_KEY_ID": "${DJANGO_AWS_ACCESS_KEY_ID}", + "DJANGO_AWS_SECRET_ACCESS_KEY": "${DJANGO_AWS_SECRET_ACCESS_KEY}", + "DJANGO_CONFIGURATION": "Staging", + "DJANGO_DB_HOST": "${DJANGO_DB_HOST}", + "DJANGO_DB_NAME": "${DJANGO_DB_NAME}", + "DJANGO_DB_USER": "${DJANGO_DB_USER}", + "DJANGO_DB_PORT": "${DJANGO_DB_PORT}", + "DJANGO_DB_PASSWORD": "${DJANGO_DB_PASSWORD}", + "DJANGO_OPEN_AI_SECRET_KEY": "${DJANGO_OPEN_AI_SECRET_KEY}", + "DJANGO_REDIS_HOST": "${DJANGO_REDIS_HOST}", + "DJANGO_REDIS_PASSWORD": "${DJANGO_REDIS_PASSWORD}", + "DJANGO_SECRET_KEY": "${DJANGO_SECRET_KEY}", + "DJANGO_SENTRY_DSN": "${DJANGO_SENTRY_DSN}", + "DJANGO_SLACK_BOT_TOKEN": "${DJANGO_SLACK_BOT_TOKEN}", + "DJANGO_SLACK_SIGNING_SECRET": "${DJANGO_SLACK_SIGNING_SECRET}" + }, + "manage_roles": true, + "project_name": "nest-backend", + "runtime": "python3.13", + "s3_bucket": "${ZAPPA_S3_BUCKET}", + "slim_handler": true, + "vpc_config": { + "SecurityGroupIds": ["${AWS_LAMBDA_VPC_SECURITY_GROUP_ID}"], + "SubnetIds": [ + "${AWS_PRIVATE_SUBNET_A}", + "${AWS_PRIVATE_SUBNET_B}", + "${AWS_PRIVATE_SUBNET_C}" + ] + } + } +} diff --git a/cspell/cspell.json b/cspell/cspell.json index 6345c970aa..a85cf271ed 100644 --- a/cspell/cspell.json +++ b/cspell/cspell.json @@ -34,6 +34,7 @@ "python", "rust", "software-terms", + "terraform", "win32" ], "enabled": true, @@ -59,6 +60,7 @@ "@cspell/dict-k8s/cspell-ext.json", "@cspell/dict-people-names/cspell-ext.json", "@cspell/dict-software-terms/cspell-ext.json", + "@cspell/dict-terraform/cspell-ext.json", "@cspell/dict-win32/cspell-ext.json" ], "useGitignore": true diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index 31cf216e82..656a6baae7 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -19,6 +19,7 @@ NOSONAR Nadu Nominatim PLR +PYTHONPATH PYTHONUNBUFFERED RUF SEO @@ -54,6 +55,7 @@ csrftoken cva demojize dismissable +dkr dsn env facebookexternalhit @@ -66,6 +68,7 @@ gunicorn heroui hsl igoat +igw inlinehilite isanori jumpstart diff --git a/cspell/package.json b/cspell/package.json index cbe41fa623..e6e2d0e5f8 100644 --- a/cspell/package.json +++ b/cspell/package.json @@ -8,6 +8,7 @@ "@cspell/dict-k8s": "^1.0.12", "@cspell/dict-people-names": "^1.1.14", "@cspell/dict-software-terms": "^4.2.5", + "@cspell/dict-terraform": "^1.1.3", "@cspell/dict-win32": "^2.0.9", "cspell": "^8.19.4" } diff --git a/cspell/pnpm-lock.yaml b/cspell/pnpm-lock.yaml index 2e471c9116..865e545c4d 100644 --- a/cspell/pnpm-lock.yaml +++ b/cspell/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@cspell/dict-software-terms': specifier: ^4.2.5 version: 4.2.5 + '@cspell/dict-terraform': + specifier: ^1.1.3 + version: 1.1.3 '@cspell/dict-win32': specifier: ^2.0.9 version: 2.0.9 diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl new file mode 100644 index 0000000000..8f874e9474 --- /dev/null +++ b/infrastructure/.terraform.lock.hcl @@ -0,0 +1,43 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", + "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", + "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", + "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", + "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", + "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", + "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", + "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", + "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", + "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + ] +} diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000000..2c6feade93 --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,231 @@ +# Infrastructure + +This document provides instructions on how to setup the infrastructure for this project. + +## Prerequisites + +Ensure you have the following setup/installed: + +- Setup Project: [CONTRIBUTING.md](https://github.com/OWASP/Nest/blob/main/CONTRIBUTING.md) +- Terraform: [Terraform Documentation](https://developer.hashicorp.com/terraform/docs) +- AWS CLI: [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +- An AWS account with credentials configured locally. + +## Setting up the infrastructure + +Follow these steps to set up the infrastructure: + +1. **Change the Directory**: + + - Change the directory using the following command: + + ```bash + cd infrastructure/ + ``` + + *Note*: The following steps assume the current working directory is `infrastructure/` + +2. **Create Variables File**: + + - Create a local variables file in the `infrastructure` directory: + + ```bash + touch terraform.tfvars + ``` + + - Copy the contents from the template file into your new local environment file: + + ```bash + cat terraform.tfvars.example > terraform.tfvars + ``` + + - Update the default `django_` prefixed variables. (database/redis credentials will be added later) + +3. **Apply Changes**: + + - Init terraform if needed: + + ```bash + terraform init + ``` + + - Apply the changes and create the infrastructure using the following command: + + ```bash + terraform apply + ``` + +4. **Copy Outputs**: + + - Run the following command to view all outputs. Use the `-raw` flag for sensitive outputs. + - Copy required outputs (i.e. `database_endpoint`, `db_password`, `redis_auth_token`, and `redis_endpoint`) + to the previously created `terraform.tfvars`: + + ```bash + terraform output + ``` + + Example Output: + + ```bash + database_endpoint = "owasp-nest-staging-proxy.proxy-000000000000.us-east-2.rds.amazonaws.com" + db_password = + ecr_repository_url = "000000000000.dkr.ecr.us-east-2.amazonaws.com/owasp-nest-staging-backend" + lambda_security_group_id = "sg-00000000000000000" + private_subnet_ids = [ + "subnet-00000000000000000", + "subnet-11111111111111111", + "subnet-22222222222222222", + ] + redis_auth_token = + redis_endpoint = "master.owasp-nest-staging-cache.aaaaaa.region1.cache.amazonaws.com" + zappa_s3_bucket = "owasp-nest-zappa-deployments" + ``` + + ```bash + terraform output -raw db_password + ``` + + ```bash + terraform output -raw redis_auth_token + ``` + +5. **Apply The Changes Again**: + + - Apply the changes again using the following command: + + ```bash + terraform apply + ``` + +*Note*: Step 4 and 5 ensure that ECS/Fargate tasks have proper environment variables. +These two steps will be removed when AWS Secrets Manager is integrated. + +## Setting up Zappa + +The Django backend deployment is managed by Zappa. This includes the API Gateway, IAM roles, and Lambda Function provision. + +1. **Change Directory**: + + - Change the directory to `backend/` using the following command: + + ```bash + cd ../backend/ + ``` + + *Note*: The following steps assume the current working directory is `backend/` + +2. **Setup Dependencies**: + + - This step may differ for different operating systems. + - The goal is to install dependencies listed in `pyproject.toml`. + - Steps for Linux: + + ```bash + poetry install && eval $(poetry env activate) + ``` + +3. **Create Zappa Settings File**: + +- Create a local Zappa settings file in the `backend` directory: + + ```bash + touch zappa_settings.json + ``` + +- Copy the contents from the template file into your new local environment file: + + ```bash + cat zappa_settings.example.json > zappa_settings.json + ``` + +4. **Populate Settings File**: + +- Replace all `${...}` variables in `zappa_settings.json` with appropriate output variables. + +5. **Deploy**: + + ```bash + zappa deploy staging + ``` + +Once deployed, Zappa will provide you with a URL. You can use this URL to test the API. + +## Setup Database + +Migrate and load data into the new database. + +1. **Setup ECR Image**: + - Login to the Elastic Container Registry using the following command: + + *Note*: replace `us-east-2` with configured region and `000000000000` with AWS Account ID. + + *Warning*: Configure a credential helper instead of using following command to login. + + ```bash + aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 000000000000.dkr.ecr.us-east-2.amazonaws.com + ``` + + - Build the backend image using the following command: + + ```bash + docker build -t owasp-nest-staging-backend:latest -f docker/Dockerfile . + ``` + + - Tag the image: + *Note*: replace `us-east-2` with configured region and `000000000000` with AWS Account ID. + + ```bash + docker tag owasp-nest-staging-backend:latest 000000000000.dkr.ecr.us-east-2.amazonaws.com/owasp-nest-staging-backend:latest + ``` + + - Push the image: + *Note*: replace `us-east-2` with configured region and `000000000000` with AWS Account ID. + + ```bash + docker push 000000000000.dkr.ecr.us-east-2.amazonaws.com/owasp-nest-staging-backend:latest + ``` + +2. **Upload Fixture to S3**: + - Upload the fixture present in `backend/data` to `nest-fixtures` bucket using the following command: + + ```bash + aws s3 cp data/nest.json.gz s3://nest-fixtures/ + ``` + +3. **Run ECS Tasks**: + - Head over to Elastic Container Service in the AWS Console. + - Click on `owasp-nest-staging-migrate` in `Task Definitions` section. + - Click Deploy > Run Task. + - Use the following configuration: + - Task details + - Task definition revision: LATEST + - Networking: + - VPC: owasp-nest-staging-vpc + - Security group name: select all with `owasp-nest-staging-` prefix. + (*Note*: temporary step, will be further improved) + - Click "Create" + - The task is now running... Click on the task ID to view Logs, Status, etc. + - Follow the same steps for `owasp-nest-staging-load-data` and `owasp-nest-staging-index-data`. + +## Cleaning Up + +- To delete the deployment use the following command: + + ```bash + zappa undeploy staging + ``` + +- To destroy Terraform infrastructure: + + ```bash + terraform destroy + ``` + +## Helpful Commands + +- To update a Zappa `staging` deployment run: + + ```bash + zappa update staging + ``` diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000000..34f3cce420 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,124 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +locals { + common_tags = { + Environment = var.environment + ManagedBy = "Terraform" + Project = var.project_name + } + django_environment_variables = { + DJANGO_ALGOLIA_APPLICATION_ID = var.django_algolia_application_id + DJANGO_ALGOLIA_WRITE_API_KEY = var.django_algolia_write_api_key + DJANGO_ALLOWED_HOSTS = var.django_allowed_hosts + DJANGO_AWS_ACCESS_KEY_ID = var.django_aws_access_key_id + DJANGO_AWS_SECRET_ACCESS_KEY = var.django_aws_secret_access_key + DJANGO_CONFIGURATION = var.django_configuration + DJANGO_DB_HOST = var.django_db_host + DJANGO_DB_NAME = var.django_db_name + DJANGO_DB_USER = var.django_db_user + DJANGO_DB_PORT = var.django_db_port + DJANGO_DB_PASSWORD = var.django_db_password + DJANGO_OPEN_AI_SECRET_KEY = var.django_open_ai_secret_key + DJANGO_REDIS_HOST = var.django_redis_host + DJANGO_REDIS_PASSWORD = var.django_redis_password + DJANGO_SECRET_KEY = var.django_secret_key + DJANGO_SENTRY_DSN = var.django_sentry_dsn + DJANGO_SLACK_BOT_TOKEN = var.django_slack_bot_token + DJANGO_SLACK_SIGNING_SECRET = var.django_slack_signing_secret + } +} + +module "cache" { + source = "./modules/cache" + + common_tags = local.common_tags + environment = var.environment + project_name = var.project_name + redis_auth_token = var.redis_auth_token + redis_engine_version = var.redis_engine_version + redis_node_type = var.redis_node_type + redis_num_cache_nodes = var.redis_num_cache_nodes + redis_port = var.redis_port + security_group_ids = [module.security.redis_sg_id] + subnet_ids = module.networking.private_subnet_ids +} + +module "database" { + source = "./modules/database" + + common_tags = local.common_tags + db_allocated_storage = var.db_allocated_storage + db_backup_retention_period = var.db_backup_retention_period + db_engine_version = var.db_engine_version + db_instance_class = var.db_instance_class + db_name = var.db_name + db_password = var.db_password + db_storage_type = var.db_storage_type + db_subnet_ids = module.networking.private_subnet_ids + db_username = var.db_username + environment = var.environment + project_name = var.project_name + proxy_security_group_ids = [module.security.rds_proxy_sg_id] + security_group_ids = [module.security.rds_sg_id] +} + +module "ecs" { + source = "./modules/ecs" + + aws_region = var.aws_region + common_tags = local.common_tags + django_environment_variables = local.django_environment_variables + environment = var.environment + fixtures_read_only_policy_arn = module.storage.fixtures_read_only_policy_arn + fixtures_s3_bucket = var.fixtures_s3_bucket + lambda_sg_id = module.security.lambda_sg_id + private_subnet_ids = module.networking.private_subnet_ids + project_name = var.project_name +} + +module "networking" { + source = "./modules/networking" + + availability_zones = var.availability_zones + common_tags = local.common_tags + environment = var.environment + private_subnet_cidrs = var.private_subnet_cidrs + project_name = var.project_name + public_subnet_cidrs = var.public_subnet_cidrs + vpc_cidr = var.vpc_cidr +} + +module "security" { + source = "./modules/security" + + common_tags = local.common_tags + db_port = var.db_port + environment = var.environment + project_name = var.project_name + redis_port = var.redis_port + vpc_id = module.networking.vpc_id +} + +module "storage" { + source = "./modules/storage" + + common_tags = local.common_tags + environment = var.environment + fixtures_s3_bucket = var.fixtures_s3_bucket + force_destroy_bucket = var.force_destroy_bucket + project_name = var.project_name + zappa_s3_bucket = var.zappa_s3_bucket +} diff --git a/infrastructure/modules/cache/.terraform.lock.hcl b/infrastructure/modules/cache/.terraform.lock.hcl new file mode 100644 index 0000000000..e15fbddce1 --- /dev/null +++ b/infrastructure/modules/cache/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + constraints = "~> 3.0" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", + "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", + "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", + "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", + "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", + "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", + "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", + "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", + "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", + "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + ] +} diff --git a/infrastructure/modules/cache/main.tf b/infrastructure/modules/cache/main.tf new file mode 100644 index 0000000000..5461264471 --- /dev/null +++ b/infrastructure/modules/cache/main.tf @@ -0,0 +1,61 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +locals { + generate_redis_auth_token = var.redis_auth_token == null || var.redis_auth_token == "" + parameter_group_name = "default.redis${local.redis_major_version}" + redis_auth_token = local.generate_redis_auth_token ? random_password.redis_auth_token[0].result : var.redis_auth_token + redis_major_version = split(".", var.redis_engine_version)[0] +} + +resource "aws_elasticache_subnet_group" "main" { + name = "${var.project_name}-${var.environment}-cache-subnet-group" + subnet_ids = var.subnet_ids + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-cache-subnet-group" + }) +} + +resource "random_password" "redis_auth_token" { + count = local.generate_redis_auth_token ? 1 : 0 + length = 32 + # Redis auth token has specific requirements for special characters. + override_special = "!&#$^<>-" + special = true +} + +resource "aws_elasticache_replication_group" "main" { + at_rest_encryption_enabled = true + auth_token = local.redis_auth_token + auto_minor_version_upgrade = var.auto_minor_version_upgrade + automatic_failover_enabled = var.redis_num_cache_nodes > 1 + description = "${var.project_name} ${var.environment} Redis cache" + engine = "redis" + engine_version = var.redis_engine_version + maintenance_window = var.maintenance_window + node_type = var.redis_node_type + num_cache_clusters = var.redis_num_cache_nodes + parameter_group_name = local.parameter_group_name + port = var.redis_port + replication_group_id = "${var.project_name}-${var.environment}-cache" + security_group_ids = var.security_group_ids + snapshot_retention_limit = var.snapshot_retention_limit + snapshot_window = var.snapshot_window + subnet_group_name = aws_elasticache_subnet_group.main.name + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-redis" + }) + transit_encryption_enabled = true +} diff --git a/infrastructure/modules/cache/outputs.tf b/infrastructure/modules/cache/outputs.tf new file mode 100644 index 0000000000..f1582444ff --- /dev/null +++ b/infrastructure/modules/cache/outputs.tf @@ -0,0 +1,10 @@ +output "redis_auth_token" { + description = "The auth token for Redis" + value = random_password.redis_auth_token[0].result + sensitive = true +} + +output "redis_primary_endpoint" { + description = "The primary endpoint of the Redis replication group" + value = aws_elasticache_replication_group.main.primary_endpoint_address +} diff --git a/infrastructure/modules/cache/variables.tf b/infrastructure/modules/cache/variables.tf new file mode 100644 index 0000000000..584b5557fd --- /dev/null +++ b/infrastructure/modules/cache/variables.tf @@ -0,0 +1,76 @@ +variable "auto_minor_version_upgrade" { + description = "Determines whether minor engine upgrades will be applied automatically." + type = bool + default = true +} + +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string +} + +variable "maintenance_window" { + description = "The weekly time range for when maintenance on the cache cluster is performed." + type = string + default = "mon:05:00-mon:07:00" +} + +variable "project_name" { + description = "The name of the project" + type = string +} + +variable "redis_auth_token" { + description = "The auth token for Redis" + type = string + sensitive = true + default = null +} + +variable "redis_engine_version" { + description = "The version of the Redis engine" + type = string +} + +variable "redis_node_type" { + description = "The node type for the Redis cache" + type = string +} + +variable "redis_num_cache_nodes" { + description = "The number of cache nodes in the Redis cluster" + type = number +} + +variable "redis_port" { + description = "The port for the Redis cache" + type = number +} + +variable "security_group_ids" { + description = "A list of security group IDs to associate with the Redis cache" + type = list(string) +} + +variable "snapshot_retention_limit" { + description = "The number of days for which automatic snapshots are retained." + type = number + default = 5 +} + +variable "snapshot_window" { + description = "The daily time range (in UTC) during which ElastiCache will begin taking a daily snapshot." + type = string + default = "03:00-05:00" +} + +variable "subnet_ids" { + description = "A list of subnet IDs for the cache subnet group" + type = list(string) +} diff --git a/infrastructure/modules/database/.terraform.lock.hcl b/infrastructure/modules/database/.terraform.lock.hcl new file mode 100644 index 0000000000..e15fbddce1 --- /dev/null +++ b/infrastructure/modules/database/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + constraints = "~> 3.0" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", + "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", + "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", + "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", + "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", + "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", + "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", + "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", + "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", + "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + ] +} diff --git a/infrastructure/modules/database/main.tf b/infrastructure/modules/database/main.tf new file mode 100644 index 0000000000..b74eaaafdc --- /dev/null +++ b/infrastructure/modules/database/main.tf @@ -0,0 +1,148 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +locals { + db_password = local.generate_db_password ? random_password.db_password[0].result : var.db_password + generate_db_password = var.db_password == null || var.db_password == "" +} + +resource "aws_db_subnet_group" "main" { + name = "${var.project_name}-${var.environment}-db-subnet-group" + subnet_ids = var.db_subnet_ids + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-db-subnet-group" + }) +} + +resource "random_password" "db_password" { + count = local.generate_db_password ? 1 : 0 + length = 32 + # Avoid special characters that might cause issues + override_special = "!#$%&*()-_=+[]{}<>:?" + special = true +} + +resource "aws_db_instance" "main" { + allocated_storage = var.db_allocated_storage + backup_retention_period = var.db_backup_retention_period + backup_window = var.db_backup_window + copy_tags_to_snapshot = var.db_copy_tags_to_snapshot + db_name = var.db_name + db_subnet_group_name = aws_db_subnet_group.main.name + enabled_cloudwatch_logs_exports = var.db_enabled_cloudwatch_logs_exports + engine = "postgres" + engine_version = var.db_engine_version + identifier = lower("${var.project_name}-${var.environment}-db") + instance_class = var.db_instance_class + maintenance_window = var.db_maintenance_window + password = local.db_password + publicly_accessible = false + skip_final_snapshot = var.db_skip_final_snapshot + storage_encrypted = true + storage_type = var.db_storage_type + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-postgres" + }) + username = var.db_username + vpc_security_group_ids = var.security_group_ids +} + +resource "aws_secretsmanager_secret" "db_credentials" { + description = "Stores the credentials for the RDS database." + name = "${var.project_name}-${var.environment}-db-credentials" + recovery_window_in_days = var.secret_recovery_window_in_days + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-db-credentials" + }) +} + +resource "aws_secretsmanager_secret_version" "db_credentials" { + secret_id = aws_secretsmanager_secret.db_credentials.id + secret_string = jsonencode({ + username = var.db_username + password = local.db_password + }) +} + +resource "aws_iam_role" "rds_proxy" { + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "rds.amazonaws.com" + } + } + ] + }) + name = "${var.project_name}-${var.environment}-rds-proxy-role" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-rds-proxy-role" + }) +} + +resource "aws_iam_role_policy" "rds_proxy" { + name = "${var.project_name}-${var.environment}-rds-proxy-policy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "secretsmanager:GetSecretValue" + ] + Effect = "Allow" + Resource = aws_secretsmanager_secret.db_credentials.arn + } + ] + }) + role = aws_iam_role.rds_proxy.id +} + +resource "aws_db_proxy" "main" { + auth { + auth_scheme = "SECRETS" + description = "Database credentials" + iam_auth = "DISABLED" + secret_arn = aws_secretsmanager_secret.db_credentials.arn + } + debug_logging = false + engine_family = "POSTGRESQL" + idle_client_timeout = 1800 + name = "${var.project_name}-${var.environment}-proxy" + require_tls = true + role_arn = aws_iam_role.rds_proxy.arn + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-rds-proxy" + }) + vpc_security_group_ids = var.proxy_security_group_ids + vpc_subnet_ids = var.db_subnet_ids +} + +resource "aws_db_proxy_default_target_group" "main" { + connection_pool_config { + connection_borrow_timeout = 120 + max_connections_percent = 100 + max_idle_connections_percent = 50 + } + db_proxy_name = aws_db_proxy.main.name +} + +resource "aws_db_proxy_target" "main" { + db_instance_identifier = aws_db_instance.main.identifier + db_proxy_name = aws_db_proxy.main.name + target_group_name = aws_db_proxy_default_target_group.main.name +} diff --git a/infrastructure/modules/database/outputs.tf b/infrastructure/modules/database/outputs.tf new file mode 100644 index 0000000000..121c915493 --- /dev/null +++ b/infrastructure/modules/database/outputs.tf @@ -0,0 +1,10 @@ +output "db_password" { + description = "The password for the RDS database" + value = local.db_password + sensitive = true +} + +output "db_proxy_endpoint" { + description = "The endpoint of the RDS proxy" + value = aws_db_proxy.main.endpoint +} diff --git a/infrastructure/modules/database/variables.tf b/infrastructure/modules/database/variables.tf new file mode 100644 index 0000000000..6977cd7d89 --- /dev/null +++ b/infrastructure/modules/database/variables.tf @@ -0,0 +1,110 @@ +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "db_allocated_storage" { + description = "The allocated storage for the RDS database in GB" + type = number +} + +variable "db_backup_retention_period" { + description = "The number of days to retain backups for" + type = number + default = 7 +} + +variable "db_backup_window" { + description = "The daily time range (in UTC) during which automated backups are created." + type = string + default = "03:00-04:00" +} + +variable "db_copy_tags_to_snapshot" { + description = "Specifies whether to copy all instance tags to snapshots." + type = bool + default = true +} + +variable "db_enabled_cloudwatch_logs_exports" { + description = "List of log types to export to CloudWatch Logs." + type = list(string) + default = ["postgresql", "upgrade"] +} + +variable "db_engine_version" { + description = "The version of the PostgreSQL engine" + type = string +} + +variable "db_instance_class" { + description = "The instance class for the RDS database" + type = string +} + +variable "db_maintenance_window" { + description = "The weekly time range (in UTC) during which system maintenance can occur." + type = string + default = "mon:04:00-mon:05:00" +} + +variable "db_name" { + description = "The name of the RDS database" + type = string +} + +variable "db_password" { + description = "The password for the RDS database" + type = string + sensitive = true + default = null +} + +variable "db_skip_final_snapshot" { + description = "Determines whether a final DB snapshot is created before the DB instance is deleted." + type = bool + default = true +} + +variable "db_storage_type" { + description = "The storage type for the RDS database" + type = string + default = "gp3" +} + +variable "db_subnet_ids" { + description = "A list of subnet IDs for the DB subnet group" + type = list(string) +} + +variable "db_username" { + description = "The username for the RDS database" + type = string +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string +} + +variable "project_name" { + description = "The name of the project" + type = string +} + +variable "proxy_security_group_ids" { + description = "A list of security group IDs to associate with the RDS proxy" + type = list(string) +} + +variable "secret_recovery_window_in_days" { + description = "The number of days that Secrets Manager waits before it can delete the secret. Set to 0 to delete immediately." + type = number + default = 0 +} + +variable "security_group_ids" { + description = "A list of security group IDs to associate with the RDS database" + type = list(string) +} diff --git a/infrastructure/modules/ecs/.terraform.lock.hcl b/infrastructure/modules/ecs/.terraform.lock.hcl new file mode 100644 index 0000000000..2efdd35b34 --- /dev/null +++ b/infrastructure/modules/ecs/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} diff --git a/infrastructure/modules/ecs/main.tf b/infrastructure/modules/ecs/main.tf new file mode 100644 index 0000000000..2ba8c19d3c --- /dev/null +++ b/infrastructure/modules/ecs/main.tf @@ -0,0 +1,232 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +resource "aws_ecs_cluster" "main" { + name = "${var.project_name}-${var.environment}-cluster" + tags = var.common_tags +} + +resource "aws_ecr_repository" "main" { + name = "${var.project_name}-${var.environment}-backend" + image_scanning_configuration { + scan_on_push = true + } + tags = var.common_tags +} + +resource "aws_iam_role" "ecs_tasks_execution_role" { + name = "${var.project_name}-${var.environment}-ecs-tasks-execution-role" + tags = var.common_tags + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs_tasks_execution_role_policy" { + role = aws_iam_role.ecs_tasks_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role" "ecs_task_role" { + name = "${var.project_name}-${var.environment}-ecs-task-role" + tags = var.common_tags + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs_task_role_fixtures_s3_access" { + role = aws_iam_role.ecs_task_role.name + policy_arn = var.fixtures_read_only_policy_arn +} + +resource "aws_iam_role" "event_bridge_role" { + name = "${var.project_name}-${var.environment}-event-bridge-role" + tags = var.common_tags + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "events.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "event_bridge_role_policy" { + role = aws_iam_role.event_bridge_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole" +} + +module "sync_data_task" { + source = "./modules/task" + + aws_region = var.aws_region + command = ["python", "manage.py", "sync-data"] + common_tags = var.common_tags + container_environment = var.django_environment_variables + cpu = var.sync_data_task_cpu + ecs_cluster_arn = aws_ecs_cluster.main.arn + ecs_tasks_execution_role_arn = aws_iam_role.ecs_tasks_execution_role.arn + environment = var.environment + event_bridge_role_arn = aws_iam_role.event_bridge_role.arn + image_url = aws_ecr_repository.main.repository_url + memory = var.sync_data_task_memory + private_subnet_ids = var.private_subnet_ids + project_name = var.project_name + schedule_expression = "cron(17 05 * * ? *)" + security_group_ids = [var.lambda_sg_id] + task_name = "sync-data" +} + +module "owasp_update_project_health_metrics_task" { + source = "./modules/task" + + aws_region = var.aws_region + command = ["/bin/sh", "-c", "python manage.py owasp-update-project-health-requirements && python manage.py owasp-update-project-health-metrics"] + common_tags = var.common_tags + container_environment = var.django_environment_variables + cpu = var.update_project_health_metrics_task_cpu + ecs_cluster_arn = aws_ecs_cluster.main.arn + ecs_tasks_execution_role_arn = aws_iam_role.ecs_tasks_execution_role.arn + environment = var.environment + event_bridge_role_arn = aws_iam_role.event_bridge_role.arn + image_url = aws_ecr_repository.main.repository_url + memory = var.update_project_health_metrics_task_memory + private_subnet_ids = var.private_subnet_ids + project_name = var.project_name + schedule_expression = "cron(17 17 * * ? *)" + security_group_ids = [var.lambda_sg_id] + task_name = "owasp-update-project-health-metrics" +} + +module "owasp_update_project_health_scores_task" { + source = "./modules/task" + + aws_region = var.aws_region + command = ["python", "manage.py", "owasp-update-project-health-scores"] + common_tags = var.common_tags + container_environment = var.django_environment_variables + cpu = var.update_project_health_scores_task_cpu + ecs_cluster_arn = aws_ecs_cluster.main.arn + ecs_tasks_execution_role_arn = aws_iam_role.ecs_tasks_execution_role.arn + environment = var.environment + event_bridge_role_arn = aws_iam_role.event_bridge_role.arn + image_url = aws_ecr_repository.main.repository_url + memory = var.update_project_health_scores_task_memory + private_subnet_ids = var.private_subnet_ids + project_name = var.project_name + schedule_expression = "cron(22 17 * * ? *)" + security_group_ids = [var.lambda_sg_id] + task_name = "owasp-update-project-health-scores" +} + +module "migrate_task" { + source = "./modules/task" + + aws_region = var.aws_region + command = ["python", "manage.py", "migrate"] + common_tags = var.common_tags + container_environment = var.django_environment_variables + cpu = var.migrate_task_cpu + ecs_cluster_arn = aws_ecs_cluster.main.arn + ecs_tasks_execution_role_arn = aws_iam_role.ecs_tasks_execution_role.arn + environment = var.environment + image_url = "${aws_ecr_repository.main.repository_url}:latest" + memory = var.migrate_task_memory + private_subnet_ids = var.private_subnet_ids + project_name = var.project_name + security_group_ids = [var.lambda_sg_id] + task_name = "migrate" +} + +module "load_data_task" { + source = "./modules/task" + + aws_region = var.aws_region + command = [ + "/bin/sh", + "-c", + <<-EOT + set -e + pip install --target=/tmp/awscli-packages awscli + export PYTHONPATH="/tmp/awscli-packages:$PYTHONPATH" + python /tmp/awscli-packages/bin/aws s3 cp s3://${var.fixtures_s3_bucket}/nest.json.gz /tmp/nest.json.gz + python manage.py loaddata /tmp/nest.json.gz -v 3 + EOT + ] + common_tags = var.common_tags + container_environment = var.django_environment_variables + cpu = var.load_data_task_cpu + ecs_cluster_arn = aws_ecs_cluster.main.arn + ecs_tasks_execution_role_arn = aws_iam_role.ecs_tasks_execution_role.arn + environment = var.environment + image_url = "${aws_ecr_repository.main.repository_url}:latest" + memory = var.load_data_task_memory + private_subnet_ids = var.private_subnet_ids + project_name = var.project_name + security_group_ids = [var.lambda_sg_id] + task_name = "load-data" + task_role_arn = aws_iam_role.ecs_task_role.arn +} + +module "index_data_task" { + source = "./modules/task" + + aws_region = var.aws_region + command = [ + "/bin/sh", + "-c", + <<-EOT + set -e + python manage.py algolia_reindex + python manage.py algolia_update_replicas + python manage.py algolia_update_synonyms + EOT + ] + common_tags = var.common_tags + container_environment = var.django_environment_variables + cpu = var.index_data_task_cpu + ecs_cluster_arn = aws_ecs_cluster.main.arn + ecs_tasks_execution_role_arn = aws_iam_role.ecs_tasks_execution_role.arn + environment = var.environment + image_url = "${aws_ecr_repository.main.repository_url}:latest" + memory = var.index_data_task_memory + private_subnet_ids = var.private_subnet_ids + project_name = var.project_name + security_group_ids = [var.lambda_sg_id] + task_name = "index-data" +} diff --git a/infrastructure/modules/ecs/modules/task/.terraform.lock.hcl b/infrastructure/modules/ecs/modules/task/.terraform.lock.hcl new file mode 100644 index 0000000000..2efdd35b34 --- /dev/null +++ b/infrastructure/modules/ecs/modules/task/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} diff --git a/infrastructure/modules/ecs/modules/task/main.tf b/infrastructure/modules/ecs/modules/task/main.tf new file mode 100644 index 0000000000..4f43fecd3d --- /dev/null +++ b/infrastructure/modules/ecs/modules/task/main.tf @@ -0,0 +1,82 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +resource "aws_cloudwatch_log_group" "task" { + name = "/ecs/${var.project_name}-${var.environment}-${var.task_name}" + retention_in_days = var.log_retention_in_days + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-${var.task_name}-logs" + }) +} + +resource "aws_ecs_task_definition" "task" { + family = "${var.project_name}-${var.environment}-${var.task_name}" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.cpu + memory = var.memory + execution_role_arn = var.ecs_tasks_execution_role_arn + task_role_arn = var.task_role_arn + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-${var.task_name}-task-def" + }) + + container_definitions = jsonencode([ + { + name = "backend" + image = var.image_url + command = var.command + essential = true + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.task.name + "awslogs-region" = var.aws_region + "awslogs-stream-prefix" = "ecs" + } + } + environment = [for name, value in var.container_environment : { + name = name + value = value + }] + } + ]) +} + +resource "aws_cloudwatch_event_rule" "task" { + count = var.schedule_expression != null ? 1 : 0 + + name = "${var.project_name}-${var.environment}-${var.task_name}-rule" + description = "Fires on a schedule to trigger the ${var.task_name} task" + schedule_expression = var.schedule_expression + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-${var.task_name}-rule" + }) +} + +resource "aws_cloudwatch_event_target" "task" { + count = var.schedule_expression != null ? 1 : 0 + + rule = aws_cloudwatch_event_rule.task[0].name + target_id = "${var.project_name}-${var.environment}-${var.task_name}-target" + arn = var.ecs_cluster_arn + role_arn = var.event_bridge_role_arn + + ecs_target { + task_definition_arn = aws_ecs_task_definition.task.arn + launch_type = "FARGATE" + network_configuration { + subnets = var.private_subnet_ids + security_groups = var.security_group_ids + assign_public_ip = false + } + } +} diff --git a/infrastructure/modules/ecs/modules/task/variables.tf b/infrastructure/modules/ecs/modules/task/variables.tf new file mode 100644 index 0000000000..85e7ce9b06 --- /dev/null +++ b/infrastructure/modules/ecs/modules/task/variables.tf @@ -0,0 +1,95 @@ +variable "aws_region" { + description = "The AWS region for the CloudWatch logs." + type = string +} + +variable "command" { + description = "The command to run in the container." + type = list(string) +} + +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "container_environment" { + description = "A map of environment variables to pass to the container." + type = map(string) + default = {} +} + +variable "cpu" { + description = "The CPU units to allocate for the task." + type = string +} + +variable "ecs_cluster_arn" { + description = "The ARN of the ECS cluster." + type = string +} + +variable "ecs_tasks_execution_role_arn" { + description = "The ARN of the ECS task execution role." + type = string +} + +variable "environment" { + description = "The environment (e.g., staging, production)." + type = string +} + +variable "event_bridge_role_arn" { + description = "The ARN of the EventBridge role to trigger the task. Only required for scheduled tasks." + type = string + default = null +} + +variable "image_url" { + description = "The URL of the ECR image to run." + type = string +} + +variable "log_retention_in_days" { + description = "The number of days to retain log events." + type = number + default = 30 +} + +variable "memory" { + description = "The memory (in MiB) to allocate for the task." + type = string +} + +variable "private_subnet_ids" { + description = "A list of private subnet IDs for the task." + type = list(string) +} + +variable "project_name" { + description = "The name of the project." + type = string +} + +variable "schedule_expression" { + description = "The cron expression for the schedule. If null, the task is not scheduled." + type = string + default = null +} + +variable "security_group_ids" { + description = "A list of security group IDs to associate with the task." + type = list(string) +} + +variable "task_name" { + description = "The unique name of the task." + type = string +} + +variable "task_role_arn" { + description = "The ARN of the IAM role for the task." + type = string + default = null +} diff --git a/infrastructure/modules/ecs/outputs.tf b/infrastructure/modules/ecs/outputs.tf new file mode 100644 index 0000000000..acb7cd7fdc --- /dev/null +++ b/infrastructure/modules/ecs/outputs.tf @@ -0,0 +1,9 @@ +output "ecs_cluster_arn" { + description = "The ARN of the ECS cluster" + value = aws_ecs_cluster.main.arn +} + +output "ecr_repository_url" { + description = "The URL of the ECR repository" + value = aws_ecr_repository.main.repository_url +} diff --git a/infrastructure/modules/ecs/variables.tf b/infrastructure/modules/ecs/variables.tf new file mode 100644 index 0000000000..5133bf9b77 --- /dev/null +++ b/infrastructure/modules/ecs/variables.tf @@ -0,0 +1,119 @@ +variable "aws_region" { + description = "The AWS region" + type = string +} + +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "django_environment_variables" { + description = "A map of environment variables for the Django container." + type = map(string) + default = {} + sensitive = true +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string +} + +variable "fixtures_read_only_policy_arn" { + description = "The ARN of the fixtures read-only IAM policy" + type = string +} + +variable "fixtures_s3_bucket" { + description = "The name of the S3 bucket for fixtures" + type = string +} + +variable "index_data_task_cpu" { + description = "The CPU for the index-data task" + type = string + default = "256" +} + +variable "index_data_task_memory" { + description = "The memory for the index-data task" + type = string + default = "2048" +} + +variable "lambda_sg_id" { + description = "The ID of the security group for the Lambda function" + type = string +} + +variable "load_data_task_cpu" { + description = "The CPU for the load-data task" + type = string + default = "256" +} + +variable "load_data_task_memory" { + description = "The memory for the load-data task" + type = string + default = "2048" +} + +variable "migrate_task_cpu" { + description = "The CPU for the migrate task" + type = string + default = "256" +} + +variable "migrate_task_memory" { + description = "The memory for the migrate task" + type = string + default = "2048" +} + +variable "private_subnet_ids" { + description = "A list of private subnet IDs" + type = list(string) +} + +variable "project_name" { + description = "The name of the project" + type = string +} + +variable "sync_data_task_cpu" { + description = "The CPU for the sync-data task" + type = string + default = "256" +} + +variable "sync_data_task_memory" { + description = "The memory for the sync-data task" + type = string + default = "2048" +} + +variable "update_project_health_metrics_task_cpu" { + description = "The CPU for the update-project-health-metrics task" + type = string + default = "256" +} + +variable "update_project_health_metrics_task_memory" { + description = "The memory for the update-project-health-metrics task" + type = string + default = "2048" +} + +variable "update_project_health_scores_task_cpu" { + description = "The CPU for the update-project-health-scores task" + type = string + default = "256" +} + +variable "update_project_health_scores_task_memory" { + description = "The memory for the update-project-health-scores task" + type = string + default = "2048" +} diff --git a/infrastructure/modules/networking/.terraform.lock.hcl b/infrastructure/modules/networking/.terraform.lock.hcl new file mode 100644 index 0000000000..e15fbddce1 --- /dev/null +++ b/infrastructure/modules/networking/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + constraints = "~> 3.0" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", + "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", + "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", + "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", + "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", + "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", + "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", + "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", + "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", + "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + ] +} diff --git a/infrastructure/modules/networking/main.tf b/infrastructure/modules/networking/main.tf new file mode 100644 index 0000000000..691ef83184 --- /dev/null +++ b/infrastructure/modules/networking/main.tf @@ -0,0 +1,104 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-vpc" + }) +} + +resource "aws_internet_gateway" "main" { + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-igw" + }) + vpc_id = aws_vpc.main.id +} + +resource "aws_subnet" "public" { + availability_zone = var.availability_zones[count.index] + cidr_block = var.public_subnet_cidrs[count.index] + count = length(var.public_subnet_cidrs) + map_public_ip_on_launch = true + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-public-${var.availability_zones[count.index]}" + Type = "Public" + }) + vpc_id = aws_vpc.main.id +} + +resource "aws_subnet" "private" { + availability_zone = var.availability_zones[count.index] + cidr_block = var.private_subnet_cidrs[count.index] + count = length(var.private_subnet_cidrs) + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-private-${var.availability_zones[count.index]}" + Type = "Private" + }) + vpc_id = aws_vpc.main.id +} + +resource "aws_eip" "nat" { + depends_on = [aws_internet_gateway.main] + domain = "vpc" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-nat-eip" + }) +} + +resource "aws_nat_gateway" "main" { + allocation_id = aws_eip.nat.id + depends_on = [aws_internet_gateway.main] + subnet_id = aws_subnet.public[0].id + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-nat" + }) +} + +resource "aws_route_table" "public" { + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-public-rt" + }) + vpc_id = aws_vpc.main.id +} + +resource "aws_route_table" "private" { + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main.id + } + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-private-rt" + }) + vpc_id = aws_vpc.main.id +} + +resource "aws_route_table_association" "public" { + count = length(aws_subnet.public) + route_table_id = aws_route_table.public.id + subnet_id = aws_subnet.public[count.index].id +} + +resource "aws_route_table_association" "private" { + count = length(aws_subnet.private) + route_table_id = aws_route_table.private.id + subnet_id = aws_subnet.private[count.index].id +} diff --git a/infrastructure/modules/networking/outputs.tf b/infrastructure/modules/networking/outputs.tf new file mode 100644 index 0000000000..ca4f0573cb --- /dev/null +++ b/infrastructure/modules/networking/outputs.tf @@ -0,0 +1,14 @@ +output "vpc_id" { + description = "The ID of the VPC" + value = aws_vpc.main.id +} + +output "public_subnet_ids" { + description = "A list of public subnet IDs" + value = aws_subnet.public[*].id +} + +output "private_subnet_ids" { + description = "A list of private subnet IDs" + value = aws_subnet.private[*].id +} diff --git a/infrastructure/modules/networking/variables.tf b/infrastructure/modules/networking/variables.tf new file mode 100644 index 0000000000..74c108743d --- /dev/null +++ b/infrastructure/modules/networking/variables.tf @@ -0,0 +1,35 @@ +variable "availability_zones" { + description = "A list of availability zones for the VPC" + type = list(string) +} + +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string +} + +variable "private_subnet_cidrs" { + description = "A list of CIDR blocks for the private subnets" + type = list(string) +} + +variable "project_name" { + description = "The name of the project" + type = string +} + +variable "public_subnet_cidrs" { + description = "A list of CIDR blocks for the public subnets" + type = list(string) +} + +variable "vpc_cidr" { + description = "The CIDR block for the VPC" + type = string +} diff --git a/infrastructure/modules/security/.terraform.lock.hcl b/infrastructure/modules/security/.terraform.lock.hcl new file mode 100644 index 0000000000..e15fbddce1 --- /dev/null +++ b/infrastructure/modules/security/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + constraints = "~> 3.0" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", + "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", + "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", + "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", + "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", + "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", + "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", + "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", + "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", + "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + ] +} diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf new file mode 100644 index 0000000000..a6b155f0b7 --- /dev/null +++ b/infrastructure/modules/security/main.tf @@ -0,0 +1,106 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +resource "aws_security_group" "lambda" { + description = "Security group for Lambda functions (Zappa)" + name = "${var.project_name}-${var.environment}-lambda-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-lambda-sg" + }) + vpc_id = var.vpc_id + + egress { + cidr_blocks = var.default_egress_cidr_blocks + description = "Allow all outbound traffic" + from_port = 0 + protocol = "-1" + to_port = 0 + } +} + +resource "aws_security_group" "rds_proxy" { + description = "Security group for RDS Proxy" + name = "${var.project_name}-${var.environment}-rds-proxy-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-rds-proxy-sg" + }) + vpc_id = var.vpc_id + + egress { + cidr_blocks = var.default_egress_cidr_blocks + description = "Allow all outbound traffic" + from_port = 0 + protocol = "-1" + to_port = 0 + } + + ingress { + description = "PostgreSQL from Lambda" + from_port = var.db_port + protocol = "tcp" + security_groups = [aws_security_group.lambda.id] + to_port = var.db_port + } +} + +resource "aws_security_group" "rds" { + description = "Security group for RDS PostgreSQL" + name = "${var.project_name}-${var.environment}-rds-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-rds-sg" + }) + vpc_id = var.vpc_id + + egress { + cidr_blocks = var.default_egress_cidr_blocks + description = "Allow all outbound traffic" + from_port = 0 + protocol = "-1" + to_port = 0 + } + + ingress { + description = "PostgreSQL from RDS Proxy" + from_port = var.db_port + protocol = "tcp" + security_groups = [aws_security_group.rds_proxy.id] + to_port = var.db_port + } +} + +resource "aws_security_group" "redis" { + description = "Security group for ElastiCache Redis" + name = "${var.project_name}-${var.environment}-redis-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-redis-sg" + }) + vpc_id = var.vpc_id + + egress { + cidr_blocks = var.default_egress_cidr_blocks + description = "Allow all outbound traffic" + from_port = 0 + protocol = "-1" + to_port = 0 + } + + ingress { + description = "Redis from Lambda" + from_port = var.redis_port + protocol = "tcp" + security_groups = [aws_security_group.lambda.id] + to_port = var.redis_port + } +} diff --git a/infrastructure/modules/security/outputs.tf b/infrastructure/modules/security/outputs.tf new file mode 100644 index 0000000000..eea4186b25 --- /dev/null +++ b/infrastructure/modules/security/outputs.tf @@ -0,0 +1,19 @@ +output "lambda_sg_id" { + description = "The ID of the Lambda security group" + value = aws_security_group.lambda.id +} + +output "rds_proxy_sg_id" { + description = "The ID of the RDS proxy security group" + value = aws_security_group.rds_proxy.id +} + +output "rds_sg_id" { + description = "The ID of the RDS security group" + value = aws_security_group.rds.id +} + +output "redis_sg_id" { + description = "The ID of the Redis security group" + value = aws_security_group.redis.id +} diff --git a/infrastructure/modules/security/variables.tf b/infrastructure/modules/security/variables.tf new file mode 100644 index 0000000000..5c84a60699 --- /dev/null +++ b/infrastructure/modules/security/variables.tf @@ -0,0 +1,36 @@ +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "db_port" { + description = "The port for the RDS database" + type = number +} + +variable "default_egress_cidr_blocks" { + description = "A list of CIDR blocks to allow for default egress traffic." + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string +} + +variable "project_name" { + description = "The name of the project" + type = string +} + +variable "redis_port" { + description = "The port for the Redis cache" + type = number +} + +variable "vpc_id" { + description = "The ID of the VPC" + type = string +} diff --git a/infrastructure/modules/storage/.terraform.lock.hcl b/infrastructure/modules/storage/.terraform.lock.hcl new file mode 100644 index 0000000000..e15fbddce1 --- /dev/null +++ b/infrastructure/modules/storage/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.17.0" + constraints = "~> 6.0" + hashes = [ + "h1:65zxvr7oxROr5hqTWQtoS5HsGOBwUko7douoc9Azptc=", + "zh:157063d66cd4b5fc650f20f56127e19c9da5d135f4231f9ca0c19a1c0bf6e29d", + "zh:2050dc03304b42204e6c58bbb1a2afd4feeac7db55d7c06be77c6b1e2ab46a0f", + "zh:2a7f7751eef636ca064700cc4574b9b54a2596d9e2e86b91c45127410d9724c6", + "zh:335fd7bb44bebfc4dd1db1c013947e1dde2518c6f2d846aac13b7314414ce461", + "zh:545c248d2eb601a7b45a34313096cae0a5201ccf31e7fd99428357ef800051e0", + "zh:57d19883a6367c245e885856a1c5395c4c743c20feff631ea4ec7b5e16826281", + "zh:66d4f080b8c268d65e8c4758ed57234e5a19deff6073ffc3753b9a4cc177b54e", + "zh:6ad50de35970f15e1ed41d39742290c1be80600b7df3a9fbb4c02f353b9586cf", + "zh:7af42fa531e4dcb3ddb09f71ca988e90626abbf56a45981c2a6c01d0b364a51b", + "zh:9a6a535a879314a9137ec9d3e858b7c490a962050845cf62620ba2bf4ae916a8", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ca213e0262c8f686fcd40e3fc84d67b8eea1596de988c13d4a8ecd4522ede669", + "zh:cc4132f682e9bf17c0649928ad92af4da07ffe7bccfe615d955225cdcf9e7f09", + "zh:dfe6de43496d2e2b6dff131fef6ada1e15f1fbba3d47235c751564d22003d05e", + "zh:e37d035fa02693a3d47fe636076cce50b6579b6adc0a36a7cf0456a2331c99ec", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + constraints = "~> 3.0" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", + "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", + "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", + "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", + "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", + "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", + "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", + "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", + "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", + "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", + ] +} diff --git a/infrastructure/modules/storage/main.tf b/infrastructure/modules/storage/main.tf new file mode 100644 index 0000000000..e0c29ee8cd --- /dev/null +++ b/infrastructure/modules/storage/main.tf @@ -0,0 +1,138 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +data "aws_iam_policy_document" "zappa" { + statement { + actions = ["s3:*"] + condition { + test = "Bool" + variable = "aws:SecureTransport" + values = ["false"] + } + effect = "Deny" + principals { + type = "*" + identifiers = ["*"] + } + resources = [ + aws_s3_bucket.zappa.arn, + "${aws_s3_bucket.zappa.arn}/*", + ] + sid = "EnforceTls" + } +} + +resource "aws_iam_policy" "fixtures_read_only" { + name = "${var.project_name}-${var.environment}-fixtures-read-only" + description = "Allows read-only access to the fixtures S3 bucket" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:GetObject" + ] + Effect = "Allow" + Resource = "arn:aws:s3:::${var.fixtures_s3_bucket}/*" + } + ] + }) +} + +resource "aws_s3_bucket" "fixtures" { # NOSONAR + bucket = var.fixtures_s3_bucket + tags = var.common_tags +} + +resource "aws_s3_bucket_lifecycle_configuration" "zappa" { + bucket = aws_s3_bucket.zappa.id + + rule { + abort_incomplete_multipart_upload { + days_after_initiation = var.abort_incomplete_multipart_upload_days + } + id = "delete-old-versions" + noncurrent_version_expiration { + noncurrent_days = var.noncurrent_version_expiration_days + } + status = "Enabled" + } +} + +resource "aws_s3_bucket" "zappa" { # NOSONAR + bucket = var.zappa_s3_bucket + force_destroy = var.force_destroy_bucket + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-zappa-deployments" + }) +} + +resource "aws_s3_bucket_policy" "zappa" { + bucket = aws_s3_bucket.zappa.id + policy = data.aws_iam_policy_document.zappa.json +} + +resource "aws_s3_bucket_public_access_block" "fixtures" { + block_public_acls = true + block_public_policy = true + bucket = aws_s3_bucket.fixtures.id + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_public_access_block" "zappa" { + block_public_acls = true + block_public_policy = true + bucket = aws_s3_bucket.zappa.id + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "fixtures" { + bucket = aws_s3_bucket.fixtures.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "zappa" { + bucket = aws_s3_bucket.zappa.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_versioning" "fixtures" { + bucket = aws_s3_bucket.fixtures.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_versioning" "zappa" { + bucket = aws_s3_bucket.zappa.id + + versioning_configuration { + status = "Enabled" + } +} diff --git a/infrastructure/modules/storage/outputs.tf b/infrastructure/modules/storage/outputs.tf new file mode 100644 index 0000000000..707b4d324e --- /dev/null +++ b/infrastructure/modules/storage/outputs.tf @@ -0,0 +1,19 @@ +output "fixtures_read_only_policy_arn" { + description = "The ARN of the fixtures read-only IAM policy" + value = aws_iam_policy.fixtures_read_only.arn +} + +output "fixtures_s3_bucket_arn" { + description = "The ARN of the S3 bucket for fixtures" + value = aws_s3_bucket.fixtures.arn +} + +output "zappa_s3_bucket" { + description = "The name of the S3 bucket for Zappa deployments" + value = aws_s3_bucket.zappa +} + +output "zappa_s3_bucket_arn" { + description = "The ARN of the S3 bucket for Zappa deployments" + value = aws_s3_bucket.zappa.arn +} diff --git a/infrastructure/modules/storage/variables.tf b/infrastructure/modules/storage/variables.tf new file mode 100644 index 0000000000..7999d8cb10 --- /dev/null +++ b/infrastructure/modules/storage/variables.tf @@ -0,0 +1,43 @@ +variable "abort_incomplete_multipart_upload_days" { + description = "Specifies the number of days after which an incomplete multipart upload is aborted." + type = number + default = 7 +} + +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string +} + +variable "fixtures_s3_bucket" { + description = "The name of the S3 bucket for fixtures" + type = string +} + +variable "force_destroy_bucket" { + description = "If true, deletes all objects from the bucket when the bucket is destroyed." + type = bool + default = false +} + +variable "noncurrent_version_expiration_days" { + description = "Specifies the number of days an object is noncurrent before it is expired." + type = number + default = 30 +} + +variable "project_name" { + description = "The name of the project" + type = string +} + +variable "zappa_s3_bucket" { + description = "The name of the S3 bucket for Zappa deployments" + type = string +} diff --git a/infrastructure/outputs.tf b/infrastructure/outputs.tf new file mode 100644 index 0000000000..dc2e0a9ba9 --- /dev/null +++ b/infrastructure/outputs.tf @@ -0,0 +1,41 @@ +output "database_endpoint" { + description = "The endpoint of the RDS proxy" + value = module.database.db_proxy_endpoint +} + +output "redis_endpoint" { + description = "The endpoint of the Redis cache" + value = module.cache.redis_primary_endpoint +} + +output "db_password" { + description = "The password for the RDS database" + value = module.database.db_password + sensitive = true +} + +output "redis_auth_token" { + description = "The auth token for Redis" + value = module.cache.redis_auth_token + sensitive = true +} + +output "private_subnet_ids" { + description = "A list of private subnet IDs" + value = module.networking.private_subnet_ids +} + +output "lambda_security_group_id" { + description = "The ID of the security group for the Lambda function" + value = module.security.lambda_sg_id +} + +output "ecr_repository_url" { + description = "The URL of the ECR repository" + value = module.ecs.ecr_repository_url +} + +output "zappa_s3_bucket" { + description = "The name of the S3 bucket for Zappa deployments" + value = module.storage.zappa_s3_bucket.bucket +} diff --git a/infrastructure/providers.tf b/infrastructure/providers.tf new file mode 100644 index 0000000000..c9d7ccbdea --- /dev/null +++ b/infrastructure/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.aws_region +} diff --git a/infrastructure/terraform.tfvars.example b/infrastructure/terraform.tfvars.example new file mode 100644 index 0000000000..a654271016 --- /dev/null +++ b/infrastructure/terraform.tfvars.example @@ -0,0 +1,23 @@ +availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] +aws_region = "us-east-2" +django_algolia_application_id = "" +django_algolia_write_api_key = "" +django_allowed_hosts = ".execute-api.us-east-2.amazonaws.com" +django_aws_access_key_id = "" +django_aws_secret_access_key = "" +django_configuration = "Staging" +django_db_host = "" +django_db_name = "owasp_nest" +django_db_user = "owasp_nest_db_user" +django_db_port = "5432" +django_db_password = "" +django_open_ai_secret_key = "" +django_redis_host = "" +django_redis_password = "" +django_secret_key = "" +django_sentry_dsn = "" +django_slack_bot_token = "" +django_slack_signing_secret = "" +environment = "staging" +force_destroy_bucket = true +project_name = "owasp-nest" diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf new file mode 100644 index 0000000000..77fb8aa747 --- /dev/null +++ b/infrastructure/variables.tf @@ -0,0 +1,267 @@ +variable "aws_region" { + description = "The AWS region to deploy resources in." + type = string + default = "us-east-1" +} + +variable "availability_zones" { + description = "A list of availability zones for the VPC" + type = list(string) + default = ["us-east-1a", "us-east-1b", "us-east-1c"] +} + +variable "db_allocated_storage" { + description = "The allocated storage for the RDS database in GB" + type = number + default = 20 +} + +variable "db_backup_retention_period" { + description = "The number of days to retain backups for" + type = number + default = 7 +} + +variable "db_engine_version" { + description = "The version of the PostgreSQL engine" + type = string + default = "16.10" +} + +variable "db_instance_class" { + description = "The instance class for the RDS database" + type = string + default = "db.t3.micro" +} + +variable "db_name" { + description = "The name of the RDS database" + type = string + default = "owasp_nest" +} + +variable "db_password" { + description = "The password for the RDS database" + type = string + sensitive = true + default = null +} + +variable "db_port" { + description = "The port for the RDS database" + type = number + default = 5432 +} + +variable "db_storage_type" { + description = "The storage type for the RDS database" + type = string + default = "gp3" +} + +variable "db_username" { + description = "The username for the RDS database" + type = string + default = "owasp_nest_db_user" +} + +variable "django_algolia_application_id" { + type = string + description = "Algolia application ID." + default = null +} + +variable "django_algolia_write_api_key" { + type = string + description = "Algolia write API key." + sensitive = true + default = null +} + +variable "django_allowed_hosts" { + type = string + description = "Comma-separated list of allowed hosts for Django." + default = null +} + +variable "django_aws_access_key_id" { + type = string + description = "AWS access key for Django." + sensitive = true + default = null +} + +variable "django_aws_secret_access_key" { + type = string + description = "AWS secret access key for Django." + sensitive = true + default = null +} + +variable "django_configuration" { + type = string + description = "Django Configuration" + default = null +} + +variable "django_db_host" { + type = string + description = "Database host URL." + default = null +} + +variable "django_db_name" { + type = string + description = "Database name." + default = null +} + +variable "django_db_password" { + type = string + description = "Database password." + sensitive = true + default = null +} + +variable "django_db_port" { + type = string + description = "Database port." + default = null +} + +variable "django_db_user" { + type = string + description = "Database user." + default = null +} + +variable "django_open_ai_secret_key" { + type = string + description = "OpenAI secret key." + sensitive = true + default = null +} + +variable "django_redis_host" { + type = string + description = "Redis host URL." + default = null +} + +variable "django_redis_password" { + type = string + description = "Redis password." + sensitive = true + default = null +} + +variable "django_secret_key" { + type = string + description = "Django secret key." + sensitive = true + default = null +} + +variable "django_sentry_dsn" { + type = string + description = "Sentry DSN for error tracking." + sensitive = true + default = null +} + +variable "django_slack_bot_token" { + type = string + description = "Slack bot token." + sensitive = true + default = null +} + +variable "django_slack_signing_secret" { + type = string + description = "Slack signing secret." + sensitive = true + default = null +} + +variable "environment" { + description = "The environment (e.g., staging, production)" + type = string + default = "staging" + validation { + condition = contains(["staging", "production"], var.environment) + error_message = "Environment must be either 'staging' or 'production'." + } +} + +variable "force_destroy_bucket" { + description = "If true, deletes all objects from the bucket when the bucket is destroyed." + type = bool + default = false +} + +variable "fixtures_s3_bucket" { + description = "The name of the S3 bucket for fixtures" + type = string + default = "nest-fixtures" +} + +variable "private_subnet_cidrs" { + description = "A list of CIDR blocks for the private subnets" + type = list(string) + default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "nest" +} + +variable "public_subnet_cidrs" { + description = "A list of CIDR blocks for the public subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "redis_auth_token" { + description = "The auth token for Redis" + type = string + sensitive = true + default = null +} + +variable "redis_engine_version" { + description = "The version of the Redis engine" + type = string + default = "7.0" +} + +variable "redis_node_type" { + description = "The node type for the Redis cache" + type = string + default = "cache.t3.micro" +} + +variable "redis_num_cache_nodes" { + description = "The number of cache nodes in the Redis cluster" + type = number + default = 1 +} + +variable "redis_port" { + description = "The port for the Redis cache" + type = number + default = 6379 +} + +variable "vpc_cidr" { + description = "The CIDR block for the VPC" + type = string + default = "10.0.0.0/16" +} + +variable "zappa_s3_bucket" { + description = "The name of the S3 bucket for Zappa deployments" + type = string + default = "owasp-nest-zappa-deployments" +}