├── .dockerignore ├── .gcloudignore ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── README.md ├── cloudbuild ├── cloud_run.yaml ├── create_superuser.yaml ├── gae_flexible.yaml ├── gae_standard.yaml ├── gae_standard_with_gcs.yaml └── gke.yaml ├── docs ├── app_engine.png └── cloud_run.png ├── gae_flexible.yaml ├── gae_standard.yaml ├── gae_standard_with_gcs.yaml ├── gke.yaml ├── main.py ├── manage.py ├── mysite ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── polls ├── __init__.py ├── admin.py ├── apps.py ├── management │ └── commands │ │ ├── __init__.py │ │ └── create_superuser_from_secrets.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── polls │ │ ├── detail.html │ │ ├── index.html │ │ └── results.html ├── test_polls.py ├── urls.py └── views.py ├── requirements.txt ├── setup.cfg └── terraform ├── envs ├── cloud_run │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── gae_flexible │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── gae_standard │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── gae_standard_with_gcs │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── gke │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── modules ├── django_cloud_run ├── README.md ├── iam.tf ├── main.tf ├── outputs.tf ├── secret.tf ├── sql.tf ├── variables.tf └── versions.tf ├── django_gae_flexible ├── README.md ├── iam.tf ├── main.tf ├── outputs.tf ├── secret.tf ├── sql.tf ├── variables.tf └── versions.tf ├── django_gae_standard ├── README.md ├── iam.tf ├── main.tf ├── outputs.tf ├── secret.tf ├── sql.tf ├── variables.tf └── versions.tf └── django_gke ├── README.md ├── iam.tf ├── main.tf ├── outputs.tf ├── secret.tf ├── sql.tf ├── variables.tf └── versions.tf /.dockerignore: -------------------------------------------------------------------------------- 1 | #!include:.gitignore 2 | ** 3 | 4 | # Allow files and directories 5 | !.dockerignore 6 | !mysite/** 7 | !polls/** 8 | !main.py 9 | !manage.py 10 | !requirements.txt 11 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | #!include:.gitignore 2 | # Ignore everything 3 | * 4 | 5 | ## Except files we want 6 | !.dockerignore 7 | !mysite/** 8 | !polls/** 9 | !Dockerfile 10 | !gae_flexible.yaml 11 | !gae_standard.yaml 12 | !gae_standard_with_gcs.yaml 13 | !main.py 14 | !manage.py 15 | !requirements.txt 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | - uses: actions/setup-python@v2 16 | - name: 'install terraform-docs' 17 | run: > 18 | curl -L 19 | "$(curl -s https://api.github.com/repos/terraform-docs/terraform-docs/releases/latest 20 | | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" 21 | > terraform-docs.tgz 22 | && tar -xzf terraform-docs.tgz terraform-docs 23 | && chmod +x terraform-docs 24 | && sudo mv terraform-docs /usr/bin/ 25 | - uses: pre-commit/action@v2.0.3 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/venv,linux,macos,python,django,windows,virtualenv,jetbrains+all,terraform 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=venv,linux,macos,python,django,windows,virtualenv,jetbrains+all,terraform 4 | 5 | ### Django ### 6 | *.log 7 | *.pot 8 | *.pyc 9 | __pycache__/ 10 | local_settings.py 11 | db.sqlite3 12 | db.sqlite3-journal 13 | media 14 | 15 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 16 | # in your Git repository. Update and uncomment the following line accordingly. 17 | # /staticfiles/ 18 | 19 | ### Django.Python Stack ### 20 | # Byte-compiled / optimized / DLL files 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | *.py,cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | cover/ 71 | 72 | # Translations 73 | *.mo 74 | 75 | # Django stuff: 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | 147 | # pytype static type analyzer 148 | .pytype/ 149 | 150 | # Cython debug symbols 151 | cython_debug/ 152 | 153 | ### JetBrains+all ### 154 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 155 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 156 | 157 | # User-specific stuff 158 | .idea/**/workspace.xml 159 | .idea/**/tasks.xml 160 | .idea/**/usage.statistics.xml 161 | .idea/**/dictionaries 162 | .idea/**/shelf 163 | 164 | # AWS User-specific 165 | .idea/**/aws.xml 166 | 167 | # Generated files 168 | .idea/**/contentModel.xml 169 | 170 | # Sensitive or high-churn files 171 | .idea/**/dataSources/ 172 | .idea/**/dataSources.ids 173 | .idea/**/dataSources.local.xml 174 | .idea/**/sqlDataSources.xml 175 | .idea/**/dynamic.xml 176 | .idea/**/uiDesigner.xml 177 | .idea/**/dbnavigator.xml 178 | 179 | # Gradle 180 | .idea/**/gradle.xml 181 | .idea/**/libraries 182 | 183 | # Gradle and Maven with auto-import 184 | # When using Gradle or Maven with auto-import, you should exclude module files, 185 | # since they will be recreated, and may cause churn. Uncomment if using 186 | # auto-import. 187 | # .idea/artifacts 188 | # .idea/compiler.xml 189 | # .idea/jarRepositories.xml 190 | # .idea/modules.xml 191 | # .idea/*.iml 192 | # .idea/modules 193 | # *.iml 194 | # *.ipr 195 | 196 | # CMake 197 | cmake-build-*/ 198 | 199 | # Mongo Explorer plugin 200 | .idea/**/mongoSettings.xml 201 | 202 | # File-based project format 203 | *.iws 204 | 205 | # IntelliJ 206 | out/ 207 | 208 | # mpeltonen/sbt-idea plugin 209 | .idea_modules/ 210 | 211 | # JIRA plugin 212 | atlassian-ide-plugin.xml 213 | 214 | # Cursive Clojure plugin 215 | .idea/replstate.xml 216 | 217 | # Crashlytics plugin (for Android Studio and IntelliJ) 218 | com_crashlytics_export_strings.xml 219 | crashlytics.properties 220 | crashlytics-build.properties 221 | fabric.properties 222 | 223 | # Editor-based Rest Client 224 | .idea/httpRequests 225 | 226 | # Android studio 3.1+ serialized cache file 227 | .idea/caches/build_file_checksums.ser 228 | 229 | ### JetBrains+all Patch ### 230 | # Ignores the whole .idea folder and all .iml files 231 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 232 | 233 | .idea/ 234 | 235 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 236 | 237 | *.iml 238 | modules.xml 239 | .idea/misc.xml 240 | *.ipr 241 | 242 | # Sonarlint plugin 243 | .idea/sonarlint 244 | 245 | ### Linux ### 246 | *~ 247 | 248 | # temporary files which can be created if a process still has a handle open of a deleted file 249 | .fuse_hidden* 250 | 251 | # KDE directory preferences 252 | .directory 253 | 254 | # Linux trash folder which might appear on any partition or disk 255 | .Trash-* 256 | 257 | # .nfs files are created when an open file is removed but is still being accessed 258 | .nfs* 259 | 260 | ### macOS ### 261 | # General 262 | .DS_Store 263 | .AppleDouble 264 | .LSOverride 265 | 266 | # Icon must end with two \r 267 | Icon 268 | 269 | 270 | # Thumbnails 271 | ._* 272 | 273 | # Files that might appear in the root of a volume 274 | .DocumentRevisions-V100 275 | .fseventsd 276 | .Spotlight-V100 277 | .TemporaryItems 278 | .Trashes 279 | .VolumeIcon.icns 280 | .com.apple.timemachine.donotpresent 281 | 282 | # Directories potentially created on remote AFP share 283 | .AppleDB 284 | .AppleDesktop 285 | Network Trash Folder 286 | Temporary Items 287 | .apdisk 288 | 289 | ### Python ### 290 | # Byte-compiled / optimized / DLL files 291 | 292 | # C extensions 293 | 294 | # Distribution / packaging 295 | 296 | # PyInstaller 297 | # Usually these files are written by a python script from a template 298 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 299 | 300 | # Installer logs 301 | 302 | # Unit test / coverage reports 303 | 304 | # Translations 305 | 306 | # Django stuff: 307 | 308 | # Flask stuff: 309 | 310 | # Scrapy stuff: 311 | 312 | # Sphinx documentation 313 | 314 | # PyBuilder 315 | 316 | # Jupyter Notebook 317 | 318 | # IPython 319 | 320 | # pyenv 321 | # For a library or package, you might want to ignore these files since the code is 322 | # intended to run in multiple environments; otherwise, check them in: 323 | # .python-version 324 | 325 | # pipenv 326 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 327 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 328 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 329 | # install all needed dependencies. 330 | 331 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 332 | 333 | # Celery stuff 334 | 335 | # SageMath parsed files 336 | 337 | # Environments 338 | 339 | # Spyder project settings 340 | 341 | # Rope project settings 342 | 343 | # mkdocs documentation 344 | 345 | # mypy 346 | 347 | # Pyre type checker 348 | 349 | # pytype static type analyzer 350 | 351 | # Cython debug symbols 352 | 353 | ### Terraform ### 354 | # Local .terraform directories 355 | **/.terraform/* 356 | 357 | # .tfstate files 358 | *.tfstate 359 | *.tfstate.* 360 | 361 | # Crash log files 362 | crash.log 363 | 364 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 365 | # password, private keys, and other secrets. These should not be part of version 366 | # control as they are data points which are potentially sensitive and subject 367 | # to change depending on the environment. 368 | # 369 | *.tfvars 370 | 371 | # Ignore override files as they are usually used to override resources locally and so 372 | # are not checked in 373 | override.tf 374 | override.tf.json 375 | *_override.tf 376 | *_override.tf.json 377 | 378 | # Include override files you do wish to add to version control using negated pattern 379 | # !example_override.tf 380 | 381 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 382 | # example: *tfplan* 383 | 384 | # Ignore CLI configuration files 385 | .terraformrc 386 | terraform.rc 387 | 388 | ### venv ### 389 | # Virtualenv 390 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 391 | [Bb]in 392 | [Ii]nclude 393 | [Ll]ib 394 | [Ll]ib64 395 | [Ll]ocal 396 | [Ss]cripts 397 | pyvenv.cfg 398 | pip-selfcheck.json 399 | 400 | ### VirtualEnv ### 401 | # Virtualenv 402 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 403 | 404 | ### Windows ### 405 | # Windows thumbnail cache files 406 | Thumbs.db 407 | Thumbs.db:encryptable 408 | ehthumbs.db 409 | ehthumbs_vista.db 410 | 411 | # Dump file 412 | *.stackdump 413 | 414 | # Folder config file 415 | [Dd]esktop.ini 416 | 417 | # Recycle Bin used on file shares 418 | $RECYCLE.BIN/ 419 | 420 | # Windows Installer files 421 | *.cab 422 | *.msi 423 | *.msix 424 | *.msm 425 | *.msp 426 | 427 | # Windows shortcuts 428 | *.lnk 429 | 430 | # End of https://www.toptal.com/developers/gitignore/api/venv,linux,macos,python,django,windows,virtualenv,jetbrains+all,terraform 431 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs|node_modules|migrations|.git|.tox' 2 | default_stages: [commit] 3 | fail_fast: false 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.0.1 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | args: ['--allow-multiple-documents'] 13 | 14 | - repo: https://github.com/psf/black 15 | rev: 22.3.0 16 | hooks: 17 | - id: black 18 | 19 | - repo: https://github.com/timothycrosley/isort 20 | rev: 5.10.1 21 | hooks: 22 | - id: isort 23 | 24 | - repo: https://gitlab.com/pycqa/flake8 25 | rev: 3.9.2 26 | hooks: 27 | - id: flake8 28 | args: ['--config=setup.cfg'] 29 | additional_dependencies: [flake8-isort] 30 | 31 | - repo: https://github.com/antonbabenko/pre-commit-terraform 32 | rev: v1.62.3 33 | hooks: 34 | - id: terraform_fmt 35 | - id: terraform_docs 36 | 37 | - repo: https://github.com/thlorenz/doctoc.git 38 | rev: v2.1.0 39 | hooks: 40 | - id: doctoc 41 | name: Add TOC for md files 42 | files: \.md$ 43 | args: 44 | - "--maxlevel" 45 | - "4" 46 | - "--title" 47 | - "**Table of Contents**" 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | ENV APP_HOME /app 6 | WORKDIR $APP_HOME 7 | 8 | COPY requirements.txt . 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | COPY . . 12 | 13 | CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 mysite.wsgi:application 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django application on Google Cloud Platform. 2 | 3 | 4 | 5 | **Table of Contents** 6 | 7 | - [Introduction](#introduction) 8 | - [Google Cloud options](#google-cloud-options) 9 | - [App Engine](#app-engine) 10 | - [Cloud Run](#cloud-run) 11 | - [Django app changes specific for Google Cloud Platform](#django-app-changes-specific-for-google-cloud-platform) 12 | - [Prerequisites](#prerequisites) 13 | - [Instruction](#instruction) 14 | - [1. Types of deployments](#1-types-of-deployments) 15 | - [2. Variables specific for your GCP project](#2-variables-specific-for-your-gcp-project) 16 | - [3. Set up infrastructure](#3-set-up-infrastructure) 17 | - [4. Deploy app](#4-deploy-app) 18 | - [5. Destroy infrastructure](#5-destroy-infrastructure) 19 | - [Extra: Create Django superuser](#extra-create-django-superuser) 20 | - [How to run app locally](#how-to-run-app-locally) 21 | - [Warnings!](#warnings) 22 | - [Links](#links) 23 | 24 | 25 | 26 | 27 | ## Introduction 28 | 29 | The Cloud is consistently growing and it may be worth considering for your next Python project. 30 | But Cloud is also very complex and the number of available services is still growing, as well as the number of decisions that have to be made when you want to create configuration for your project. 31 | If you want to learn how to run a simple, basic Django app in the Google Cloud Platform [GCP] and see how easy it can be as well as to get the underlying services in the form of the code (Infrastructure as Code [IaC]), this is a place for you. 32 | 33 | There are many ways to deploy a Django application on the GCP: 34 | 35 | - App Engine (covered here) 36 | - Cloud Run (covered here) 37 | - Kubernetes (TODO) 38 | - Compute Engine (not covered here) 39 | 40 | Most of them are covered in [GCP documentation](https://cloud.google.com/python/django/) which is quite good in my opinion, however if you are not familiar with the Cloud, all these services and operations may seem confusing (and redundant). 41 | Django apps mentioned in these tutorials are almost the same, they have changes dependent on the service type on which they were supposed to be running, nevertheless some changes are not related. 42 | Moreover, I found some of the tutorials and apps to contain tiny bugs. 43 | 44 | So what I have done here is a simple Django application (based on Django project tutorial: [Writing your first Django app](https://docs.djangoproject.com/en/3.2/intro/tutorial01/), where all changes specific to given GCP services are grouped and can be found in the possible fewest number of places for an easy analysis. 45 | Additionally, infrastructure is wrapped in the Terraform (IaC tool) which allows you to easily create (and destroy) all necessary resources. 46 | The process of deploying application itself is not handled by Terraform ([Don’t Deploy Applications with Terraform - Paul Durivage](https://medium.com/google-cloud/dont-deploy-applications-with-terraform-2f4508a45987)), but it is wrapped in the easy to follow Google Cloud Build [GCB] pipelines, 47 | separate for each service. Due to the fact that inheritance between GCB pipelines is not possible, they have a lot in common but analysis of differences between them should not be a problem for you. 48 | For GCB purposes I wrapped the app in Docker, even though the App Engine does not require it, but it was the easiest way to provide a proxy connection to Cloud SQL database ([app-engine-exec-wrapper,](https://github.com/GoogleCloudPlatform/ruby-docker/tree/master/app-engine-exec-wrapper)). 49 | 50 | Hopefully such a condensed project may help you learn how GCP services may be used along with Python projects, so some ideas could be picked up in the future. 51 | 52 | 53 | ## Google Cloud options 54 | 55 | ### App Engine 56 | 57 | > App Engine is a fully managed, serverless platform for developing and hosting web applications at scale. You can choose from several popular languages, libraries, and frameworks to develop your apps, and then let App Engine take care of provisioning servers and scaling your app instances based on demand. 58 | > 59 | > -- [App Engine documentation](https://cloud.google.com/appengine/docs) 60 | 61 | ![App Engine](./docs/app_engine.png) 62 | 63 | This architecture consists of: 64 | 65 | - App Engine - the main service which provides Django app directly to the users 66 | - Secrets - stores sensitive data (secret key, database credentials, bucket name, etc.) 67 | - Cloud SQL - relational database (PostgreSQL) 68 | - Cloud Storage - file storage (static files could optionally be served by App Engine directly) 69 | 70 | ### Cloud Run 71 | 72 | > Cloud Run is a managed compute platform that enables you to run containers that are invocable via requests or events. Cloud Run is serverless: it abstracts away all infrastructure management, so you can focus on what matters most — building great applications. 73 | > 74 | > -- [Cloud Run documentation]() 75 | 76 | ![Cloud RUn](./docs/cloud_run.png) 77 | 78 | This architecture consists of: 79 | 80 | - Cloud Run- the main service which provides Django app directly to the users 81 | - Secrets - stores sensitive data (secret key, database credentials, bucket name, etc.) 82 | - Cloud SQL - relational database (PostgreSQL) 83 | - Cloud Storage - file storage 84 | 85 | ## Django app changes specific for Google Cloud Platform 86 | 87 | All changes to the fresh Django app are located in `mysite/settings.py` file 88 | and are related to three areas: secrets, database and storage. 89 | I will describe each of them. 90 | 91 | 1. Secrets 92 | 93 | As depicted in diagrams from the previous point, the application connects to the Google Secrets service to obtain some sensitive information. 94 | 95 | ```python 96 | import io 97 | import os 98 | from pathlib import Path 99 | 100 | import environ 101 | from google.cloud import secretmanager 102 | 103 | BASE_DIR = Path(__file__).resolve().parent.parent 104 | 105 | 106 | env = environ.Env(DEBUG=(bool, False)) 107 | env_file = os.path.join(BASE_DIR, ".env") 108 | 109 | 110 | if os.path.isfile(env_file): 111 | env.read_env(env_file) 112 | # Pull secrets from Google Secret Manager 113 | elif os.environ.get("GOOGLE_CLOUD_PROJECT", None): 114 | project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") 115 | settings_name = os.environ.get("SETTINGS_NAME", "django_settings") 116 | client = secretmanager.SecretManagerServiceClient() 117 | name = f"projects/{project_id}/secrets/{settings_name}/versions/latest" 118 | payload = client.access_secret_version(name=name).payload.data.decode("UTF-8") 119 | env.read_env(io.StringIO(payload)) 120 | else: 121 | raise Exception("No local .env or GOOGLE_CLOUD_PROJECT detected. No secrets found.") 122 | ``` 123 | 124 | Environmental variables are handled by [`django-environ`](https://django-environ.readthedocs.io/en/latest/) package. 125 | 126 | If the `.env` file exists it is treated as the source of secrets (extends environmental variables). 127 | 128 | Otherwise, if `GOOGLE_CLOUD_PROJECT` environmental variable existence check is positive, then client of Google Secrets 129 | fetches the secret (with default name `django_settings`) and extends environmental variables with values from it. 130 | 131 | If there is no `.env` file and no `GOOGLE_CLOUD_PROJECT` variable, then an exception is raised and the app will not be able to start. 132 | 133 | The secret (mentioned `django_settings` from Google Secrets) consists of three variables: 134 | 135 | - `SECRET_KEY` - which is used to provide cryptographic signing. 136 | - `DATABASE_URL` - database connection information and credentials. 137 | - `GS_BUCKET_NAME` - [Google Cloud Storage](https://cloud.google.com/storage/) bucket name (may be empty for Google App Engine standard environment). 138 | 139 | 2. Database 140 | 141 | ```python 142 | default_sqlite3_database = "sqlite:///" + str(BASE_DIR / "db.sqlite3") 143 | DATABASES = {"default": env.db("DATABASE_URL", default=default_sqlite3_database)} 144 | 145 | # If the flag as been set, configure to use proxy 146 | if os.getenv("USE_CLOUD_SQL_AUTH_PROXY", None): 147 | DATABASES["default"]["HOST"] = "127.0.0.1" 148 | DATABASES["default"]["PORT"] = 5432 149 | ``` 150 | 151 | Database connection string is obtained from the `DATABASE_URL` environment variable. 152 | Otherwise, the default `sqlite3` database is used. 153 | 154 | Then, if the `USE_CLOUD_SQL_AUTH_PROXY` environmental variable exists, 155 | the database connection will be modified to make it work with [Cloud SQL Auth proxy](https://cloud.google.com/sql/docs/postgres/sql-proxy). 156 | 157 | 3. Storage 158 | 159 | ```python 160 | GS_BUCKET_NAME = env("GS_BUCKET_NAME", default=None) 161 | 162 | STATIC_URL = "/static/" 163 | 164 | if GS_BUCKET_NAME: 165 | DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" 166 | STATICFILES_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" 167 | GS_DEFAULT_ACL = "publicRead" 168 | else: 169 | STATIC_ROOT = "static" 170 | STATICFILES_DIRS = [] 171 | ``` 172 | 173 | If `GS_BUCKET_NAME` environment variable exists, then the relevant storage backend will be set with the help of [django-storages](https://github.com/jschneier/django-storages/) package. 174 | 175 | ## Prerequisites 176 | 177 | Topics you should be familiar with since they will be not covered: 178 | 179 | - Python and Django 180 | - Cloud - basic cloud concepts in general 181 | - Google Cloud Platform: basics of the services, use of `gcloud` CLI, managing billing and documentation about [Django on GCP (GCP documentation)](https://cloud.google.com/python/django/) 182 | - Terraform - basic use and concepts. You can also check my tutorial on Medium: [Terraform Tutorial: Introduction to Infrastructure as Code](https://tobiaszkedzierski.medium.com/terraform-tutorial-introduction-to-infrastructure-as-code-dccec643bfdb) 183 | 184 | What you should prepare: 185 | - Google Cloud Project - create a fresh GCP project or use an existing one (but it may cause Terraform exceptions) 186 | - [`gcloud`](https://cloud.google.com/sdk/gcloud) - install GCP cli and authorize it with a relevant GCP Project 187 | - [Terraform](https://www.terraform.io/downloads.html) - install the latest version 188 | - Python [optionally] - Python 3.9 in virtual environment if you want to run Django app locally 189 | 190 | 191 | ## Instruction 192 | 193 | Most of the terminal commands stated here are executed within the Terraform environment folder relevant to the chosen solution. 194 | 195 | ### 1. Types of deployments 196 | 197 | | Terraform environment folder | Description | GCS | `CLOUD_BUILD_FILE` variable | Config file | Used Terraform module | 198 | |----------------------------------------|---------------------------------------------------------|-----|-----------------------------------------|------------------------------|-------------------------------------------------------------------------------------| 199 | | `terraform/envs/gae_standard` | App Engine Standard environment **without** GCS storage | ❌ | `cloudbuild/gae_standard.yaml` | `gae_standard.yaml` | [`terraform/modules/django_gae_standard`](./terraform/modules/django_gae_standard) | 200 | | `terraform/envs/gae_standard_with_gcs` | App Engine Standard environment | ✅ | `cloudbuild/gae_standard_with_gcs.yaml` | `gae_standard_with_gcs.yaml` | [`terraform/modules/django_gae_standard`](./terraform/modules/django_gae_standard) | 201 | | `terraform/envs/gae_flexible` | App Engine Flexible environment | ✅ | `cloudbuild/gae_flexible.yaml` | `gae_flexible.yaml` | [`terraform/modules/django_gae_flexible`](./terraform/modules/django_gae_flexible) | 202 | | `terraform/envs/cloud_run` | Cloud Run | ✅ | `cloudbuild/cloud_run.yaml` | - | [`terraform/modules/django_cloud_run`](./terraform/modules/django_cloud_run) | 203 | 204 | ### 2. Variables specific for your GCP project 205 | 206 | 1. Shell environmental variables 207 | 208 | Set environmental variables. 209 | Some values you have to know beforehand, like `PROJECT_ID`. 210 | Others you can generate on the fly, like `DJANGO_SECRET_KEY` however, remember to keep them somewhere (see the next step). 211 | They will be used to provide input variables for Terraform and for `gcloud` commands. 212 | 213 | ```bash 214 | export PROJECT_ID=django-cloud-tf-test-001 215 | export REGION=europe-central2 216 | export ZONE=europe-central2-a 217 | export SQL_DATABASE_INSTANCE_NAME="${PROJECT_ID}-db-instance" 218 | export SQL_DATABASE_NAME="${PROJECT_ID}-db" 219 | export SERVICE_NAME=polls-service 220 | export SERVICE_ACCOUNT_NAME=polls-service-account # for cloud run 221 | export SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" # for cloud run 222 | 223 | export DJANGO_SECRET_KEY=$(cat /dev/urandom | LC_ALL=C tr -dc '[:alpha:]'| fold -w 50 | head -n1) 224 | export SQL_USER=$(cat /dev/urandom | LC_ALL=C tr -dc '[:alpha:]'| fold -w 10 | head -n1) 225 | export SQL_PASSWORD=$(cat /dev/urandom | LC_ALL=C tr -dc '[:alpha:]'| fold -w 10 | head -n1) 226 | ``` 227 | 228 | 2. `gcloud` project configuration 229 | 230 | ```bash 231 | gcloud config set project $PROJECT_ID 232 | ``` 233 | 234 | 3. Terraform variables 235 | 236 | Terraform can read variables from environment variables. 237 | Naming convention is `TF_VAR_variable_name`. 238 | 239 | ```bash 240 | export TF_VAR_project_id=$PROJECT_ID 241 | export TF_VAR_region=$REGION 242 | export TF_VAR_zone=$ZONE 243 | export TF_VAR_django_secret_key=$DJANGO_SECRET_KEY 244 | export TF_VAR_sql_database_instance_name=$SQL_DATABASE_INSTANCE_NAME 245 | export TF_VAR_sql_database_name=$SQL_DATABASE_NAME 246 | export TF_VAR_sql_user=$SQL_USER 247 | export TF_VAR_sql_password=$SQL_PASSWORD 248 | export TF_VAR_cloud_run_service_account_name=$SERVICE_ACCOUNT_NAME 249 | ``` 250 | 251 | Another way to provide input variables is the '.tfvars' file. 252 | With variables set in the previous step, such file could be generated with the following command: 253 | 254 | ```bash 255 | cat << EOF > terraform.tfvars 256 | project_id = "$PROJECT_ID" 257 | region = "$REGION" 258 | zone = "$ZONE" 259 | django_secret_key = "$DJANGO_SECRET_KEY" 260 | sql_database_instance_name = "$SQL_DATABASE_INSTANCE_NAME" 261 | sql_database_name = "$SQL_DATABASE_NAME" 262 | sql_user = "$SQL_USER" 263 | sql_password = "$SQL_PASSWORD" 264 | cloud_run_service_account_name = "$SERVICE_ACCOUNT_NAME" 265 | EOF 266 | ``` 267 | 268 | More about variables: [Input Variables](https://www.terraform.io/language/values/variables) and [Variable Definition Precedence](https://www.terraform.io/language/values/variables#variable-definition-precedence) 269 | 270 | ### 3. Set up infrastructure 271 | 272 | Set up infrastructure with basic Terraform commands: 273 | 274 | ```bash 275 | terraform init 276 | terraform plan 277 | terraform apply 278 | ``` 279 | 280 | Known issues: 281 | 282 | - [No way to delete an application](https://issuetracker.google.com/issues/35874988) - 13 years old Google issue: 283 | 284 | ```Error: Error creating App Engine application: googleapi: Error 409: This application already exists and cannot be re-created., alreadyExist``` 285 | 286 | After destroying infrastructure and recreating it again this error occurs since the App Engine app was (silently) not deleted as intended. 287 | It could be fixed by importing the existing App Engine app into Terraform state: 288 | 289 | ```shell 290 | terraform import module.$TERRAFORM_MODULE_NAME.google_app_engine_application.app "${PROJECT_ID}" 291 | ``` 292 | 293 | Replace `$TERRAFORM_MODULE_NAME` with the relevant module name (see `main.tf` in env of your choice). 294 | 295 | Example for `django_gae_standard`: 296 | 297 | ```shell 298 | terraform import module.django_gae_standard.google_app_engine_application.app "${PROJECT_ID}" 299 | ``` 300 | 301 | More about importing: [Terraform - import](https://www.terraform.io/docs/cli/import/index.html) 302 | 303 | - random null occurrence for `data.google_project.project.number` ([my comment on `hashicorp/terraform-provider-google - data.google_project.project.project_id sometimes null` issue](https://github.com/hashicorp/terraform-provider-google/issues/10587#issuecomment-984589651)): 304 | 305 | ```The expression result is null. Cannot include a null value in a string template.``` 306 | 307 | The only solution here is to retry until it works and wait until the new version of the `hashicorp/terraform-provider-google` has it eliminated. 308 | 309 | ### 4. Deploy app 310 | 311 | I share the opinion that Terraform should only be used to provide the infrastructure and the 312 | deployment of the application itself should be handled separately 313 | (see [Don’t Deploy Applications with Terraform - Paul Durivage](https://medium.com/google-cloud/dont-deploy-applications-with-terraform-2f4508a45987)). 314 | 315 | GCB pipelines handle the operation of deploying and/or updating the application. 316 | 317 | 1. Set GCB pipeline relevant to the chosen deployment 318 | 319 | Substitute the path with the value taken from the [Types of deployments](#1-types-of-deployments) table 320 | 321 | ```bash 322 | export CLOUD_BUILD_FILE= 323 | ``` 324 | 325 | 2. Deploy 326 | 327 | Commands should be run from the root repository directory (see `CLOUD_BUILD_FILE` variable). 328 | 329 | - App Engine 330 | 331 | Run GCB pipeline: 332 | 333 | ```bash 334 | gcloud builds submit \ 335 | --project $PROJECT_ID \ 336 | --config $CLOUD_BUILD_FILE \ 337 | --substitutions _INSTANCE_NAME=$SQL_DATABASE_INSTANCE_NAME,_REGION=$REGION,_SERVICE_NAME=$SERVICE_NAME 338 | ``` 339 | 340 | Display GAE application url: 341 | 342 | ```bash 343 | gcloud app describe --format "value(defaultHostname)" 344 | ``` 345 | 346 | Known issues: 347 | 348 | - ambiguous error occurrence during the last step, a Terraform Google provider issue, wait a while and retry 349 | 350 | ```bash 351 | Step #5 - "deploy app": ERROR: (gcloud.app.deploy) NOT_FOUND: Unable to retrieve P4SA: [service-123456789101@gcp-gae-service.iam.gserviceaccount.com] from GAIA. Could be GAIA propagation delay or request from deleted apps. 352 | Finished Step #5 - "deploy app" 353 | ``` 354 | 355 | - Cloud Run 356 | 357 | Run GCB pipeline: 358 | 359 | ```bash 360 | gcloud builds submit \ 361 | --project $PROJECT_ID \ 362 | --config $CLOUD_BUILD_FILE \ 363 | --substitutions _INSTANCE_NAME=$SQL_DATABASE_INSTANCE_NAME,_REGION=$REGION,_SERVICE_NAME=$SERVICE_NAME,_SERVICE_ACCOUNT_NAME=$SERVICE_ACCOUNT_NAME 364 | ``` 365 | 366 | Display Cloud Run application url: 367 | 368 | ```bash 369 | gcloud run services list --filter SERVICE:$SERVICE_NAME --format "value(status.address.url)" 370 | ``` 371 | 372 | ### 5. Destroy infrastructure 373 | 374 | ```shell 375 | terraform destroy 376 | ``` 377 | 378 | ### Extra: Create Django superuser 379 | 380 | Superuser credentials are intended to be stored as Google Secret. 381 | Default name for the secret is `superuser_credentials`. 382 | These credentials are used by `cloudbuild/create_superuser.yaml` GCB pipeline for superuser creation. 383 | 384 | An easy way to quickly create and destroy Google Secrets is to use the `gcloud` cli. 385 | Optionally, resources could be created in TF as well, however if superuser credentials are needed only once, 386 | it does not seem to be the best idea to store it as IaC. 387 | 388 | 1. Create the secret: 389 | 390 | ```shell 391 | export SECRET_NAME="superuser_credentials" 392 | export SUPERUSER_USERNAME="super_username" 393 | export SUPERUSER_PASSWORD="super_password" 394 | 395 | gcloud secrets create $SECRET_NAME --replication-policy="automatic" 396 | echo -n "USERNAME=${SUPERUSER_USERNAME}\nPASSWORD=${SUPERUSER_PASSWORD}\n" | \ 397 | gcloud secrets versions add $SECRET_NAME --data-file=- 398 | ``` 399 | 400 | Optionally, you can read the secret to verify if it is correct: 401 | 402 | ```shell 403 | gcloud secrets versions access latest --secret=$SECRET_NAME 404 | ``` 405 | 406 | 2. Run GCB pipeline which creates Django superuser: 407 | 408 | ```shell 409 | gcloud builds submit \ 410 | --project $PROJECT_ID \ 411 | --config cloudbuild/create_superuser.yaml \ 412 | --substitutions _INSTANCE_NAME=$SQL_DATABASE_INSTANCE_NAME,_REGION=$REGION,_SERVICE_NAME=$SERVICE_NAME 413 | ``` 414 | 415 | 3. Delete the secret: 416 | 417 | ```shell 418 | gcloud secrets delete $SECRET_NAME 419 | ``` 420 | 421 | Optionally create Secret as a Terraform resource (credentials provided as variables): 422 | 423 | ```hcl 424 | variable "django_superuser_username" { 425 | description = "Django superuser username" 426 | type = string 427 | } 428 | 429 | variable "django_superuser_password" { 430 | description = "Django superuser password" 431 | type = string 432 | } 433 | 434 | resource "google_secret_manager_secret" "superuser_credentials" { 435 | secret_id = var.django_settings_name 436 | depends_on = [google_project_service.gcp_services] 437 | labels = { 438 | label = "superuser_credentials" 439 | } 440 | 441 | replication { 442 | automatic = true 443 | } 444 | } 445 | 446 | resource "google_secret_manager_secret_version" "superuser_credentials_version" { 447 | secret = google_secret_manager_secret.django_settings.id 448 | 449 | secret_data = <<-EOF 450 | USERNAME=${var.django_superuser_username} 451 | PASSWORD=${var.django_superuser_password} 452 | EOF 453 | 454 | depends_on = [module.django_cloud_run] # relevant module for your case 455 | } 456 | ``` 457 | 458 | ### How to run app locally 459 | 460 | Instruction on how to run this app locally with or without connection to the cloud services. 461 | If you want to run the app with connection to the cloud services you have to set up the infrastructure first (steps 1-3 in [Instructions](#instructions)). 462 | 463 | 1. Create Python virtual environment and install dependencies: 464 | 465 | ```shell 466 | python -m venv venv 467 | source venv/bin/activate 468 | pip install -r requirements.txt 469 | ``` 470 | 471 | 1. Set up connection to Google Cloud services (if you need them): 472 | 473 | - Authenticate to GCP: 474 | 475 | ```shell 476 | gcloud auth application-default login 477 | ``` 478 | 479 | - Install [Cloud SQL Auth proxy](https://cloud.google.com/sql/docs/postgres/sql-proxy): 480 | 481 | ```shell 482 | # Linux 64-bit 483 | wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy 484 | # MacOS 64-bit 485 | curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64 486 | 487 | chmod +x cloud_sql_proxy 488 | ``` 489 | 490 | - Export environment variables: 491 | 492 | ```shell 493 | export GOOGLE_CLOUD_PROJECT=$PROJECT_ID 494 | export USE_CLOUD_SQL_AUTH_PROXY=true 495 | ``` 496 | 497 | 2. Create `.env` file with secrets: 498 | 499 | - without connection to cloud services: 500 | 501 | ```shell 502 | echo "DEBUG=True" > .env 503 | ``` 504 | 505 | - with connection to cloud services (values fetched from Google Secrets): 506 | 507 | ```shell 508 | echo "DEBUG=True" > .env 509 | 510 | # The output will be formatted as UTF-8 which can corrupt binary secrets. 511 | # To get the raw bytes, have Cloud SDK print the response as base64-encoded and decode 512 | gcloud secrets versions access latest --secret=django_settings --format='get(payload.data)' \ 513 | | tr '_-' '/+' | base64 -d >> .env 514 | ``` 515 | 516 | 1. Run the Django migrations to set up your models and assets: 517 | 518 | ```shell 519 | python manage.py makemigrations 520 | python manage.py makemigrations polls 521 | python manage.py migrate 522 | python manage.py collectstatic 523 | ``` 524 | 525 | 1. Start the Django web server: 526 | 527 | ```shell 528 | python manage.py runserver 529 | ``` 530 | 531 | ## Warnings! 532 | 533 | 1. Remember to edit `.gcloudignore` and `.dockerignore`. It excludes all files except implicitly added. 534 | 2. Running services and operations costs real **MONEY**. Make sure that you do not leave any resources that consume credits unintentionally. 535 | 3. Examples here are not production-ready and do not provide a sufficient level of security. If you want to run it within your organization, consult it with the person responsible for Cloud (e.g. Cloud Security Officer). 536 | 537 | ## Links 538 | 539 | - [ Python on Google Cloud](https://cloud.google.com/python) 540 | - [Running Django on the App Engine standard environment](https://cloud.google.com/python/django/appengine) 541 | - [Running Django on the App Engine flexible environment](https://cloud.google.com/python/django/flexible-environment) 542 | - [Running Django on the Cloud Run environment ](https://cloud.google.com/python/django/run) 543 | -------------------------------------------------------------------------------- /cloudbuild/cloud_run.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: "build image" 3 | name: "gcr.io/cloud-builders/docker" 4 | args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] 5 | 6 | - id: "push image" 7 | name: "gcr.io/cloud-builders/docker" 8 | args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] 9 | 10 | - id: "apply migrations" 11 | name: "gcr.io/google-appengine/exec-wrapper" 12 | args: 13 | [ 14 | "-i", 15 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 16 | "-s", 17 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 18 | "-e", 19 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 20 | "-e", 21 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 22 | "--", 23 | "python", 24 | "manage.py", 25 | "migrate", 26 | ] 27 | 28 | - id: "collect static files" 29 | name: "gcr.io/google-appengine/exec-wrapper" 30 | args: 31 | [ 32 | "-i", 33 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 34 | "-s", 35 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 36 | "-e", 37 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 38 | "-e", 39 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 40 | "--", 41 | "python", 42 | "manage.py", 43 | "collectstatic", 44 | "--verbosity", 45 | "2", 46 | "--noinput" 47 | ] 48 | 49 | - id: "deploy app" 50 | name: "gcr.io/cloud-builders/gcloud" 51 | args: 52 | [ 53 | "run", "deploy", 54 | "${_SERVICE_NAME}", 55 | "--platform", 56 | "managed", 57 | "--region", 58 | "${_REGION}", 59 | "--image", 60 | "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", 61 | "--set-cloudsql-instances", 62 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 63 | "--service-account", 64 | "${_SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com", 65 | "--update-env-vars=GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 66 | "--allow-unauthenticated" 67 | ] 68 | 69 | - id: "get app url" 70 | name: "gcr.io/cloud-builders/gcloud" 71 | args: ["run", "services", "list", "--filter", "SERVICE:${_SERVICE_NAME}", "--format", "value(status.address.url)"] 72 | 73 | substitutions: 74 | _INSTANCE_NAME: db-instance 75 | _REGION: us-central1 76 | _SERVICE_NAME: polls-service 77 | _SECRET_SETTINGS_NAME: django_settings 78 | _SERVICE_ACCOUNT_NAME: polls-service-account 79 | 80 | images: 81 | - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" 82 | -------------------------------------------------------------------------------- /cloudbuild/create_superuser.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: "build image" 3 | name: "gcr.io/cloud-builders/docker" 4 | args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] 5 | 6 | - id: "push image" 7 | name: "gcr.io/cloud-builders/docker" 8 | args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] 9 | 10 | - id: "create superuser" 11 | name: "gcr.io/google-appengine/exec-wrapper" 12 | args: 13 | [ 14 | "-i", 15 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 16 | "-s", 17 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 18 | "-e", 19 | "SUPERUSER_CREDENTIALS=${_SUPERUSER_CREDENTIALS}", 20 | "-e", 21 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 22 | "--", 23 | "python", 24 | "manage.py", 25 | "create_superuser_from_secrets" 26 | ] 27 | 28 | substitutions: 29 | _INSTANCE_NAME: db-instance 30 | _REGION: us-central1 31 | _SERVICE_NAME: polls-service 32 | _SUPERUSER_CREDENTIALS: superuser_credentials 33 | 34 | images: 35 | - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" 36 | -------------------------------------------------------------------------------- /cloudbuild/gae_flexible.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: "build image" 3 | name: "gcr.io/cloud-builders/docker" 4 | args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] 5 | 6 | - id: "push image" 7 | name: "gcr.io/cloud-builders/docker" 8 | args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] 9 | 10 | - id: "apply migrations" 11 | name: "gcr.io/google-appengine/exec-wrapper" 12 | args: 13 | [ 14 | "-i", 15 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 16 | "-s", 17 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 18 | "-e", 19 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 20 | "-e", 21 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 22 | "--", 23 | "python", 24 | "manage.py", 25 | "migrate", 26 | ] 27 | 28 | - id: "collect static files" 29 | name: "gcr.io/google-appengine/exec-wrapper" 30 | args: 31 | [ 32 | "-i", 33 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 34 | "-s", 35 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 36 | "-e", 37 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 38 | "-e", 39 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 40 | "--", 41 | "python", 42 | "manage.py", 43 | "collectstatic", 44 | "--verbosity", 45 | "2", 46 | "--noinput" 47 | ] 48 | 49 | - id: "modify configuration file" 50 | name: "gcr.io/cloud-builders/gcloud" 51 | entrypoint: 'bash' 52 | args: 53 | - "-c" 54 | - | 55 | sed -i "s/PROJECT_ID/$PROJECT_ID/g" ${_GAE_FILE} 56 | sed -i "s/REGION/${_REGION}/g" ${_GAE_FILE} 57 | sed -i "s/INSTANCE_NAME/${_INSTANCE_NAME}/g" ${_GAE_FILE} 58 | cat ${_GAE_FILE} 59 | 60 | # Removing Dockerfile to avoid error: 61 | # ERROR: (gcloud.app.deploy) There is a Dockerfile in the current directory, and the runtime field in 62 | # /workspace/gae_flexible.yaml is currently set to [runtime: python]. 63 | # To use your Dockerfile to build a custom runtime, set the runtime field to [runtime: custom]. 64 | # To continue using the [python] runtime, please remove the Dockerfile from this directory. 65 | # 66 | # more info about custom runtimes at: https://cloud.google.com/appengine/docs/flexible/custom-runtimes 67 | - id: "delete Dockerfile" 68 | name: "gcr.io/cloud-builders/gcloud" 69 | entrypoint: 'bash' 70 | args: ["-c", "rm Dockerfile"] 71 | 72 | - id: "deploy app" 73 | name: "gcr.io/cloud-builders/gcloud" 74 | args: ["app", "deploy", "${_GAE_FILE}", "--quiet"] 75 | 76 | - id: "get app url" 77 | name: "gcr.io/cloud-builders/gcloud" 78 | args: ["app", "describe", "--format", "value(defaultHostname)"] 79 | 80 | substitutions: 81 | _GAE_FILE: gae_flexible.yaml 82 | _INSTANCE_NAME: db-instance 83 | _REGION: us-central1 84 | _SERVICE_NAME: polls-service 85 | _SECRET_SETTINGS_NAME: django_settings 86 | 87 | images: 88 | - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" 89 | -------------------------------------------------------------------------------- /cloudbuild/gae_standard.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: "build image" 3 | name: "gcr.io/cloud-builders/docker" 4 | args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] 5 | 6 | - id: "push image" 7 | name: "gcr.io/cloud-builders/docker" 8 | args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] 9 | 10 | - id: "apply migrations" 11 | name: "gcr.io/google-appengine/exec-wrapper" 12 | args: 13 | [ 14 | "-i", 15 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 16 | "-s", 17 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 18 | "-e", 19 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 20 | "-e", 21 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 22 | "--", 23 | "python", 24 | "manage.py", 25 | "migrate", 26 | ] 27 | 28 | - id: "install requirements" 29 | name: "python:3.9" 30 | entrypoint: pip 31 | args: ["install", "-r", "requirements.txt", "--user"] 32 | 33 | - id: "collect static files" 34 | name: "python:3.9" 35 | entrypoint: python 36 | args: ["manage.py", "collectstatic", "--verbosity", "2", "--noinput"] 37 | env: 38 | - "GOOGLE_CLOUD_PROJECT=$PROJECT_ID" 39 | 40 | - id: "deploy app" 41 | name: "gcr.io/cloud-builders/gcloud" 42 | args: ["app", "deploy", "${_GAE_FILE}", "--quiet"] 43 | 44 | - id: "get app url" 45 | name: "gcr.io/cloud-builders/gcloud" 46 | args: ["app", "describe", "--format", "value(defaultHostname)"] 47 | 48 | substitutions: 49 | _GAE_FILE: gae_standard.yaml 50 | _INSTANCE_NAME: db-instance 51 | _REGION: us-central1 52 | _SERVICE_NAME: polls-service 53 | _SECRET_SETTINGS_NAME: django_settings 54 | 55 | images: 56 | - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" 57 | -------------------------------------------------------------------------------- /cloudbuild/gae_standard_with_gcs.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: "build image" 3 | name: "gcr.io/cloud-builders/docker" 4 | args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] 5 | 6 | - id: "push image" 7 | name: "gcr.io/cloud-builders/docker" 8 | args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] 9 | 10 | - id: "apply migrations" 11 | name: "gcr.io/google-appengine/exec-wrapper" 12 | args: 13 | [ 14 | "-i", 15 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 16 | "-s", 17 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 18 | "-e", 19 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 20 | "-e", 21 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 22 | "--", 23 | "python", 24 | "manage.py", 25 | "migrate", 26 | ] 27 | 28 | - id: "collect static files" 29 | name: "gcr.io/google-appengine/exec-wrapper" 30 | args: 31 | [ 32 | "-i", 33 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 34 | "-s", 35 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 36 | "-e", 37 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 38 | "-e", 39 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 40 | "--", 41 | "python", 42 | "manage.py", 43 | "collectstatic", 44 | "--verbosity", 45 | "2", 46 | "--noinput" 47 | ] 48 | 49 | - id: "deploy app" 50 | name: "gcr.io/cloud-builders/gcloud" 51 | args: ["app", "deploy", "${_GAE_FILE}", "--quiet"] 52 | 53 | - id: "get app url" 54 | name: "gcr.io/cloud-builders/gcloud" 55 | args: ["app", "describe", "--format", "value(defaultHostname)"] 56 | 57 | substitutions: 58 | _GAE_FILE: gae_standard_with_gcs.yaml 59 | _INSTANCE_NAME: db-instance 60 | _REGION: us-central1 61 | _SERVICE_NAME: polls-service 62 | _SECRET_SETTINGS_NAME: django_settings 63 | 64 | images: 65 | - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" 66 | -------------------------------------------------------------------------------- /cloudbuild/gke.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: "build image" 3 | name: "gcr.io/cloud-builders/docker" 4 | args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] 5 | 6 | - id: "push image" 7 | name: "gcr.io/cloud-builders/docker" 8 | args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] 9 | 10 | - id: "apply migrations" 11 | name: "gcr.io/google-appengine/exec-wrapper" 12 | args: 13 | [ 14 | "-i", 15 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 16 | "-s", 17 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 18 | "-e", 19 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 20 | "-e", 21 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 22 | "--", 23 | "python", 24 | "manage.py", 25 | "migrate", 26 | ] 27 | 28 | - id: "collect static files" 29 | name: "gcr.io/google-appengine/exec-wrapper" 30 | args: 31 | [ 32 | "-i", 33 | "gcr.io/$PROJECT_ID/${_SERVICE_NAME}", 34 | "-s", 35 | "${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}", 36 | "-e", 37 | "SETTINGS_NAME=${_SECRET_SETTINGS_NAME}", 38 | "-e", 39 | "GOOGLE_CLOUD_PROJECT=$PROJECT_ID", 40 | "--", 41 | "python", 42 | "manage.py", 43 | "collectstatic", 44 | "--verbosity", 45 | "2", 46 | "--noinput" 47 | ] 48 | 49 | - id: "deploy app" 50 | name: "gcr.io/cloud-builders/gcloud" 51 | args: 52 | [ 53 | # TODO 54 | "run", 55 | ] 56 | 57 | substitutions: 58 | _INSTANCE_NAME: db-instance 59 | _REGION: us-central1 60 | _SERVICE_NAME: polls-service 61 | _SECRET_SETTINGS_NAME: django_settings 62 | _SERVICE_ACCOUNT_NAME: polls-service-account 63 | 64 | images: 65 | - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" 66 | -------------------------------------------------------------------------------- /docs/app_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TobKed/django_on_gcp/4e25321d856f09a15a58f54cc273abf1e879126d/docs/app_engine.png -------------------------------------------------------------------------------- /docs/cloud_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TobKed/django_on_gcp/4e25321d856f09a15a58f54cc273abf1e879126d/docs/cloud_run.png -------------------------------------------------------------------------------- /gae_flexible.yaml: -------------------------------------------------------------------------------- 1 | runtime: python 2 | env: flex 3 | entrypoint: gunicorn -b :$PORT mysite.wsgi 4 | 5 | beta_settings: 6 | cloud_sql_instances: PROJECT_ID:REGION:INSTANCE_NAME 7 | 8 | runtime_config: 9 | python_version: 3 10 | -------------------------------------------------------------------------------- /gae_standard.yaml: -------------------------------------------------------------------------------- 1 | runtime: python39 2 | 3 | handlers: 4 | # This configures Google App Engine to serve the files in the app's static 5 | # directory. 6 | - url: /static 7 | static_dir: static/ 8 | 9 | # This handler routes all requests not caught above to your main app. It is 10 | # required when static routes are defined, but can be omitted (along with 11 | # the entire handlers section) when there are no static files defined. 12 | - url: /.* 13 | script: auto 14 | -------------------------------------------------------------------------------- /gae_standard_with_gcs.yaml: -------------------------------------------------------------------------------- 1 | runtime: python39 2 | -------------------------------------------------------------------------------- /gke.yaml: -------------------------------------------------------------------------------- 1 | # TODO sed replace SERVICE_NAME 2 | 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: SERVICE_NAME 7 | labels: 8 | app: SERVICE_NAME 9 | spec: 10 | replicas: 3 11 | selector: 12 | matchLabels: 13 | app: SERVICE_NAME 14 | template: 15 | metadata: 16 | labels: 17 | app: SERVICE_NAME 18 | spec: 19 | containers: 20 | - name: SERVICE_NAME-app 21 | # TODO sed replace PROJECT_ID 22 | image: gcr.io/PROIECT_ID/SERVICE_NAME 23 | imagePullPolicy: Always 24 | env: 25 | - name: USE_CLOUD_SQL_AUTH_PROXY 26 | value: "true" 27 | - name: DATABASE_NAME 28 | valueFrom: 29 | secretKeyRef: 30 | name: cloudsql 31 | key: database 32 | - name: DATABASE_USER 33 | valueFrom: 34 | secretKeyRef: 35 | name: cloudsql 36 | key: username 37 | - name: DATABASE_PASSWORD 38 | valueFrom: 39 | secretKeyRef: 40 | name: cloudsql 41 | key: password 42 | ports: 43 | - containerPort: 8080 44 | - name: cloudsql-proxy 45 | image: gcr.io/cloudsql-docker/gce-proxy:1.16 46 | # TODO sed replace CLOUDSQL_CONNECTION_STRING 47 | command: [ 48 | "/cloud_sql_proxy", 49 | "--dir=/cloudsql", 50 | "-instances=CLOUDSQL_CONNECTION_STRING=tcp:5432", 51 | "-credential_file=/secrets/cloudsql/credentials.json" 52 | ] 53 | volumeMounts: 54 | - name: cloudsql-oauth-credentials 55 | mountPath: /secrets/cloudsql 56 | readOnly: true 57 | - name: ssl-certs 58 | mountPath: /etc/ssl/certs 59 | - name: cloudsql 60 | mountPath: /cloudsql 61 | 62 | volumes: 63 | - name: cloudsql-oauth-credentials 64 | secret: 65 | secretName: cloudsql-oauth-credentials 66 | - name: ssl-certs 67 | hostPath: 68 | path: /etc/ssl/certs 69 | - name: cloudsql 70 | emptyDir: {} 71 | 72 | --- 73 | 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | name: SERVICE_NAME 78 | labels: 79 | app: SERVICE_NAME 80 | spec: 81 | type: LoadBalancer 82 | ports: 83 | - port: 80 84 | targetPort: 8080 85 | selector: 86 | app: SERVICE_NAME 87 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from mysite.wsgi import application 2 | 3 | # App Engine by default looks for a main.py file at the root of the app 4 | # directory with a WSGI-compatible object called app. 5 | # This file imports the WSGI-compatible object of your Django app, 6 | # application from mysite/wsgi.py and renames it app so it is discoverable by 7 | # App Engine without additional configuration. 8 | # Alternatively, you can add a custom entrypoint field in your app.yaml: 9 | # entrypoint: gunicorn -b :$PORT mysite.wsgi 10 | app = application 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TobKed/django_on_gcp/4e25321d856f09a15a58f54cc273abf1e879126d/mysite/__init__.py -------------------------------------------------------------------------------- /mysite/settings.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | from pathlib import Path 4 | 5 | import environ 6 | from google.cloud import secretmanager 7 | 8 | BASE_DIR = Path(__file__).resolve().parent.parent 9 | 10 | 11 | env = environ.Env(DEBUG=(bool, False)) 12 | env_file = os.path.join(BASE_DIR, ".env") 13 | 14 | if os.path.isfile(env_file): 15 | env.read_env(env_file) 16 | # Pull secrets from Google Secret Manager 17 | elif os.environ.get("GOOGLE_CLOUD_PROJECT", None): 18 | project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") 19 | settings_name = os.environ.get("SETTINGS_NAME", "django_settings") 20 | client = secretmanager.SecretManagerServiceClient() 21 | name = f"projects/{project_id}/secrets/{settings_name}/versions/latest" 22 | payload = client.access_secret_version(name=name).payload.data.decode("UTF-8") 23 | env.read_env(io.StringIO(payload)) 24 | else: 25 | raise Exception("No local .env or GOOGLE_CLOUD_PROJECT detected. No secrets found.") 26 | 27 | # Quick-start development settings - unsuitable for production 28 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 29 | 30 | # SECURITY WARNING: keep the secret key used in production secret! 31 | SECRET_KEY = env("SECRET_KEY") 32 | 33 | # SECURITY WARNING: don't run with debug turned on in production! 34 | DEBUG = env.bool("DEBUG", False) 35 | 36 | # TODO: this should be changed for production 37 | ALLOWED_HOSTS = ["*"] 38 | 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = [ 43 | "polls.apps.PollsConfig", 44 | "django.contrib.admin", 45 | "django.contrib.auth", 46 | "django.contrib.contenttypes", 47 | "django.contrib.sessions", 48 | "django.contrib.messages", 49 | "django.contrib.staticfiles", 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | "django.middleware.security.SecurityMiddleware", 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | ] 61 | 62 | ROOT_URLCONF = "mysite.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = "mysite.wsgi.application" 81 | 82 | 83 | # DatabaseŃ 84 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 85 | 86 | default_sqlite3_database = "sqlite:///" + str(BASE_DIR / "db.sqlite3") 87 | DATABASES = {"default": env.db("DATABASE_URL", default=default_sqlite3_database)} 88 | 89 | # If the flag as been set, configure to use proxy 90 | if os.getenv("USE_CLOUD_SQL_AUTH_PROXY", None): 91 | DATABASES["default"]["HOST"] = "127.0.0.1" 92 | DATABASES["default"]["PORT"] = 5432 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 104 | }, 105 | { 106 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 107 | }, 108 | { 109 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 116 | 117 | LANGUAGE_CODE = "en-us" 118 | TIME_ZONE = "UTC" 119 | USE_I18N = True 120 | USE_L10N = True 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 126 | 127 | GS_BUCKET_NAME = env("GS_BUCKET_NAME", default=None) 128 | 129 | STATIC_URL = "/static/" 130 | 131 | if GS_BUCKET_NAME: 132 | DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" 133 | STATICFILES_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" 134 | GS_DEFAULT_ACL = "publicRead" 135 | else: 136 | STATIC_ROOT = "static" 137 | STATICFILES_DIRS = [] 138 | 139 | # Default primary key field type 140 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 141 | 142 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 143 | -------------------------------------------------------------------------------- /mysite/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | 6 | urlpatterns = [ 7 | path("", include("polls.urls")), 8 | path("admin/", admin.site.urls), 9 | ] 10 | 11 | if not settings.GS_BUCKET_NAME: 12 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 13 | -------------------------------------------------------------------------------- /mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /polls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TobKed/django_on_gcp/4e25321d856f09a15a58f54cc273abf1e879126d/polls/__init__.py -------------------------------------------------------------------------------- /polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from polls.models import Choice, Question 4 | 5 | admin.site.register(Question) 6 | admin.site.register(Choice) 7 | -------------------------------------------------------------------------------- /polls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PollsConfig(AppConfig): 5 | name = "polls" 6 | -------------------------------------------------------------------------------- /polls/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TobKed/django_on_gcp/4e25321d856f09a15a58f54cc273abf1e879126d/polls/management/commands/__init__.py -------------------------------------------------------------------------------- /polls/management/commands/create_superuser_from_secrets.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | 4 | import environ 5 | from django.contrib.auth.models import User 6 | from django.core.management.base import BaseCommand 7 | from google.cloud import secretmanager 8 | 9 | env = environ.Env() 10 | 11 | 12 | class Command(BaseCommand): 13 | help = "Create superuser with credentials from secrets" 14 | 15 | def handle(self, *args, **options): 16 | project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") 17 | superuser_credentials = os.environ.get( 18 | "SUPERUSER_CREDENTIALS", "superuser_credentials" 19 | ) 20 | client = secretmanager.SecretManagerServiceClient() 21 | name = f"projects/{project_id}/secrets/{superuser_credentials}/versions/latest" 22 | payload = client.access_secret_version(name=name).payload.data.decode("UTF-8") 23 | env.read_env(io.StringIO(payload)) 24 | 25 | username = env.str("USERNAME") 26 | password = env.str("PASSWORD") 27 | 28 | User.objects.create_superuser( 29 | username=username.strip(), password=password.strip() 30 | ) 31 | 32 | self.stdout.write(self.style.SUCCESS("Successfully created superuser")) 33 | -------------------------------------------------------------------------------- /polls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-04 11:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Question", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("question_text", models.CharField(max_length=200)), 27 | ("pub_date", models.DateTimeField(verbose_name="date published")), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name="Choice", 32 | fields=[ 33 | ( 34 | "id", 35 | models.BigAutoField( 36 | auto_created=True, 37 | primary_key=True, 38 | serialize=False, 39 | verbose_name="ID", 40 | ), 41 | ), 42 | ("choice_text", models.CharField(max_length=200)), 43 | ("votes", models.IntegerField(default=0)), 44 | ( 45 | "question", 46 | models.ForeignKey( 47 | on_delete=django.db.models.deletion.CASCADE, to="polls.question" 48 | ), 49 | ), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /polls/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TobKed/django_on_gcp/4e25321d856f09a15a58f54cc273abf1e879126d/polls/migrations/__init__.py -------------------------------------------------------------------------------- /polls/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | from django.utils import timezone 5 | 6 | 7 | class Question(models.Model): 8 | question_text = models.CharField(max_length=200) 9 | pub_date = models.DateTimeField("date published") 10 | 11 | def __str__(self): 12 | return self.question_text 13 | 14 | def was_published_recently(self): 15 | now = timezone.now() 16 | return now - datetime.timedelta(days=1) <= self.pub_date <= now 17 | 18 | 19 | class Choice(models.Model): 20 | question = models.ForeignKey(Question, on_delete=models.CASCADE) 21 | choice_text = models.CharField(max_length=200) 22 | votes = models.IntegerField(default=0) 23 | 24 | def __str__(self): 25 | return self.choice_text 26 | -------------------------------------------------------------------------------- /polls/templates/polls/detail.html: -------------------------------------------------------------------------------- 1 |

{{ question.question_text }}

2 | 3 | {% if error_message %}

{{ error_message }}

{% endif %} 4 | 5 |
6 | {% csrf_token %} 7 | {% for choice in question.choice_set.all %} 8 | 9 |
10 | {% endfor %} 11 | 12 |
13 | -------------------------------------------------------------------------------- /polls/templates/polls/index.html: -------------------------------------------------------------------------------- 1 | Hello, world. You're at the polls index. 2 | 3 | {% if latest_question_list %} 4 | 9 | {% else %} 10 |

No polls are available.

11 | {% endif %} 12 | -------------------------------------------------------------------------------- /polls/templates/polls/results.html: -------------------------------------------------------------------------------- 1 |

{{ question.question_text }}

2 | 3 | 8 | 9 | Vote again? 10 | -------------------------------------------------------------------------------- /polls/test_polls.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.test import Client, TestCase # noqa: 401 4 | from django.urls import reverse 5 | from django.utils import timezone 6 | 7 | from polls.models import Choice, Question 8 | 9 | 10 | def create_question(question_text, days): 11 | """ 12 | Create a question with the given `question_text` and published the 13 | given number of `days` offset to now (negative for questions published 14 | in the past, positive for questions that have yet to be published). 15 | """ 16 | time = timezone.now() + datetime.timedelta(days=days) 17 | return Question.objects.create(question_text=question_text, pub_date=time) 18 | 19 | 20 | class PollViewTests(TestCase): 21 | def setUp(self): 22 | question = Question( 23 | question_text="This is a test question", pub_date=timezone.now() 24 | ) 25 | question.save() 26 | self.question = question 27 | 28 | choice = Choice(choice_text="This is a test choice", votes=0) 29 | choice.question = question 30 | choice.save() 31 | self.choice = choice 32 | 33 | self.client = Client() 34 | 35 | def test_index_view(self): 36 | response = self.client.get("/") 37 | assert response.status_code == 200 38 | assert self.question.question_text in str(response.content) 39 | 40 | def test_detail_view(self): 41 | response = self.client.get(reverse("polls:detail", args=(self.question.id,))) 42 | assert response.status_code == 200 43 | assert self.question.question_text in str(response.content) 44 | assert self.choice.choice_text in str(response.content) 45 | 46 | def test_results_view(self): 47 | response = self.client.get(reverse("polls:results", args=(self.question.id,))) 48 | assert response.status_code == 200 49 | assert self.question.question_text in str(response.content) 50 | assert self.choice.choice_text in str(response.content) 51 | 52 | 53 | class QuestionIndexViewTests(TestCase): 54 | def test_no_questions(self): 55 | """ 56 | If no questions exist, an appropriate message is displayed. 57 | """ 58 | response = self.client.get(reverse("polls:index")) 59 | self.assertEqual(response.status_code, 200) 60 | self.assertContains(response, "No polls are available.") 61 | self.assertQuerysetEqual(response.context["latest_question_list"], []) 62 | 63 | def test_past_question(self): 64 | """ 65 | Questions with a pub_date in the past are displayed on the 66 | index page. 67 | """ 68 | question = create_question(question_text="Past question.", days=-30) 69 | response = self.client.get(reverse("polls:index")) 70 | self.assertQuerysetEqual( 71 | response.context["latest_question_list"], 72 | [question], 73 | ) 74 | 75 | def test_future_question(self): 76 | """ 77 | Questions with a pub_date in the future aren't displayed on 78 | the index page. 79 | """ 80 | create_question(question_text="Future question.", days=30) 81 | response = self.client.get(reverse("polls:index")) 82 | self.assertContains(response, "No polls are available.") 83 | self.assertQuerysetEqual(response.context["latest_question_list"], []) 84 | 85 | def test_future_question_and_past_question(self): 86 | """ 87 | Even if both past and future questions exist, only past questions 88 | are displayed. 89 | """ 90 | question = create_question(question_text="Past question.", days=-30) 91 | create_question(question_text="Future question.", days=30) 92 | response = self.client.get(reverse("polls:index")) 93 | self.assertQuerysetEqual( 94 | response.context["latest_question_list"], 95 | [question], 96 | ) 97 | 98 | def test_two_past_questions(self): 99 | """ 100 | The questions index page may display multiple questions. 101 | """ 102 | question1 = create_question(question_text="Past question 1.", days=-30) 103 | question2 = create_question(question_text="Past question 2.", days=-5) 104 | response = self.client.get(reverse("polls:index")) 105 | self.assertQuerysetEqual( 106 | response.context["latest_question_list"], 107 | [question2, question1], 108 | ) 109 | 110 | 111 | class QuestionModelTests(TestCase): 112 | def test_was_published_recently_with_future_question(self): 113 | """ 114 | was_published_recently() returns False for questions whose pub_date 115 | is in the future. 116 | """ 117 | time = timezone.now() + datetime.timedelta(days=30) 118 | future_question = Question(pub_date=time) 119 | self.assertIs(future_question.was_published_recently(), False) 120 | 121 | def test_was_published_recently_with_old_question(self): 122 | """ 123 | was_published_recently() returns False for questions whose pub_date 124 | is older than 1 day. 125 | """ 126 | time = timezone.now() - datetime.timedelta(days=1, seconds=1) 127 | old_question = Question(pub_date=time) 128 | self.assertIs(old_question.was_published_recently(), False) 129 | 130 | def test_was_published_recently_with_recent_question(self): 131 | """ 132 | was_published_recently() returns True for questions whose pub_date 133 | is within the last day. 134 | """ 135 | time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) 136 | recent_question = Question(pub_date=time) 137 | self.assertIs(recent_question.was_published_recently(), True) 138 | 139 | 140 | class QuestionDetailViewTests(TestCase): 141 | def test_future_question(self): 142 | """ 143 | The detail view of a question with a pub_date in the future 144 | returns a 404 not found. 145 | """ 146 | future_question = create_question(question_text="Future question.", days=5) 147 | url = reverse("polls:detail", args=(future_question.id,)) 148 | response = self.client.get(url) 149 | self.assertEqual(response.status_code, 404) 150 | 151 | def test_past_question(self): 152 | """ 153 | The detail view of a question with a pub_date in the past 154 | displays the question's text. 155 | """ 156 | past_question = create_question(question_text="Past Question.", days=-5) 157 | url = reverse("polls:detail", args=(past_question.id,)) 158 | response = self.client.get(url) 159 | self.assertContains(response, past_question.question_text) 160 | -------------------------------------------------------------------------------- /polls/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from polls import views 4 | 5 | app_name = "polls" 6 | urlpatterns = [ 7 | path("", views.IndexView.as_view(), name="index"), 8 | path("/", views.DetailView.as_view(), name="detail"), 9 | path("/results/", views.ResultsView.as_view(), name="results"), 10 | path("/vote/", views.vote, name="vote"), 11 | ] 12 | -------------------------------------------------------------------------------- /polls/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | from django.urls import reverse 4 | from django.utils import timezone 5 | from django.views import generic 6 | 7 | from polls.models import Choice, Question 8 | 9 | 10 | class IndexView(generic.ListView): 11 | template_name = "polls/index.html" 12 | context_object_name = "latest_question_list" 13 | 14 | def get_queryset(self): 15 | """Return the last five published questions.""" 16 | return Question.objects.filter(pub_date__lte=timezone.now()).order_by( 17 | "-pub_date" 18 | )[:5] 19 | 20 | 21 | class DetailView(generic.DetailView): 22 | model = Question 23 | template_name = "polls/detail.html" 24 | 25 | def get_queryset(self): 26 | """ 27 | Excludes any questions that aren't published yet. 28 | """ 29 | return Question.objects.filter(pub_date__lte=timezone.now()) 30 | 31 | 32 | class ResultsView(generic.DetailView): 33 | model = Question 34 | template_name = "polls/results.html" 35 | 36 | 37 | def vote(request, question_id): 38 | question = get_object_or_404(Question, pk=question_id) 39 | try: 40 | selected_choice = question.choice_set.get(pk=request.POST["choice"]) 41 | except (KeyError, Choice.DoesNotExist): 42 | return render( 43 | request, 44 | "polls/detail.html", 45 | { 46 | "question": question, 47 | "error_message": "You didn't select a choice.", 48 | }, 49 | ) 50 | else: 51 | selected_choice.votes += 1 52 | selected_choice.save() 53 | return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) 54 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.2.14 2 | django-storages[google]==1.12.3 3 | django-environ==0.8.1 4 | psycopg2-binary==2.9.1 5 | gunicorn==20.1.0 6 | google-cloud-secret-manager==2.7.2 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 4 | 5 | [pycodestyle] 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 8 | -------------------------------------------------------------------------------- /terraform/envs/cloud_run/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | } 4 | 5 | provider "google" { 6 | project = var.project_id 7 | region = var.region 8 | zone = var.zone 9 | } 10 | 11 | resource "google_storage_bucket" "bucket" { 12 | name = "${var.project_id}-media" 13 | location = "EU" 14 | force_destroy = true 15 | } 16 | 17 | module "django_cloud_run" { 18 | source = "../../modules/django_cloud_run" 19 | 20 | project_id = var.project_id 21 | region = var.region 22 | zone = var.zone 23 | 24 | django_secret_key = var.django_secret_key 25 | sql_user = var.sql_user 26 | sql_password = var.sql_password 27 | sql_database_instance_name = var.sql_database_instance_name 28 | sql_database_name = var.sql_database_name 29 | gcs_bucket_name = google_storage_bucket.bucket.name 30 | cloud_run_service_account_name = var.cloud_run_service_account_name 31 | } 32 | -------------------------------------------------------------------------------- /terraform/envs/cloud_run/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_instance_name" { 2 | value = module.django_cloud_run.google_sql_database_instance_name 3 | } 4 | 5 | output "google_sql_database_name" { 6 | value = module.django_cloud_run.google_sql_database_name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/envs/cloud_run/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "europe-central2" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "europe-central2-a" 16 | } 17 | 18 | variable "cloud_run_service_account_name" { 19 | description = "Name of the service account created for Cloud RUn app" 20 | type = string 21 | } 22 | 23 | variable "sql_database_instance_name" { 24 | description = "SQL database instance name" 25 | type = string 26 | } 27 | 28 | variable "sql_database_name" { 29 | description = "SQL database name" 30 | type = string 31 | } 32 | 33 | variable "django_secret_key" { 34 | description = "Django app secret key" 35 | type = string 36 | } 37 | 38 | variable "sql_user" { 39 | description = "SQL database username" 40 | type = string 41 | } 42 | 43 | variable "sql_password" { 44 | description = "SQL database password" 45 | type = string 46 | } 47 | -------------------------------------------------------------------------------- /terraform/envs/gae_flexible/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | } 4 | 5 | provider "google" { 6 | project = var.project_id 7 | region = var.region 8 | zone = var.zone 9 | } 10 | 11 | resource "google_storage_bucket" "bucket" { 12 | name = "${var.project_id}-media" 13 | location = "EU" 14 | force_destroy = true 15 | } 16 | 17 | module "django_gae_flexible" { 18 | source = "../../modules/django_gae_flexible" 19 | 20 | project_id = var.project_id 21 | region = var.region 22 | zone = var.zone 23 | 24 | django_secret_key = var.django_secret_key 25 | sql_user = var.sql_user 26 | sql_password = var.sql_password 27 | sql_database_instance_name = var.sql_database_instance_name 28 | sql_database_name = var.sql_database_name 29 | gcs_bucket_name = google_storage_bucket.bucket.name 30 | } 31 | -------------------------------------------------------------------------------- /terraform/envs/gae_flexible/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_instance_name" { 2 | value = module.django_gae_flexible.google_sql_database_instance_name 3 | } 4 | 5 | output "google_sql_database_name" { 6 | value = module.django_gae_flexible.google_sql_database_name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/envs/gae_flexible/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "europe-central2" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "europe-central2-a" 16 | } 17 | 18 | variable "sql_database_instance_name" { 19 | description = "SQL database instance name" 20 | type = string 21 | } 22 | 23 | variable "sql_database_name" { 24 | description = "SQL database name" 25 | type = string 26 | } 27 | 28 | variable "django_secret_key" { 29 | description = "Django app secret key" 30 | type = string 31 | } 32 | 33 | variable "sql_user" { 34 | description = "SQL database username" 35 | type = string 36 | } 37 | 38 | variable "sql_password" { 39 | description = "SQL database password" 40 | type = string 41 | } 42 | -------------------------------------------------------------------------------- /terraform/envs/gae_standard/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | } 4 | 5 | provider "google" { 6 | project = var.project_id 7 | region = var.region 8 | zone = var.zone 9 | } 10 | 11 | module "django_gae_standard" { 12 | source = "../../modules/django_gae_standard" 13 | 14 | project_id = var.project_id 15 | region = var.region 16 | zone = var.zone 17 | 18 | django_secret_key = var.django_secret_key 19 | sql_user = var.sql_user 20 | sql_password = var.sql_password 21 | sql_database_instance_name = var.sql_database_instance_name 22 | sql_database_name = var.sql_database_name 23 | } 24 | -------------------------------------------------------------------------------- /terraform/envs/gae_standard/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_instance_name" { 2 | value = module.django_gae_standard.google_sql_database_instance_name 3 | } 4 | 5 | output "google_sql_database_name" { 6 | value = module.django_gae_standard.google_sql_database_name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/envs/gae_standard/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "europe-central2" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "europe-central2-a" 16 | } 17 | 18 | variable "sql_database_instance_name" { 19 | description = "SQL database instance name" 20 | type = string 21 | } 22 | 23 | variable "sql_database_name" { 24 | description = "SQL database name" 25 | type = string 26 | } 27 | 28 | variable "django_secret_key" { 29 | description = "Django app secret key" 30 | type = string 31 | } 32 | 33 | variable "sql_user" { 34 | description = "SQL database username" 35 | type = string 36 | } 37 | 38 | variable "sql_password" { 39 | description = "SQL database password" 40 | type = string 41 | } 42 | -------------------------------------------------------------------------------- /terraform/envs/gae_standard_with_gcs/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | } 4 | 5 | provider "google" { 6 | project = var.project_id 7 | region = var.region 8 | zone = var.zone 9 | } 10 | 11 | resource "google_storage_bucket" "bucket" { 12 | name = "${var.project_id}-media" 13 | location = "EU" 14 | force_destroy = true 15 | } 16 | 17 | module "django_gae_standard" { 18 | source = "../../modules/django_gae_standard" 19 | 20 | project_id = var.project_id 21 | region = var.region 22 | zone = var.zone 23 | 24 | django_secret_key = var.django_secret_key 25 | sql_user = var.sql_user 26 | sql_password = var.sql_password 27 | sql_database_instance_name = var.sql_database_instance_name 28 | sql_database_name = var.sql_database_name 29 | gcs_bucket_name = google_storage_bucket.bucket.name 30 | } 31 | -------------------------------------------------------------------------------- /terraform/envs/gae_standard_with_gcs/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_instance_name" { 2 | value = module.django_gae_standard.google_sql_database_instance_name 3 | } 4 | 5 | output "google_sql_database_name" { 6 | value = module.django_gae_standard.google_sql_database_name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/envs/gae_standard_with_gcs/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "europe-central2" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "europe-central2-a" 16 | } 17 | 18 | variable "sql_database_instance_name" { 19 | description = "SQL database instance name" 20 | type = string 21 | } 22 | 23 | variable "sql_database_name" { 24 | description = "SQL database name" 25 | type = string 26 | } 27 | 28 | variable "django_secret_key" { 29 | description = "Django app secret key" 30 | type = string 31 | } 32 | 33 | variable "sql_user" { 34 | description = "SQL database username" 35 | type = string 36 | } 37 | 38 | variable "sql_password" { 39 | description = "SQL database password" 40 | type = string 41 | } 42 | -------------------------------------------------------------------------------- /terraform/envs/gke/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | } 4 | 5 | provider "google" { 6 | project = var.project_id 7 | region = var.region 8 | zone = var.zone 9 | } 10 | 11 | resource "google_storage_bucket" "bucket" { 12 | name = "${var.project_id}-media" 13 | location = "EU" 14 | force_destroy = true 15 | } 16 | 17 | module "django_gke" { 18 | source = "../../modules/django_gke" 19 | 20 | project_id = var.project_id 21 | region = var.region 22 | zone = var.zone 23 | 24 | django_secret_key = var.django_secret_key 25 | sql_user = var.sql_user 26 | sql_password = var.sql_password 27 | sql_database_instance_name = var.sql_database_instance_name 28 | gcs_bucket_name = google_storage_bucket.bucket.name 29 | cloud_run_service_account_name = var.cloud_run_service_account_name 30 | } 31 | -------------------------------------------------------------------------------- /terraform/envs/gke/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_name" { 2 | value = module.django_gke.google_sql_database_name 3 | } 4 | -------------------------------------------------------------------------------- /terraform/envs/gke/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "europe-central2" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "europe-central2-a" 16 | } 17 | 18 | variable "sql_database_instance_name" { 19 | description = "SQL database instance name" 20 | type = string 21 | } 22 | 23 | variable "django_secret_key" { 24 | description = "Django app secret key" 25 | type = string 26 | } 27 | 28 | variable "sql_user" { 29 | description = "SQL database username" 30 | type = string 31 | } 32 | 33 | variable "sql_password" { 34 | description = "SQL database password" 35 | type = string 36 | } 37 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** 4 | 5 | - [Requirements](#requirements) 6 | - [Providers](#providers) 7 | - [Modules](#modules) 8 | - [Resources](#resources) 9 | - [Inputs](#inputs) 10 | - [Outputs](#outputs) 11 | 12 | 13 | 14 | 15 | ## Requirements 16 | 17 | | Name | Version | 18 | |------|---------| 19 | | [terraform](#requirement\_terraform) | >= 1.0.10 | 20 | | [google](#requirement\_google) | ~> 4.1.0 | 21 | 22 | ## Providers 23 | 24 | | Name | Version | 25 | |------|---------| 26 | | [google](#provider\_google) | ~> 4.1.0 | 27 | 28 | ## Modules 29 | 30 | No modules. 31 | 32 | ## Resources 33 | 34 | | Name | Type | 35 | |------|------| 36 | | [google_project_iam_binding.cloudsql_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 37 | | [google_project_iam_binding.cloudsql_client](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 38 | | [google_project_iam_binding.run_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 39 | | [google_project_iam_binding.run_invoker](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 40 | | [google_project_iam_binding.secret_manager_secret_accessor](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 41 | | [google_project_service.gcp_services](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_service) | resource | 42 | | [google_secret_manager_secret.django_settings](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret) | resource | 43 | | [google_secret_manager_secret_version.django_settings_version](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_version) | resource | 44 | | [google_service_account.cloud_run_service_account](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | 45 | | [google_service_account_iam_binding.admin-account-iam](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource | 46 | | [google_sql_database.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database) | resource | 47 | | [google_sql_database_instance.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance) | resource | 48 | | [google_sql_user.sql_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_user) | resource | 49 | | [google_storage_bucket_iam_binding.storage_object_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_iam_binding) | resource | 50 | | [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | 51 | 52 | ## Inputs 53 | 54 | | Name | Description | Type | Default | Required | 55 | |------|-------------|------|---------|:--------:| 56 | | [cloud\_run\_service\_account\_name](#input\_cloud\_run\_service\_account\_name) | Name of the service account created for Cloud RUn app | `string` | n/a | yes | 57 | | [django\_secret\_key](#input\_django\_secret\_key) | Django app secret key | `string` | n/a | yes | 58 | | [django\_settings\_name](#input\_django\_settings\_name) | Django settings name | `string` | `"django_settings"` | no | 59 | | [gcp\_service\_list](#input\_gcp\_service\_list) | The list of apis necessary for the project | `list(string)` |
[
"secretmanager.googleapis.com",
"cloudbuild.googleapis.com",
"run.googleapis.com",
"sqladmin.googleapis.com"
]
| no | 60 | | [gcs\_bucket\_name](#input\_gcs\_bucket\_name) | Name of existing Google Cloud Storage bucket | `string` | n/a | yes | 61 | | [project\_id](#input\_project\_id) | Project id where app will be deployed | `string` | n/a | yes | 62 | | [region](#input\_region) | Region of the components | `string` | `"us-central1"` | no | 63 | | [sql\_database\_instance\_name](#input\_sql\_database\_instance\_name) | SQL database instance name | `string` | `"database-instance"` | no | 64 | | [sql\_database\_name](#input\_sql\_database\_name) | SQL database name | `string` | `"database"` | no | 65 | | [sql\_password](#input\_sql\_password) | SQL database password | `string` | n/a | yes | 66 | | [sql\_user](#input\_sql\_user) | SQL database username | `string` | n/a | yes | 67 | | [zone](#input\_zone) | Zone of the components | `string` | `"us-central1-a"` | no | 68 | 69 | ## Outputs 70 | 71 | | Name | Description | 72 | |------|-------------| 73 | | [google\_sql\_database\_instance\_name](#output\_google\_sql\_database\_instance\_name) | n/a | 74 | | [google\_sql\_database\_name](#output\_google\_sql\_database\_name) | n/a | 75 | 76 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/iam.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | google_cloud_build_default_service_account = "${data.google_project.project.number}@cloudbuild.gserviceaccount.com" 3 | } 4 | 5 | resource "google_service_account" "cloud_run_service_account" { 6 | account_id = var.cloud_run_service_account_name 7 | display_name = "Cloud Run Service Account" 8 | } 9 | 10 | resource "google_storage_bucket_iam_binding" "storage_object_admin" { 11 | bucket = var.gcs_bucket_name 12 | role = "roles/storage.objectAdmin" 13 | 14 | members = [ 15 | "serviceAccount:${google_service_account.cloud_run_service_account.email}", 16 | ] 17 | 18 | depends_on = [google_service_account.cloud_run_service_account] 19 | } 20 | 21 | resource "google_project_iam_binding" "secret_manager_secret_accessor" { 22 | project = var.project_id 23 | role = "roles/secretmanager.secretAccessor" 24 | 25 | members = [ 26 | "serviceAccount:${google_service_account.cloud_run_service_account.email}", 27 | "serviceAccount:${local.google_cloud_build_default_service_account}" 28 | ] 29 | 30 | depends_on = [ 31 | google_project_service.gcp_services, 32 | google_service_account.cloud_run_service_account 33 | ] 34 | } 35 | 36 | resource "google_project_iam_binding" "run_invoker" { 37 | project = var.project_id 38 | role = "roles/run.invoker" 39 | 40 | members = [ 41 | "serviceAccount:${google_service_account.cloud_run_service_account.email}", 42 | ] 43 | 44 | depends_on = [google_service_account.cloud_run_service_account] 45 | } 46 | 47 | resource "google_project_iam_binding" "run_admin" { 48 | project = var.project_id 49 | role = "roles/run.admin" 50 | 51 | members = [ 52 | "serviceAccount:${local.google_cloud_build_default_service_account}" 53 | ] 54 | 55 | depends_on = [google_project_service.gcp_services] 56 | } 57 | 58 | resource "google_project_iam_binding" "cloudsql_client" { 59 | project = var.project_id 60 | role = "roles/cloudsql.client" 61 | 62 | members = [ 63 | "serviceAccount:${google_service_account.cloud_run_service_account.email}", 64 | ] 65 | 66 | depends_on = [google_project_service.gcp_services] 67 | } 68 | 69 | resource "google_project_iam_binding" "cloudsql_admin" { 70 | project = var.project_id 71 | role = "roles/cloudsql.admin" 72 | 73 | members = [ 74 | "serviceAccount:${local.google_cloud_build_default_service_account}" 75 | ] 76 | 77 | depends_on = [ 78 | google_service_account.cloud_run_service_account, 79 | google_project_service.gcp_services 80 | ] 81 | } 82 | 83 | resource "google_service_account_iam_binding" "admin-account-iam" { 84 | service_account_id = google_service_account.cloud_run_service_account.name 85 | role = "roles/iam.serviceAccountUser" 86 | 87 | members = [ 88 | "serviceAccount:${local.google_cloud_build_default_service_account}" 89 | ] 90 | 91 | depends_on = [google_project_service.gcp_services] 92 | } 93 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/main.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | project = var.project_id 3 | region = var.region 4 | zone = var.zone 5 | } 6 | 7 | data "google_project" "project" { 8 | } 9 | 10 | variable "gcp_service_list" { 11 | description = "The list of apis necessary for the project" 12 | type = list(string) 13 | default = [ 14 | "secretmanager.googleapis.com", 15 | "cloudbuild.googleapis.com", 16 | "run.googleapis.com", 17 | "sqladmin.googleapis.com" 18 | ] 19 | } 20 | 21 | resource "google_project_service" "gcp_services" { 22 | for_each = toset(var.gcp_service_list) 23 | project = var.project_id 24 | service = each.key 25 | 26 | disable_dependent_services = true 27 | } 28 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_name" { 2 | value = google_sql_database.database.name 3 | } 4 | 5 | output "google_sql_database_instance_name" { 6 | value = google_sql_database_instance.database.name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/secret.tf: -------------------------------------------------------------------------------- 1 | resource "google_secret_manager_secret" "django_settings" { 2 | secret_id = var.django_settings_name 3 | depends_on = [google_project_service.gcp_services] 4 | labels = { 5 | label = "django_settings" 6 | } 7 | 8 | replication { 9 | automatic = true 10 | } 11 | } 12 | 13 | resource "google_secret_manager_secret_version" "django_settings_version" { 14 | secret = google_secret_manager_secret.django_settings.id 15 | 16 | # for debugging purposes you may want to add DEBUG=True 17 | secret_data = <<-EOF 18 | SECRET_KEY=${var.django_secret_key} 19 | DATABASE_URL=postgres://${var.sql_user}:${var.sql_password}@//cloudsql/${var.project_id}:${var.region}:${google_sql_database_instance.database.name}/${google_sql_database.database.name} 20 | GS_BUCKET_NAME=${var.gcs_bucket_name} 21 | EOF 22 | } 23 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/sql.tf: -------------------------------------------------------------------------------- 1 | resource "google_sql_database_instance" "database" { 2 | name = var.sql_database_instance_name 3 | database_version = "POSTGRES_13" 4 | region = var.region 5 | 6 | deletion_protection = false 7 | 8 | settings { 9 | tier = "db-f1-micro" 10 | } 11 | } 12 | 13 | resource "google_sql_database" "database" { 14 | name = var.sql_database_name 15 | instance = google_sql_database_instance.database.name 16 | 17 | # workaround for terraform-provider-google issue: 18 | # googleapi: Error 400: Invalid request: Failed to delete user root. 19 | # helpful hint in comment: 20 | # https://github.com/hashicorp/terraform-provider-google/issues/3820#issuecomment-573665424 21 | depends_on = [google_sql_user.sql_user] 22 | } 23 | 24 | resource "google_sql_user" "sql_user" { 25 | instance = google_sql_database_instance.database.name 26 | name = var.sql_user 27 | password = var.sql_password 28 | } 29 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "us-central1" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "us-central1-a" 16 | } 17 | 18 | variable "django_secret_key" { 19 | description = "Django app secret key" 20 | type = string 21 | } 22 | 23 | variable "cloud_run_service_account_name" { 24 | description = "Name of the service account created for Cloud RUn app" 25 | type = string 26 | } 27 | 28 | variable "django_settings_name" { 29 | description = "Django settings name" 30 | type = string 31 | default = "django_settings" 32 | } 33 | 34 | variable "sql_database_instance_name" { 35 | description = "SQL database instance name" 36 | type = string 37 | default = "database-instance" 38 | } 39 | 40 | variable "sql_database_name" { 41 | description = "SQL database name" 42 | type = string 43 | default = "database" 44 | } 45 | 46 | variable "sql_user" { 47 | description = "SQL database username" 48 | type = string 49 | } 50 | 51 | variable "sql_password" { 52 | description = "SQL database password" 53 | type = string 54 | } 55 | 56 | variable "gcs_bucket_name" { 57 | description = "Name of existing Google Cloud Storage bucket" 58 | type = string 59 | } 60 | -------------------------------------------------------------------------------- /terraform/modules/django_cloud_run/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | 4 | required_providers { 5 | google = { 6 | source = "hashicorp/google" 7 | version = "~> 4.1.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** 4 | 5 | - [Requirements](#requirements) 6 | - [Providers](#providers) 7 | - [Modules](#modules) 8 | - [Resources](#resources) 9 | - [Inputs](#inputs) 10 | - [Outputs](#outputs) 11 | 12 | 13 | 14 | 15 | ## Requirements 16 | 17 | | Name | Version | 18 | |------|---------| 19 | | [terraform](#requirement\_terraform) | >= 1.0.10 | 20 | | [google](#requirement\_google) | ~> 4.1.0 | 21 | 22 | ## Providers 23 | 24 | | Name | Version | 25 | |------|---------| 26 | | [google](#provider\_google) | ~> 4.1.0 | 27 | 28 | ## Modules 29 | 30 | No modules. 31 | 32 | ## Resources 33 | 34 | | Name | Type | 35 | |------|------| 36 | | [google_app_engine_application.app](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/app_engine_application) | resource | 37 | | [google_project_iam_binding.app_engine_app_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 38 | | [google_project_iam_binding.cloudsql_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 39 | | [google_project_iam_binding.secret_manager_secret_accessor](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 40 | | [google_project_service.gcp_services](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_service) | resource | 41 | | [google_secret_manager_secret.django_settings](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret) | resource | 42 | | [google_secret_manager_secret_version.django_settings_version](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_version) | resource | 43 | | [google_service_account_iam_binding.admin-account-iam](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource | 44 | | [google_sql_database.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database) | resource | 45 | | [google_sql_database_instance.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance) | resource | 46 | | [google_sql_user.sql_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_user) | resource | 47 | | [google_app_engine_default_service_account.default](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/app_engine_default_service_account) | data source | 48 | | [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | 49 | 50 | ## Inputs 51 | 52 | | Name | Description | Type | Default | Required | 53 | |------|-------------|------|---------|:--------:| 54 | | [django\_secret\_key](#input\_django\_secret\_key) | Django app secret key | `string` | n/a | yes | 55 | | [django\_settings\_name](#input\_django\_settings\_name) | Django settings name | `string` | `"django_settings"` | no | 56 | | [gcp\_service\_list](#input\_gcp\_service\_list) | The list of apis necessary for the project | `list(string)` |
[
"secretmanager.googleapis.com",
"cloudbuild.googleapis.com",
"appengine.googleapis.com",
"appengineflex.googleapis.com",
"sqladmin.googleapis.com"
]
| no | 57 | | [gcs\_bucket\_name](#input\_gcs\_bucket\_name) | Name of existing Google Cloud Storage bucket (define if static files should be served from GCS) | `string` | `""` | no | 58 | | [project\_id](#input\_project\_id) | Project id where app will be deployed | `string` | n/a | yes | 59 | | [region](#input\_region) | Region of the components | `string` | `"us-central1"` | no | 60 | | [sql\_database\_instance\_name](#input\_sql\_database\_instance\_name) | SQL database instance name | `string` | `"database-instance"` | no | 61 | | [sql\_database\_name](#input\_sql\_database\_name) | SQL database name | `string` | `"database"` | no | 62 | | [sql\_password](#input\_sql\_password) | SQL database password | `string` | n/a | yes | 63 | | [sql\_user](#input\_sql\_user) | SQL database username | `string` | n/a | yes | 64 | | [zone](#input\_zone) | Zone of the components | `string` | `"us-central1-a"` | no | 65 | 66 | ## Outputs 67 | 68 | | Name | Description | 69 | |------|-------------| 70 | | [google\_sql\_database\_instance\_name](#output\_google\_sql\_database\_instance\_name) | n/a | 71 | | [google\_sql\_database\_name](#output\_google\_sql\_database\_name) | n/a | 72 | 73 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/iam.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | google_cloud_build_default_service_account = "${data.google_project.project.number}@cloudbuild.gserviceaccount.com" 3 | } 4 | 5 | data "google_app_engine_default_service_account" "default" { 6 | depends_on = [google_project_service.gcp_services] 7 | } 8 | 9 | resource "google_project_iam_binding" "secret_manager_secret_accessor" { 10 | project = var.project_id 11 | role = "roles/secretmanager.secretAccessor" 12 | 13 | members = [ 14 | "serviceAccount:${data.google_app_engine_default_service_account.default.email}", 15 | "serviceAccount:${local.google_cloud_build_default_service_account}" 16 | ] 17 | 18 | depends_on = [google_project_service.gcp_services] 19 | } 20 | 21 | resource "google_project_iam_binding" "app_engine_app_admin" { 22 | project = var.project_id 23 | role = "roles/appengine.appAdmin" 24 | 25 | members = [ 26 | "serviceAccount:${local.google_cloud_build_default_service_account}" 27 | ] 28 | 29 | depends_on = [google_project_service.gcp_services] 30 | } 31 | 32 | resource "google_project_iam_binding" "cloudsql_admin" { 33 | project = var.project_id 34 | role = "roles/cloudsql.admin" 35 | 36 | members = [ 37 | "serviceAccount:${local.google_cloud_build_default_service_account}" 38 | ] 39 | 40 | depends_on = [google_project_service.gcp_services] 41 | } 42 | 43 | resource "google_service_account_iam_binding" "admin-account-iam" { 44 | service_account_id = data.google_app_engine_default_service_account.default.name 45 | role = "roles/iam.serviceAccountUser" 46 | 47 | members = [ 48 | "serviceAccount:${local.google_cloud_build_default_service_account}" 49 | ] 50 | 51 | depends_on = [google_project_service.gcp_services] 52 | } 53 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/main.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | project = var.project_id 3 | region = var.region 4 | zone = var.zone 5 | } 6 | 7 | data "google_project" "project" { 8 | } 9 | 10 | variable "gcp_service_list" { 11 | description = "The list of apis necessary for the project" 12 | type = list(string) 13 | default = [ 14 | "secretmanager.googleapis.com", 15 | "cloudbuild.googleapis.com", 16 | "appengine.googleapis.com", 17 | "appengineflex.googleapis.com", 18 | "sqladmin.googleapis.com" 19 | ] 20 | } 21 | 22 | resource "google_project_service" "gcp_services" { 23 | for_each = toset(var.gcp_service_list) 24 | project = var.project_id 25 | service = each.key 26 | 27 | disable_dependent_services = true 28 | } 29 | 30 | resource "google_app_engine_application" "app" { 31 | project = var.project_id 32 | location_id = var.region 33 | } 34 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_name" { 2 | value = google_sql_database.database.name 3 | } 4 | 5 | output "google_sql_database_instance_name" { 6 | value = google_sql_database_instance.database.name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/secret.tf: -------------------------------------------------------------------------------- 1 | resource "google_secret_manager_secret" "django_settings" { 2 | secret_id = var.django_settings_name 3 | depends_on = [google_project_service.gcp_services] 4 | labels = { 5 | label = "django_settings" 6 | } 7 | 8 | replication { 9 | automatic = true 10 | } 11 | } 12 | 13 | resource "google_secret_manager_secret_version" "django_settings_version" { 14 | secret = google_secret_manager_secret.django_settings.id 15 | 16 | # for debugging purposes you may want to add DEBUG=True 17 | secret_data = <<-EOF 18 | SECRET_KEY=${var.django_secret_key} 19 | DATABASE_URL=postgres://${var.sql_user}:${var.sql_password}@//cloudsql/${var.project_id}:${var.region}:${google_sql_database_instance.database.name}/${google_sql_database.database.name} 20 | GS_BUCKET_NAME=${var.gcs_bucket_name} 21 | EOF 22 | } 23 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/sql.tf: -------------------------------------------------------------------------------- 1 | resource "google_sql_database_instance" "database" { 2 | name = var.sql_database_instance_name 3 | database_version = "POSTGRES_13" 4 | region = var.region 5 | 6 | deletion_protection = false 7 | 8 | settings { 9 | tier = "db-f1-micro" 10 | } 11 | } 12 | 13 | resource "google_sql_database" "database" { 14 | name = var.sql_database_name 15 | instance = google_sql_database_instance.database.name 16 | 17 | # workaround for terraform-provider-google issue: 18 | # googleapi: Error 400: Invalid request: Failed to delete user root. 19 | # helpful hint in comment: 20 | # https://github.com/hashicorp/terraform-provider-google/issues/3820#issuecomment-573665424 21 | depends_on = [google_sql_user.sql_user] 22 | } 23 | 24 | resource "google_sql_user" "sql_user" { 25 | instance = google_sql_database_instance.database.name 26 | name = var.sql_user 27 | password = var.sql_password 28 | } 29 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "us-central1" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "us-central1-a" 16 | } 17 | 18 | variable "django_secret_key" { 19 | description = "Django app secret key" 20 | type = string 21 | } 22 | 23 | variable "django_settings_name" { 24 | description = "Django settings name" 25 | type = string 26 | default = "django_settings" 27 | } 28 | 29 | variable "sql_database_instance_name" { 30 | description = "SQL database instance name" 31 | type = string 32 | default = "database-instance" 33 | } 34 | 35 | variable "sql_database_name" { 36 | description = "SQL database name" 37 | type = string 38 | default = "database" 39 | } 40 | 41 | variable "sql_user" { 42 | description = "SQL database username" 43 | type = string 44 | } 45 | 46 | variable "sql_password" { 47 | description = "SQL database password" 48 | type = string 49 | } 50 | 51 | variable "gcs_bucket_name" { 52 | description = "Name of existing Google Cloud Storage bucket (define if static files should be served from GCS)" 53 | type = string 54 | default = "" 55 | } 56 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_flexible/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | 4 | required_providers { 5 | google = { 6 | source = "hashicorp/google" 7 | version = "~> 4.1.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** 4 | 5 | - [Requirements](#requirements) 6 | - [Providers](#providers) 7 | - [Modules](#modules) 8 | - [Resources](#resources) 9 | - [Inputs](#inputs) 10 | - [Outputs](#outputs) 11 | 12 | 13 | 14 | 15 | ## Requirements 16 | 17 | | Name | Version | 18 | |------|---------| 19 | | [terraform](#requirement\_terraform) | >= 1.0.10 | 20 | | [google](#requirement\_google) | ~> 4.1.0 | 21 | 22 | ## Providers 23 | 24 | | Name | Version | 25 | |------|---------| 26 | | [google](#provider\_google) | ~> 4.1.0 | 27 | 28 | ## Modules 29 | 30 | No modules. 31 | 32 | ## Resources 33 | 34 | | Name | Type | 35 | |------|------| 36 | | [google_app_engine_application.app](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/app_engine_application) | resource | 37 | | [google_project_iam_binding.app_engine_app_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 38 | | [google_project_iam_binding.cloudsql_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 39 | | [google_project_iam_binding.secret_manager_secret_accessor](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 40 | | [google_project_service.gcp_services](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_service) | resource | 41 | | [google_secret_manager_secret.django_settings](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret) | resource | 42 | | [google_secret_manager_secret_version.django_settings_version](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_version) | resource | 43 | | [google_service_account_iam_binding.admin-account-iam](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource | 44 | | [google_sql_database.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database) | resource | 45 | | [google_sql_database_instance.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance) | resource | 46 | | [google_sql_user.sql_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_user) | resource | 47 | | [google_app_engine_default_service_account.default](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/app_engine_default_service_account) | data source | 48 | | [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | 49 | 50 | ## Inputs 51 | 52 | | Name | Description | Type | Default | Required | 53 | |------|-------------|------|---------|:--------:| 54 | | [django\_secret\_key](#input\_django\_secret\_key) | Django app secret key | `string` | n/a | yes | 55 | | [django\_settings\_name](#input\_django\_settings\_name) | Django settings name | `string` | `"django_settings"` | no | 56 | | [gcp\_service\_list](#input\_gcp\_service\_list) | The list of apis necessary for the project | `list(string)` |
[
"secretmanager.googleapis.com",
"cloudbuild.googleapis.com",
"appengine.googleapis.com",
"sqladmin.googleapis.com"
]
| no | 57 | | [gcs\_bucket\_name](#input\_gcs\_bucket\_name) | Name of existing Google Cloud Storage bucket (define if static files should be served from GCS) | `string` | `""` | no | 58 | | [project\_id](#input\_project\_id) | Project id where app will be deployed | `string` | n/a | yes | 59 | | [region](#input\_region) | Region of the components | `string` | `"us-central1"` | no | 60 | | [sql\_database\_instance\_name](#input\_sql\_database\_instance\_name) | SQL database instance name | `string` | `"database-instance"` | no | 61 | | [sql\_database\_name](#input\_sql\_database\_name) | SQL database name | `string` | `"database"` | no | 62 | | [sql\_password](#input\_sql\_password) | SQL database password | `string` | n/a | yes | 63 | | [sql\_user](#input\_sql\_user) | SQL database username | `string` | n/a | yes | 64 | | [zone](#input\_zone) | Zone of the components | `string` | `"us-central1-a"` | no | 65 | 66 | ## Outputs 67 | 68 | | Name | Description | 69 | |------|-------------| 70 | | [google\_sql\_database\_instance\_name](#output\_google\_sql\_database\_instance\_name) | n/a | 71 | | [google\_sql\_database\_name](#output\_google\_sql\_database\_name) | n/a | 72 | 73 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/iam.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | google_cloud_build_default_service_account = "${data.google_project.project.number}@cloudbuild.gserviceaccount.com" 3 | } 4 | 5 | data "google_app_engine_default_service_account" "default" { 6 | depends_on = [google_project_service.gcp_services] 7 | } 8 | 9 | resource "google_project_iam_binding" "secret_manager_secret_accessor" { 10 | project = var.project_id 11 | role = "roles/secretmanager.secretAccessor" 12 | 13 | members = [ 14 | "serviceAccount:${data.google_app_engine_default_service_account.default.email}", 15 | "serviceAccount:${local.google_cloud_build_default_service_account}" 16 | ] 17 | 18 | depends_on = [google_project_service.gcp_services] 19 | } 20 | 21 | resource "google_project_iam_binding" "app_engine_app_admin" { 22 | project = var.project_id 23 | role = "roles/appengine.appAdmin" 24 | 25 | members = [ 26 | "serviceAccount:${local.google_cloud_build_default_service_account}" 27 | ] 28 | 29 | depends_on = [google_project_service.gcp_services] 30 | } 31 | 32 | resource "google_project_iam_binding" "cloudsql_admin" { 33 | project = var.project_id 34 | role = "roles/cloudsql.admin" 35 | 36 | members = [ 37 | "serviceAccount:${local.google_cloud_build_default_service_account}" 38 | ] 39 | 40 | depends_on = [google_project_service.gcp_services] 41 | } 42 | 43 | resource "google_service_account_iam_binding" "admin-account-iam" { 44 | service_account_id = data.google_app_engine_default_service_account.default.name 45 | role = "roles/iam.serviceAccountUser" 46 | 47 | members = [ 48 | "serviceAccount:${local.google_cloud_build_default_service_account}" 49 | ] 50 | 51 | depends_on = [google_project_service.gcp_services] 52 | } 53 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/main.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | project = var.project_id 3 | region = var.region 4 | zone = var.zone 5 | } 6 | 7 | data "google_project" "project" { 8 | } 9 | 10 | variable "gcp_service_list" { 11 | description = "The list of apis necessary for the project" 12 | type = list(string) 13 | default = [ 14 | "secretmanager.googleapis.com", 15 | "cloudbuild.googleapis.com", 16 | "appengine.googleapis.com", 17 | "sqladmin.googleapis.com" 18 | ] 19 | } 20 | 21 | resource "google_project_service" "gcp_services" { 22 | for_each = toset(var.gcp_service_list) 23 | project = var.project_id 24 | service = each.key 25 | 26 | disable_dependent_services = true 27 | } 28 | 29 | resource "google_app_engine_application" "app" { 30 | project = var.project_id 31 | location_id = var.region 32 | } 33 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_name" { 2 | value = google_sql_database.database.name 3 | } 4 | 5 | output "google_sql_database_instance_name" { 6 | value = google_sql_database_instance.database.name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/secret.tf: -------------------------------------------------------------------------------- 1 | resource "google_secret_manager_secret" "django_settings" { 2 | secret_id = var.django_settings_name 3 | depends_on = [google_project_service.gcp_services] 4 | labels = { 5 | label = "django_settings" 6 | } 7 | 8 | replication { 9 | automatic = true 10 | } 11 | } 12 | 13 | resource "google_secret_manager_secret_version" "django_settings_version" { 14 | secret = google_secret_manager_secret.django_settings.id 15 | 16 | # for debugging purposes you may want to add DEBUG=True 17 | secret_data = <<-EOF 18 | SECRET_KEY=${var.django_secret_key} 19 | DATABASE_URL=postgres://${var.sql_user}:${var.sql_password}@//cloudsql/${var.project_id}:${var.region}:${google_sql_database_instance.database.name}/${google_sql_database.database.name} 20 | GS_BUCKET_NAME=${var.gcs_bucket_name} 21 | EOF 22 | } 23 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/sql.tf: -------------------------------------------------------------------------------- 1 | resource "google_sql_database_instance" "database" { 2 | name = var.sql_database_instance_name 3 | database_version = "POSTGRES_13" 4 | region = var.region 5 | 6 | deletion_protection = false 7 | 8 | settings { 9 | tier = "db-f1-micro" 10 | } 11 | } 12 | 13 | resource "google_sql_database" "database" { 14 | name = var.sql_database_name 15 | instance = google_sql_database_instance.database.name 16 | 17 | # workaround for terraform-provider-google issue: 18 | # googleapi: Error 400: Invalid request: Failed to delete user root. 19 | # helpful hint in comment: 20 | # https://github.com/hashicorp/terraform-provider-google/issues/3820#issuecomment-573665424 21 | depends_on = [google_sql_user.sql_user] 22 | } 23 | 24 | resource "google_sql_user" "sql_user" { 25 | instance = google_sql_database_instance.database.name 26 | name = var.sql_user 27 | password = var.sql_password 28 | } 29 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "us-central1" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "us-central1-a" 16 | } 17 | 18 | variable "django_secret_key" { 19 | description = "Django app secret key" 20 | type = string 21 | } 22 | 23 | variable "django_settings_name" { 24 | description = "Django settings name" 25 | type = string 26 | default = "django_settings" 27 | } 28 | 29 | variable "sql_database_instance_name" { 30 | description = "SQL database instance name" 31 | type = string 32 | default = "database-instance" 33 | } 34 | 35 | variable "sql_database_name" { 36 | description = "SQL database name" 37 | type = string 38 | default = "database" 39 | } 40 | 41 | variable "sql_user" { 42 | description = "SQL database username" 43 | type = string 44 | } 45 | 46 | variable "sql_password" { 47 | description = "SQL database password" 48 | type = string 49 | } 50 | 51 | variable "gcs_bucket_name" { 52 | description = "Name of existing Google Cloud Storage bucket (define if static files should be served from GCS)" 53 | type = string 54 | default = "" 55 | } 56 | -------------------------------------------------------------------------------- /terraform/modules/django_gae_standard/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | 4 | required_providers { 5 | google = { 6 | source = "hashicorp/google" 7 | version = "~> 4.1.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** 4 | 5 | - [Requirements](#requirements) 6 | - [Providers](#providers) 7 | - [Modules](#modules) 8 | - [Resources](#resources) 9 | - [Inputs](#inputs) 10 | - [Outputs](#outputs) 11 | 12 | 13 | 14 | 15 | ## Requirements 16 | 17 | | Name | Version | 18 | |------|---------| 19 | | [terraform](#requirement\_terraform) | >= 1.0.10 | 20 | | [google](#requirement\_google) | ~> 4.0.0 | 21 | 22 | ## Providers 23 | 24 | | Name | Version | 25 | |------|---------| 26 | | [google](#provider\_google) | ~> 4.0.0 | 27 | 28 | ## Modules 29 | 30 | No modules. 31 | 32 | ## Resources 33 | 34 | | Name | Type | 35 | |------|------| 36 | | [google_app_engine_application.app](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/app_engine_application) | resource | 37 | | [google_project_iam_binding.cloudsql_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 38 | | [google_project_iam_binding.cloudsql_client](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 39 | | [google_project_iam_binding.run_invoker](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 40 | | [google_project_iam_binding.secret_manager_secret_accessor](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_binding) | resource | 41 | | [google_project_service.gcp_services](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_service) | resource | 42 | | [google_secret_manager_secret.django_settings](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret) | resource | 43 | | [google_secret_manager_secret_version.django_settings_version](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_version) | resource | 44 | | [google_service_account.cloud_run_service_account](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | 45 | | [google_service_account_iam_binding.admin-account-iam](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource | 46 | | [google_sql_database.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database) | resource | 47 | | [google_sql_database_instance.database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance) | resource | 48 | | [google_sql_user.sql_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_user) | resource | 49 | | [google_storage_bucket_iam_binding.storage_object_admin](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_iam_binding) | resource | 50 | | [google_app_engine_default_service_account.default](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/app_engine_default_service_account) | data source | 51 | | [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | 52 | 53 | ## Inputs 54 | 55 | | Name | Description | Type | Default | Required | 56 | |------|-------------|------|---------|:--------:| 57 | | [cloud\_run\_service\_account\_name](#input\_cloud\_run\_service\_account\_name) | Name of the service account created for Cloud RUn app | `string` | n/a | yes | 58 | | [django\_secret\_key](#input\_django\_secret\_key) | Django app secret key | `string` | n/a | yes | 59 | | [django\_settings\_name](#input\_django\_settings\_name) | Django settings name | `string` | `"django_settings"` | no | 60 | | [gcp\_service\_list](#input\_gcp\_service\_list) | The list of apis necessary for the project | `list(string)` |
[
"secretmanager.googleapis.com",
"cloudbuild.googleapis.com",
"run.googleapis.com",
"sqladmin.googleapis.com"
]
| no | 61 | | [gcs\_bucket\_name](#input\_gcs\_bucket\_name) | Name of existing Google Cloud Storage bucket | `string` | n/a | yes | 62 | | [project\_id](#input\_project\_id) | Project id where app will be deployed | `string` | n/a | yes | 63 | | [region](#input\_region) | Region of the components | `string` | `"us-central1"` | no | 64 | | [sql\_database\_instance\_name](#input\_sql\_database\_instance\_name) | SQL database instance name | `string` | `"database-instance"` | no | 65 | | [sql\_database\_name](#input\_sql\_database\_name) | SQL database name | `string` | `"database"` | no | 66 | | [sql\_password](#input\_sql\_password) | SQL database password | `string` | n/a | yes | 67 | | [sql\_user](#input\_sql\_user) | SQL database username | `string` | n/a | yes | 68 | | [zone](#input\_zone) | Zone of the components | `string` | `"us-central1-a"` | no | 69 | 70 | ## Outputs 71 | 72 | | Name | Description | 73 | |------|-------------| 74 | | [google\_sql\_database\_name](#output\_google\_sql\_database\_name) | n/a | 75 | 76 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/iam.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | google_cloud_build_default_service_account = "${data.google_project.project.number}@cloudbuild.gserviceaccount.com" 3 | } 4 | 5 | resource "google_service_account" "gke_cloud_sql_service_account" { 6 | account_id = "${var.project_id}-gke-cloud-sql-sa" 7 | display_name = "GKE Service Account" 8 | } 9 | 10 | resource "google_storage_bucket_iam_binding" "storage_object_admin" { 11 | bucket = var.gcs_bucket_name 12 | role = "roles/storage.objectAdmin" 13 | 14 | members = [ 15 | "serviceAccount:${google_service_account.gke_cloud_sql_service_account.email}", 16 | ] 17 | } 18 | 19 | resource "google_project_iam_binding" "secret_manager_secret_accessor" { 20 | project = var.project_id 21 | role = "roles/secretmanager.secretAccessor" 22 | 23 | members = [ 24 | "serviceAccount:${google_service_account.gke_cloud_sql_service_account.email}", 25 | "serviceAccount:${local.google_cloud_build_default_service_account}" 26 | ] 27 | } 28 | 29 | resource "google_project_iam_binding" "run_invoker" { 30 | project = var.project_id 31 | role = "roles/run.invoker" 32 | 33 | members = [ 34 | "serviceAccount:${google_service_account.gke_cloud_sql_service_account.email}", 35 | ] 36 | } 37 | 38 | resource "google_project_iam_binding" "run_admin" { 39 | project = var.project_id 40 | role = "roles/run.admin" 41 | 42 | members = [ 43 | "serviceAccount:${local.google_cloud_build_default_service_account}" 44 | ] 45 | } 46 | 47 | resource "google_project_iam_binding" "cloudsql_client" { 48 | project = var.project_id 49 | role = "roles/cloudsql.client" 50 | 51 | members = [ 52 | "serviceAccount:${google_service_account.gke_cloud_sql_service_account.email}", 53 | ] 54 | } 55 | 56 | resource "google_project_iam_binding" "cloudsql_admin" { 57 | project = var.project_id 58 | role = "roles/cloudsql.admin" 59 | 60 | members = [ 61 | "serviceAccount:${local.google_cloud_build_default_service_account}" 62 | ] 63 | } 64 | 65 | resource "google_service_account_iam_binding" "admin-account-iam" { 66 | service_account_id = google_service_account.gke_cloud_sql_service_account.name 67 | role = "roles/iam.serviceAccountUser" 68 | 69 | members = [ 70 | "serviceAccount:${local.google_cloud_build_default_service_account}" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/main.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | project = var.project_id 3 | region = var.region 4 | zone = var.zone 5 | } 6 | 7 | data "google_project" "project" { 8 | } 9 | 10 | variable "gcp_service_list" { 11 | description = "The list of apis necessary for the project" 12 | type = list(string) 13 | default = [ 14 | "secretmanager.googleapis.com", 15 | "cloudbuild.googleapis.com", 16 | "run.googleapis.com", 17 | "sqladmin.googleapis.com" 18 | ] 19 | } 20 | 21 | resource "google_project_service" "gcp_services" { 22 | for_each = toset(var.gcp_service_list) 23 | project = var.project_id 24 | service = each.key 25 | } 26 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_sql_database_name" { 2 | value = google_sql_database.database.name 3 | } 4 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/secret.tf: -------------------------------------------------------------------------------- 1 | resource "google_secret_manager_secret" "django_settings" { 2 | secret_id = var.django_settings_name 3 | depends_on = [google_project_service.gcp_services] 4 | labels = { 5 | label = "django_settings" 6 | } 7 | 8 | replication { 9 | automatic = true 10 | } 11 | } 12 | 13 | resource "google_secret_manager_secret_version" "django_settings_version" { 14 | secret = google_secret_manager_secret.django_settings.id 15 | 16 | # for debugging purposes you may want to add DEBUG=True 17 | secret_data = <<-EOF 18 | SECRET_KEY=${var.django_secret_key} 19 | DATABASE_URL=postgres://${var.sql_user}:${var.sql_password}@//cloudsql/${var.project_id}:${var.region}:${google_sql_database_instance.database.name}/${google_sql_database.database.name} 20 | GS_BUCKET_NAME=${var.gcs_bucket_name} 21 | EOF 22 | } 23 | 24 | resource "google_service_account_key" "gke_cloud_sql_service_account_key" { 25 | service_account_id = google_service_account.gke_cloud_sql_service_account.name 26 | } 27 | 28 | resource "kubernetes_secret" "cloudsql_oauth_credentials" { 29 | metadata { 30 | name = "cloudsql-oauth-credentials" 31 | } 32 | data = { 33 | "credentials.json" = base64decode(google_service_account_key.gke_cloud_sql_service_account_key.private_key) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/sql.tf: -------------------------------------------------------------------------------- 1 | resource "google_sql_database_instance" "database" { 2 | name = var.sql_database_instance_name 3 | database_version = "POSTGRES_13" 4 | region = var.region 5 | 6 | deletion_protection = false 7 | 8 | settings { 9 | tier = "db-f1-micro" 10 | } 11 | } 12 | 13 | resource "google_sql_database" "database" { 14 | name = var.sql_database_name 15 | instance = google_sql_database_instance.database.name 16 | } 17 | 18 | resource "google_sql_user" "sql_user" { 19 | instance = google_sql_database_instance.database.name 20 | name = var.sql_user 21 | password = var.sql_password 22 | } 23 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project id where app will be deployed" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "Region of the components" 8 | type = string 9 | default = "us-central1" 10 | } 11 | 12 | variable "zone" { 13 | description = "Zone of the components" 14 | type = string 15 | default = "us-central1-a" 16 | } 17 | 18 | variable "django_secret_key" { 19 | description = "Django app secret key" 20 | type = string 21 | } 22 | 23 | variable "cloud_run_service_account_name" { 24 | description = "Name of the service account created for Cloud RUn app" 25 | type = string 26 | } 27 | 28 | variable "django_settings_name" { 29 | description = "Django settings name" 30 | type = string 31 | default = "django_settings" 32 | } 33 | 34 | variable "sql_database_instance_name" { 35 | description = "SQL database instance name" 36 | type = string 37 | default = "database-instance" 38 | } 39 | 40 | variable "sql_database_name" { 41 | description = "SQL database name" 42 | type = string 43 | default = "database" 44 | } 45 | 46 | variable "sql_user" { 47 | description = "SQL database username" 48 | type = string 49 | } 50 | 51 | variable "sql_password" { 52 | description = "SQL database password" 53 | type = string 54 | } 55 | 56 | variable "gcs_bucket_name" { 57 | description = "Name of existing Google Cloud Storage bucket" 58 | type = string 59 | } 60 | -------------------------------------------------------------------------------- /terraform/modules/django_gke/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.10" 3 | required_providers { 4 | 5 | google = { 6 | source = "hashicorp/google" 7 | version = "~> 4.0.0" 8 | } 9 | } 10 | 11 | } 12 | --------------------------------------------------------------------------------