├── .env.sample ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── app │ ├── __init__.py │ ├── asgi.py │ ├── s3_backends.py │ ├── settings.py │ ├── templates │ │ └── index.html │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── manage.py └── publish │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── static │ └── publish │ │ └── style.css │ ├── templates │ └── publish │ │ ├── base.html │ │ ├── index.html │ │ └── post.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── deploy ├── dashboard-sa.yaml ├── django.yaml ├── kustomization.yaml └── storageclass.yaml ├── docker-compose-deploy.yml ├── docker-compose.yml ├── infra ├── .terraform.lock.hcl ├── ecr.tf ├── efs.tf ├── eks.tf ├── main.tf ├── outputs.tf ├── rds.tf ├── variables.tf └── vpc.tf ├── proxy ├── Dockerfile ├── default.conf.tpl ├── headers.conf └── run.sh ├── requirements.txt └── scripts └── run.sh /.env.sample: -------------------------------------------------------------------------------- 1 | DB_NAME=db 2 | DB_USER=dbuser 3 | DB_PASS=dbpassword 4 | DJANGO_SECRET_KEY=secretkey123 5 | DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .DS_Store 131 | .vscode/ 132 | # Local .terraform directories 133 | **/.terraform/* 134 | 135 | # .tfstate files 136 | *.tfstate 137 | *.tfstate.* 138 | 139 | # Crash log files 140 | crash.log 141 | crash.*.log 142 | 143 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 144 | # password, private keys, and other secrets. These should not be part of version 145 | # control as they are data points which are potentially sensitive and subject 146 | # to change depending on the environment. 147 | *.tfvars 148 | *.tfvars.json 149 | 150 | # Ignore override files as they are usually used to override resources locally and so 151 | # are not checked in 152 | override.tf 153 | override.tf.json 154 | *_override.tf 155 | *_override.tf.json 156 | 157 | # Include override files you do wish to add to version control using negated pattern 158 | # !example_override.tf 159 | 160 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 161 | # example: *tfplan* 162 | 163 | # Ignore CLI configuration files 164 | .terraformrc 165 | terraform.rc 166 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.2-alpine3.17 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | COPY ./requirements.txt /requirements.txt 6 | 7 | RUN apk add --update --no-cache postgresql-client build-base postgresql-dev \ 8 | musl-dev zlib zlib-dev linux-headers 9 | 10 | RUN python -m venv /py && \ 11 | /py/bin/pip install --upgrade pip && \ 12 | /py/bin/pip install -r /requirements.txt 13 | 14 | COPY ./scripts /scripts 15 | RUN chmod -R +x /scripts 16 | 17 | ENV PATH="/scripts:/py/bin:$PATH" 18 | 19 | COPY ./app /app 20 | WORKDIR /app 21 | EXPOSE 80 22 | 23 | CMD ["/scripts/run.sh"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LondonAppDeveloper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Banner image 4 | 5 |
6 | 7 |
8 |

Full-Stack Consulting and Courses.

9 | Website | 10 | Courses | 11 | Tutorials | 12 | Consulting 13 |
14 | 15 |

16 | 17 | # Deploy Django to Kubernetes on AWS (EKS) Finished Code 18 | 19 | Code for [How to Deploy Django to Kubernetes: Part 2](https://youtube.com/live/X_00g6HQwvI) YouTube live stream. 20 | 21 | ## What's covered? 22 | 23 | * How to setup Kubernetes (EKS) using Terraform 24 | * How to setup an RDS database that can be used from EKS 25 | * How to setup EFS for persistent data storage 26 | * How to Deploy a Django app which supports the Django admin and static media files. 27 | 28 | ## Requirements 29 | 30 | * [Terraform](https://developer.hashicorp.com/terraform/downloads?product_intent=terraform) 31 | * [aws-vault](https://github.com/99designs/aws-vault) for AWS authentication 32 | * [Docker](https://docs.docker.com/engine/install/) for building and pushing Docker images 33 | * [Kubernetes CLI (kubectl)](https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/) 34 | * [Helm](https://helm.sh/docs/intro/quickstart/#install-helm) 35 | * [AWS account](https://aws.amazon.com/free/) 36 | 37 | ## Commands 38 | 39 | Useful commands used in the tutorial. 40 | 41 | ### Terraform 42 | 43 | Initialise terraform (required after adding new modules): 44 | ```sh 45 | terraform init 46 | ``` 47 | 48 | Plan terraform (see what changes will be made to resources): 49 | ```sh 50 | terraform plan 51 | ``` 52 | 53 | Apply Teraform (make changes to resources after confirmation): 54 | ```sh 55 | terraform apply 56 | ``` 57 | 58 | Destroy resources in Terraform (removes everything after confirmation): 59 | ```sh 60 | terraform destroy 61 | ``` 62 | 63 | ### AWS CLI 64 | 65 | Configure local EKS CLI to use cluster deployed by Terraform 66 | ```sh 67 | aws eks --region $(terraform output -raw region) update-kubeconfig \ 68 | --name $(terraform output -raw cluster_name) 69 | ``` 70 | 71 | > NOTE: For Windows users, you may need to adjust the `$()` syntax. You can simply run `terraform output` to view all outputs and manually include them in the command. 72 | 73 | Authenticate Docker with ECR 74 | 75 | ```sh 76 | aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com 77 | ``` 78 | 79 | ### Docker 80 | 81 | Build and compress image in amd46 platform architecture: 82 | 83 | ```sh 84 | docker build -t : --platform linux/amd64 --compress . 85 | docker push : 86 | ``` 87 | 88 | ### Kubernetes CLI (kubectl) 89 | 90 | Get a list of running nodes in cluster: 91 | ```sh 92 | kubectl get nodes 93 | ``` 94 | 95 | Apply recommended dashboard configuration: 96 | 97 | ```sh 98 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml 99 | ``` 100 | 101 | Create a cluster role binding: 102 | ```sh 103 | kubectl create clusterrolebinding serviceaccounts-cluster-admin \ 104 | --clusterrole=cluster-admin \ 105 | --group=system:serviceaccounts 106 | ``` 107 | 108 | Create an auth token for a user (required to authenticate with the Kubernetes Dashboard: 109 | ```sh 110 | kubectl create token admin-user --duration 4h -n kubernetes-dashboard 111 | ``` 112 | 113 | Start the kubernetes proxy (allows access to Kubernetes dashboard and API): 114 | 115 | ```sh 116 | kubectl proxy 117 | ``` 118 | 119 | > NOTE: The dashboard is accessible via this URL once the proxy is running: [http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/](http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/) 120 | 121 | Apply kubernetes config (requires a `kustomization.yaml` file in the root of the target directory): 122 | ```sh 123 | kubectl apply -k ./path/to/config 124 | ``` 125 | 126 | Execute a command on a running pod (for example, to get shell or create a superuser account with Django) 127 | 128 | ```sh 129 | kubectl exec -it sh 130 | ``` 131 | 132 | 133 | ### Helm 134 | 135 | Install EFS CSI driver in Kubernetes: 136 | 137 | ```sh 138 | helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/ 139 | 140 | helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \ 141 | --namespace kube-system \ 142 | --set image.repository=602401143452.dkr.ecr.eu-west-2.amazonaws.com/eks/aws-efs-csi-driver \ 143 | --set controller.serviceAccount.create=true \ 144 | --set controller.serviceAccount.name=efs-csi-controller-sa \ 145 | --set "controller.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"= 146 | ``` 147 | 148 | > NOTE: The `` comes from the deployed resource in Terraform and can be viewed by running `terraform output efs_csi_sa_role`. 149 | > The `image.repository` value is different for each region and you can find the right one in the [Amazon container image repositories docs page](https://docs.aws.amazon.com/eks/latest/userguide/add-ons-images.html). 150 | 151 | ## Resources 152 | 153 | * [Starting Point](https://github.com/LondonAppDeveloper/aws-django-eks-tutorial-starter) - this is the project we are starting from in the tutorial 154 | * [Terraform .gitignore file template](https://github.com/github/gitignore/blob/main/Terraform.gitignore) 155 | * [Terraform VPC AWS module](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) 156 | * [Terraform RDS AWS module](https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest) 157 | * [Terraform security group AWS module](https://registry.terraform.io/modules/terraform-aws-modules/security-group/aws/latest) 158 | * [Terraform EKS AWS module](https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest) 159 | * [Terraform IAM AWS modules](https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest) 160 | * [Kubernetes docs for Deploying the Dashboard UI](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#deploying-the-dashboard-ui) 161 | * [Local Dashboard Proxy URL](http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/) 162 | * [Docs for installing the Amazon EFS CSI driver](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html) 163 | * [ECR private registry authentication docs](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html) 164 | -------------------------------------------------------------------------------- /app/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LondonAppDeveloper/aws-django-eks-tutorial/a6e91e39126559be0a60434a5eb1bdd3d29c0f16/app/app/__init__.py -------------------------------------------------------------------------------- /app/app/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for app project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /app/app/s3_backends.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom storage backends for S3. 3 | """ 4 | from storages.backends.s3boto3 import S3Boto3Storage 5 | 6 | 7 | class StaticS3Storage(S3Boto3Storage): 8 | location = "static" 9 | default_acl = "public-read" 10 | 11 | 12 | class MediaS3Storage(S3Boto3Storage): 13 | location = "media" 14 | default_acl = "public-read" 15 | -------------------------------------------------------------------------------- /app/app/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | from pathlib import Path 13 | import os 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = os.environ.get('SECRET_KEY', 'changeme') 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = bool(int(os.environ.get('DEBUG', 0))) 27 | 28 | ALLOWED_HOSTS = [] 29 | ALLOWED_HOSTS.extend( 30 | filter( 31 | None, 32 | os.environ.get('ALLOWED_HOSTS', '').split(','), 33 | ) 34 | ) 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'publish', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'app.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'app.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.postgresql', 86 | 'HOST': os.environ.get('DB_HOST'), 87 | 'NAME': os.environ.get('DB_NAME'), 88 | 'USER': os.environ.get('DB_USER'), 89 | 'PASSWORD': os.environ.get('DB_PASS'), 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 127 | 128 | STATIC_URL = '/static/static/' 129 | STATIC_ROOT = '/vol/web/static' 130 | MEDIA_URL = '/static/media/' 131 | MEDIA_ROOT = '/vol/web/media' 132 | 133 | # Default primary key field type 134 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 135 | 136 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 137 | -------------------------------------------------------------------------------- /app/app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample app 4 |

Hello world from Django app.

5 | 6 | 7 | -------------------------------------------------------------------------------- /app/app/urls.py: -------------------------------------------------------------------------------- 1 | """app URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | from app import views 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls), 24 | path('', include('publish.urls')) 25 | ] 26 | 27 | if settings.DEBUG: 28 | urlpatterns += static( 29 | settings.MEDIA_URL, document_root=settings.MEDIA_ROOT 30 | ) 31 | -------------------------------------------------------------------------------- /app/app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | def index(request): 4 | res = f'Request: {request.META}' 5 | return HttpResponse(res) 6 | -------------------------------------------------------------------------------- /app/app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/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', 'app.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 | -------------------------------------------------------------------------------- /app/publish/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LondonAppDeveloper/aws-django-eks-tutorial/a6e91e39126559be0a60434a5eb1bdd3d29c0f16/app/publish/__init__.py -------------------------------------------------------------------------------- /app/publish/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from publish import models 4 | 5 | 6 | admin.site.register(models.Post) 7 | -------------------------------------------------------------------------------- /app/publish/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PublishConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'publish' 7 | -------------------------------------------------------------------------------- /app/publish/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | 3 | from publish.models import Post 4 | 5 | 6 | class PostForm(ModelForm): 7 | class Meta: 8 | model = Post 9 | fields = ['title', 'author_name', 'content', 'image'] 10 | -------------------------------------------------------------------------------- /app/publish/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2023-01-04 08:00 2 | 3 | from django.db import migrations, models 4 | import publish.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Post', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('author_name', models.CharField(max_length=255)), 20 | ('title', models.CharField(max_length=255)), 21 | ('content', models.TextField()), 22 | ('image', models.ImageField(upload_to=publish.models.post_image_path)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /app/publish/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LondonAppDeveloper/aws-django-eks-tutorial/a6e91e39126559be0a60434a5eb1bdd3d29c0f16/app/publish/migrations/__init__.py -------------------------------------------------------------------------------- /app/publish/models.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | import os 3 | 4 | from django.db import models 5 | 6 | 7 | def post_image_path(instance, filename): 8 | """ 9 | Generate path for post image using UUID to ensure uniqueness. 10 | """ 11 | ext = os.path.splitext(filename)[1] 12 | return os.path.join('posts', f'{uuid4()}{ext}') 13 | 14 | 15 | class Post(models.Model): 16 | author_name = models.CharField(max_length=255) 17 | title = models.CharField(max_length=255) 18 | content = models.TextField() 19 | image = models.ImageField(upload_to=post_image_path) 20 | -------------------------------------------------------------------------------- /app/publish/static/publish/style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #c7c7c7; 4 | font-family:Arial, Helvetica, sans-serif; 5 | } 6 | 7 | .page-wrapper { 8 | max-width: 400px; 9 | margin: 0 auto; 10 | } 11 | 12 | .form-wrapper { 13 | border-radius: 5px; 14 | background-color: #e8e8e8; 15 | padding: 20px; 16 | } 17 | 18 | input[type=file] { 19 | width: 100%; 20 | margin: 5px 0; 21 | } 22 | 23 | input[type=text], textarea { 24 | width: 100%; 25 | border: 1px solid #a5a5a5; 26 | border-radius: 3px; 27 | padding: 10px; 28 | box-sizing: border-box; 29 | margin: 5px 0; 30 | } 31 | 32 | label { 33 | margin: 10px 0; 34 | } 35 | 36 | input[type=submit] { 37 | width: 100%; 38 | background-color: rgb(0, 136, 255); 39 | color: #fff; 40 | padding: 15px 20px; 41 | border: none; 42 | border-radius: 4px; 43 | } 44 | 45 | img { 46 | max-width: 400px; 47 | } 48 | -------------------------------------------------------------------------------- /app/publish/templates/publish/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block head %}{% endblock %} 7 | 8 | 9 | 10 |
11 | {% block body %}{% endblock %} 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/publish/templates/publish/index.html: -------------------------------------------------------------------------------- 1 | {% extends "publish/base.html" %} 2 | 3 | {% block head %} 4 | Publish home page 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

Publish home page

9 |
10 |
11 | {% csrf_token %} 12 | {{ form }} 13 | 14 |
15 |
16 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /app/publish/templates/publish/post.html: -------------------------------------------------------------------------------- 1 | {% extends "publish/base.html" %} 2 | 3 | {% block head %} 4 | {{ post.title }} 5 | {% endblock %} 6 | 7 | {% block body %} 8 | Home 9 |

{{ post.title }}

10 | {% if post.image %} 11 | 12 | {% endif %} 13 |

14 | {{ post.content }} 15 |

16 |

17 | Author: {{ post.author_name }} 18 |

19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/publish/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /app/publish/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from publish import views 3 | 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | path('post//', views.view_post, name='view-post') 8 | ] 9 | -------------------------------------------------------------------------------- /app/publish/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.urls import reverse 3 | 4 | from publish.forms import PostForm 5 | from publish.models import Post 6 | 7 | 8 | def index(request): 9 | """ 10 | Show landing page and handle form to publish post. 11 | """ 12 | posts = Post.objects.all() 13 | if request.method == 'POST': 14 | form = PostForm(request.POST, request.FILES) 15 | if form.is_valid(): 16 | post = form.save() 17 | return redirect(reverse('view-post', args=[post.id])) 18 | else: 19 | form = PostForm() 20 | return render( 21 | request, 'publish/index.html', {'form': form, 'posts': posts} 22 | ) 23 | 24 | 25 | def view_post(request, post_id): 26 | """ 27 | Show specific post. 28 | """ 29 | post = Post.objects.get(id=post_id) 30 | return render(request, 'publish/post.html', {'post': post}) 31 | -------------------------------------------------------------------------------- /deploy/dashboard-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: admin-user 5 | namespace: kubernetes-dashboard 6 | -------------------------------------------------------------------------------- /deploy/django.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: django 5 | labels: 6 | app: django 7 | spec: 8 | ports: 9 | - port: 8000 10 | selector: 11 | app: django 12 | tier: app 13 | type: LoadBalancer 14 | 15 | --- 16 | apiVersion: v1 17 | kind: PersistentVolumeClaim 18 | metadata: 19 | name: django-app-pvc 20 | labels: 21 | app: django 22 | spec: 23 | accessModes: 24 | - ReadWriteMany 25 | storageClassName: efs-sc 26 | resources: 27 | requests: 28 | storage: 5Gi 29 | 30 | --- 31 | apiVersion: apps/v1 32 | kind: Deployment 33 | metadata: 34 | name: django 35 | labels: 36 | app: django 37 | spec: 38 | selector: 39 | matchLabels: 40 | app: django 41 | tier: app 42 | strategy: 43 | type: Recreate 44 | template: 45 | metadata: 46 | labels: 47 | app: django 48 | tier: app 49 | spec: 50 | volumes: 51 | - name: django-app-data 52 | persistentVolumeClaim: 53 | claimName: django-app-pvc 54 | containers: 55 | - image: 875086615781.dkr.ecr.eu-west-2.amazonaws.com/django-k8s-app:latest # Change this to your ECR repo for app 56 | name: app 57 | ports: 58 | - containerPort: 8080 59 | name: app 60 | volumeMounts: 61 | - name: django-app-data 62 | mountPath: /vol/web 63 | env: 64 | - name: DB_HOST 65 | value: django-k8s-db.chelb620du2o.eu-west-2.rds.amazonaws.com # Change this to your RDS endpoint 66 | - name: DB_NAME 67 | value: djangoproject 68 | - name: DB_USER 69 | value: djangouser 70 | - name: ALLOWED_HOSTS 71 | value: "a7c9e85ef89a941f68bd8d8917cd4b7d-1489129437.eu-west-2.elb.amazonaws.com" # Change this to your service host 72 | - name: DB_PASS 73 | valueFrom: 74 | secretKeyRef: 75 | name: db-password 76 | key: password 77 | - name: SECRET_KEY 78 | valueFrom: 79 | secretKeyRef: 80 | name: django 81 | key: secret 82 | 83 | - image: 875086615781.dkr.ecr.eu-west-2.amazonaws.com/django-k8s-proxy:latest # Change this to your ECR repo for proxy 84 | name: proxy 85 | ports: 86 | - containerPort: 8000 87 | name: proxy 88 | volumeMounts: 89 | - name: django-app-data 90 | mountPath: /vol/web 91 | env: 92 | - name: APP_HOST 93 | value: "127.0.0.1" 94 | - name: APP_PORT 95 | value: "8080" 96 | -------------------------------------------------------------------------------- /deploy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | secretGenerator: 2 | - name: db-password 3 | literals: 4 | - password=samplepassword123 5 | - name: django 6 | literals: 7 | - secret=secretkey123 8 | resources: 9 | - django.yaml 10 | - storageclass.yaml 11 | -------------------------------------------------------------------------------- /deploy/storageclass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1 2 | kind: StorageClass 3 | metadata: 4 | name: efs-sc 5 | provisioner: efs.csi.aws.com 6 | parameters: 7 | provisioningMode: efs-ap 8 | fileSystemId: fs-0513165627431326d # Change this to your EFS ID 9 | directoryPerms: "755" 10 | basePath: "/dynamic_provisioning" 11 | -------------------------------------------------------------------------------- /docker-compose-deploy.yml: -------------------------------------------------------------------------------- 1 | services: 2 | django: 3 | build: 4 | context: . 5 | restart: always 6 | volumes: 7 | - static-data:/vol/web 8 | environment: 9 | - DB_HOST=db 10 | - DB_NAME=${DB_NAME} 11 | - DB_USER=${DB_USER} 12 | - DB_PASS=${DB_PASS} 13 | - SECRET_KEY=${DJANGO_SECRET_KEY} 14 | - ALLOWED_HOSTS=${DJANGO_ALLOWED_HOSTS} 15 | depends_on: 16 | db: 17 | condition: service_healthy 18 | 19 | db: 20 | image: postgres:13-alpine 21 | restart: always 22 | volumes: 23 | - postgres-data:/var/lib/postgresql/data 24 | environment: 25 | - POSTGRES_DB=${DB_NAME} 26 | - POSTGRES_USER=${DB_USER} 27 | - POSTGRES_PASSWORD=${DB_PASS} 28 | healthcheck: 29 | test: ["CMD", "pg_isready", "-q", "-d", "db", "-U", "dbuser"] 30 | interval: 5s 31 | timeout: 5s 32 | retries: 5 33 | 34 | proxy: 35 | build: 36 | context: ./proxy 37 | restart: always 38 | depends_on: 39 | - django 40 | ports: 41 | - 8080:8000 42 | volumes: 43 | - static-data:/vol/web 44 | environment: 45 | - LISTEN_PORT=8000 46 | - APP_HOST=django 47 | - APP_PORT=8080 48 | 49 | volumes: 50 | postgres-data: 51 | static-data: 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | volumes: 6 | - ./app:/app 7 | - dev-app-data:/vol/web 8 | command: > 9 | sh -c "python manage.py migrate && 10 | python manage.py runserver 0.0.0.0:8000" 11 | environment: 12 | - DEBUG=1 13 | - DB_HOST=database 14 | - DB_NAME=django-dev-db 15 | - DB_USER=devuser 16 | - DB_PASS=devpassword123 17 | 18 | ports: 19 | - 8000:8000 20 | depends_on: 21 | database: 22 | condition: service_healthy 23 | 24 | database: 25 | image: postgres:12-alpine 26 | volumes: 27 | - dev-db-data:/var/lib/postgresql/data 28 | environment: 29 | - POSTGRES_DB=django-dev-db 30 | - POSTGRES_USER=devuser 31 | - POSTGRES_PASSWORD=devpassword123 32 | healthcheck: 33 | test: ["CMD", "pg_isready", "-q", "-d", "django-dev-db", "-U", "devuser"] 34 | interval: 5s 35 | timeout: 5s 36 | retries: 5 37 | 38 | volumes: 39 | dev-db-data: 40 | dev-app-data: 41 | -------------------------------------------------------------------------------- /infra/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.67.0" 6 | constraints = "~> 4.67.0" 7 | hashes = [ 8 | "h1:5Zfo3GfRSWBaXs4TGQNOflr1XaYj6pRnVJLX5VAjFX4=", 9 | "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", 10 | "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", 11 | "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", 12 | "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", 13 | "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", 14 | "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", 15 | "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", 16 | "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", 17 | "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", 18 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 19 | "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", 20 | "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", 21 | "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", 22 | "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", 23 | "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/cloudinit" { 28 | version = "2.3.2" 29 | constraints = ">= 2.0.0" 30 | hashes = [ 31 | "h1:ocyv0lvfyvzW4krenxV5CL4Jq5DiA3EUfoy8DR6zFMw=", 32 | "zh:2487e498736ed90f53de8f66fe2b8c05665b9f8ff1506f751c5ee227c7f457d1", 33 | "zh:3d8627d142942336cf65eea6eb6403692f47e9072ff3fa11c3f774a3b93130b3", 34 | "zh:434b643054aeafb5df28d5529b72acc20c6f5ded24decad73b98657af2b53f4f", 35 | "zh:436aa6c2b07d82aa6a9dd746a3e3a627f72787c27c80552ceda6dc52d01f4b6f", 36 | "zh:458274c5aabe65ef4dbd61d43ce759287788e35a2da004e796373f88edcaa422", 37 | "zh:54bc70fa6fb7da33292ae4d9ceef5398d637c7373e729ed4fce59bd7b8d67372", 38 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 39 | "zh:893ba267e18749c1a956b69be569f0d7bc043a49c3a0eb4d0d09a8e8b2ca3136", 40 | "zh:95493b7517bce116f75cdd4c63b7c82a9d0d48ec2ef2f5eb836d262ef96d0aa7", 41 | "zh:9ae21ab393be52e3e84e5cce0ef20e690d21f6c10ade7d9d9d22b39851bfeddc", 42 | "zh:cc3b01ac2472e6d59358d54d5e4945032efbc8008739a6d4946ca1b621a16040", 43 | "zh:f23bfe9758f06a1ec10ea3a81c9deedf3a7b42963568997d84a5153f35c5839a", 44 | ] 45 | } 46 | 47 | provider "registry.terraform.io/hashicorp/kubernetes" { 48 | version = "2.20.0" 49 | constraints = ">= 2.10.0" 50 | hashes = [ 51 | "h1:E7VAZorKe5oXn6h1nxP3ROwWNiQSrZlTawzix1sh8kM=", 52 | "zh:30bc224c94d2c90a7d44554f2ad30e3b62c7ffc6ddb7d4fd31b9acafb8b5ad77", 53 | "zh:3903cc9f0c3169a24265c4920d925ed7e37cbc4312237b29bd5b4ddcd6bdc535", 54 | "zh:512240f6dad36c0116a8717487a4ea12a6b4191028782c5b6749037892e2c6ed", 55 | "zh:57d5f77dcde7781803b465205aec3507780bfaa77031f5b893ae7cbebd4789b6", 56 | "zh:6274ab8c3b59634c344c337218223640e9d954996b9299587ca924e4dfb77aa4", 57 | "zh:6d838a25f3e3c696cf894f0adb44b41b461a2c76f914f1ae2c318ccbb1ec4e36", 58 | "zh:92f09e3e03311c4e24601b704d85de57677f49e29f42cc3479fafa68f5de300a", 59 | "zh:abb3cd606e485a46c076d6f60d37b5e5ecaa128c0150c8235627b484f2fac902", 60 | "zh:afc07f5c0d7ce2cc907600e4f87a1290203a36221951e19e5d3f1409a0502377", 61 | "zh:d9c01e4f12fabf5d6d9d11ceb409585b71c2abcad478496446de6ff18bbf2f5f", 62 | "zh:f40faba2269184b305f229503945400ed6eeafec7ac395c23f243bccab7b11b2", 63 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 64 | ] 65 | } 66 | 67 | provider "registry.terraform.io/hashicorp/random" { 68 | version = "3.5.1" 69 | constraints = ">= 3.1.0" 70 | hashes = [ 71 | "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=", 72 | "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", 73 | "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", 74 | "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", 75 | "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", 76 | "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", 77 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 78 | "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", 79 | "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", 80 | "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", 81 | "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", 82 | "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", 83 | "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", 84 | ] 85 | } 86 | 87 | provider "registry.terraform.io/hashicorp/time" { 88 | version = "0.9.1" 89 | constraints = ">= 0.9.0" 90 | hashes = [ 91 | "h1:VxyoYYOCaJGDmLz4TruZQTSfQhvwEcMxvcKclWdnpbs=", 92 | "zh:00a1476ecf18c735cc08e27bfa835c33f8ac8fa6fa746b01cd3bcbad8ca84f7f", 93 | "zh:3007f8fc4a4f8614c43e8ef1d4b0c773a5de1dcac50e701d8abc9fdc8fcb6bf5", 94 | "zh:5f79d0730fdec8cb148b277de3f00485eff3e9cf1ff47fb715b1c969e5bbd9d4", 95 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 96 | "zh:8c8094689a2bed4bb597d24a418bbbf846e15507f08be447d0a5acea67c2265a", 97 | "zh:a6d9206e95d5681229429b406bc7a9ba4b2d9b67470bda7df88fa161508ace57", 98 | "zh:aa299ec058f23ebe68976c7581017de50da6204883950de228ed9246f309e7f1", 99 | "zh:b129f00f45fba1991db0aa954a6ba48d90f64a738629119bfb8e9a844b66e80b", 100 | "zh:ef6cecf5f50cda971c1b215847938ced4cb4a30a18095509c068643b14030b00", 101 | "zh:f1f46a4f6c65886d2dd27b66d92632232adc64f92145bf8403fe64d5ffa5caea", 102 | "zh:f79d6155cda7d559c60d74883a24879a01c4d5f6fd7e8d1e3250f3cd215fb904", 103 | "zh:fd59fa73074805c3575f08cd627eef7acda14ab6dac2c135a66e7a38d262201c", 104 | ] 105 | } 106 | 107 | provider "registry.terraform.io/hashicorp/tls" { 108 | version = "4.0.4" 109 | constraints = ">= 3.0.0" 110 | hashes = [ 111 | "h1:GZcFizg5ZT2VrpwvxGBHQ/hO9r6g0vYdQqx3bFD3anY=", 112 | "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55", 113 | "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848", 114 | "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be", 115 | "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5", 116 | "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe", 117 | "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e", 118 | "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48", 119 | "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8", 120 | "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60", 121 | "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e", 122 | "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316", 123 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /infra/ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "app" { 2 | name = "${var.prefix}-app" 3 | image_tag_mutability = "MUTABLE" 4 | force_delete = true 5 | 6 | image_scanning_configuration { 7 | scan_on_push = true 8 | } 9 | } 10 | 11 | resource "aws_ecr_repository" "proxy" { 12 | name = "${var.prefix}-proxy" 13 | image_tag_mutability = "MUTABLE" 14 | force_delete = true 15 | 16 | image_scanning_configuration { 17 | scan_on_push = true 18 | } 19 | } 20 | 21 | resource "aws_iam_policy" "allow_ecr_app" { 22 | name = "${local.cluster_name}-read-ecr-app" 23 | 24 | policy = jsonencode({ 25 | Version = "2012-10-17" 26 | Statement = [ 27 | { 28 | Effect = "Allow" 29 | Action = [ 30 | "ecr:BatchCheckLayerAvailability", 31 | "ecr:BatchGetImage", 32 | "ecr:GetDownloadUrlForLayer", 33 | "ecr:GetAuthorizationToken" 34 | ], 35 | Resource = aws_ecr_repository.app.arn 36 | } 37 | ] 38 | }) 39 | } 40 | 41 | resource "aws_iam_policy" "allow_ecr_proxy" { 42 | name = "${local.cluster_name}-eks-read-ecr-proxy" 43 | 44 | policy = jsonencode({ 45 | Version = "2012-10-17" 46 | Statement = [ 47 | { 48 | Effect = "Allow" 49 | Action = [ 50 | "ecr:BatchCheckLayerAvailability", 51 | "ecr:BatchGetImage", 52 | "ecr:GetDownloadUrlForLayer", 53 | "ecr:GetAuthorizationToken" 54 | ], 55 | Resource = aws_ecr_repository.proxy.arn 56 | } 57 | ] 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /infra/efs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_efs_file_system" "data" { 2 | 3 | creation_token = "${module.eks.cluster_name}-data" 4 | tags = { 5 | Name = "${module.eks.cluster_name}-data" 6 | } 7 | } 8 | 9 | resource "aws_efs_mount_target" "data" { 10 | count = length(module.vpc.private_subnets) 11 | file_system_id = aws_efs_file_system.data.id 12 | subnet_id = module.vpc.private_subnets[count.index] 13 | security_groups = [aws_security_group.allow-efs.id] 14 | } 15 | 16 | resource "aws_security_group" "allow-efs" { 17 | name = "${local.cluster_name}-allow-efs-sg" 18 | description = "Allow EFS access for EKS cluster." 19 | vpc_id = module.vpc.vpc_id 20 | 21 | ingress { 22 | from_port = 2049 23 | to_port = 2049 24 | protocol = "tcp" 25 | cidr_blocks = module.vpc.private_subnets_cidr_blocks 26 | } 27 | 28 | tags = { 29 | Name = "Allow EFS access for ${local.cluster_name}" 30 | } 31 | } 32 | 33 | module "efs_csi_irsa_role" { 34 | source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" 35 | version = "~> 5.19.0" 36 | 37 | role_name_prefix = "${var.prefix}-efs-csi" 38 | attach_efs_csi_policy = true 39 | 40 | oidc_providers = { 41 | one = { 42 | provider_arn = module.eks.oidc_provider_arn 43 | namespace_service_accounts = ["kube-system:efs-csi-controller-sa"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /infra/eks.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | cluster_name = "${var.prefix}-cluster" 3 | } 4 | 5 | module "eks" { 6 | source = "terraform-aws-modules/eks/aws" 7 | version = "~> 19.14" 8 | 9 | cluster_name = local.cluster_name 10 | cluster_version = "1.26" 11 | 12 | vpc_id = module.vpc.vpc_id 13 | subnet_ids = module.vpc.private_subnets 14 | cluster_endpoint_public_access = true 15 | cluster_endpoint_public_access_cidrs = ["0.0.0.0/0"] # Update to your IP 16 | 17 | iam_role_additional_policies = { 18 | AllowECRApp = aws_iam_policy.allow_ecr_app.arn 19 | AllowECRProxy = aws_iam_policy.allow_ecr_proxy.arn 20 | } 21 | 22 | eks_managed_node_group_defaults = { 23 | ami_type = "AL2_x86_64" 24 | } 25 | 26 | eks_managed_node_groups = { 27 | node_group = { 28 | name = "k8s-ng-1" 29 | 30 | instance_types = ["t3.small"] 31 | 32 | min_size = 1 33 | max_size = 3 34 | desired_size = 1 35 | 36 | labels = { 37 | Environment = "Dev" 38 | } 39 | } 40 | } 41 | } 42 | 43 | module "vpc_cni_irsa" { 44 | source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" 45 | version = "~> 5.19" 46 | 47 | role_name_prefix = "${var.prefix}-vpc-cni-irsa" 48 | attach_vpc_cni_policy = true 49 | vpc_cni_enable_ipv4 = true 50 | 51 | oidc_providers = { 52 | main = { 53 | provider_arn = module.eks.oidc_provider_arn 54 | namespace_service_accounts = ["kube-system:aws-node"] 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /infra/main.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 4.67.0" 7 | } 8 | } 9 | required_version = "1.4.6" 10 | } 11 | 12 | provider "aws" { 13 | region = var.region 14 | } 15 | 16 | data "aws_availability_zones" "available" {} 17 | -------------------------------------------------------------------------------- /infra/outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | 3 | output "cluster_name" { 4 | description = "Name of EKS cluster in AWS." 5 | value = module.eks.cluster_name 6 | } 7 | 8 | output "region" { 9 | description = "AWS region" 10 | value = var.region 11 | } 12 | 13 | output "ecr_app_url" { 14 | description = "ECR repo name for app" 15 | value = aws_ecr_repository.app.repository_url 16 | } 17 | 18 | output "ecr_proxy_url" { 19 | description = "ECR repo name for proxy" 20 | value = aws_ecr_repository.proxy.repository_url 21 | } 22 | 23 | output "efs_csi_sa_role" { 24 | value = module.efs_csi_irsa_role.iam_role_arn 25 | } 26 | 27 | output "efs_id" { 28 | value = aws_efs_file_system.data.id 29 | } 30 | 31 | output "db_instance_address" { 32 | description = "The address of the RDS instance" 33 | value = module.db.db_instance_address 34 | } 35 | -------------------------------------------------------------------------------- /infra/rds.tf: -------------------------------------------------------------------------------- 1 | module "db" { 2 | source = "terraform-aws-modules/rds/aws" 3 | version = "~> 5.9.0" 4 | identifier = "${var.prefix}-db" 5 | 6 | engine = "postgres" 7 | engine_version = "14.7" 8 | instance_class = "db.t4g.micro" 9 | family = "postgres14" 10 | 11 | allocated_storage = 5 12 | skip_final_snapshot = true 13 | 14 | db_name = "djangoproject" 15 | username = "djangouser" 16 | create_random_password = false 17 | password = var.db_password 18 | 19 | multi_az = false 20 | db_subnet_group_name = module.vpc.database_subnet_group 21 | vpc_security_group_ids = [module.rds_security_group.security_group_id] 22 | } 23 | 24 | module "rds_security_group" { 25 | source = "terraform-aws-modules/security-group/aws" 26 | version = "~> 4.17.2" 27 | 28 | name = "${var.prefix}-rds-sg" 29 | description = "RDS security group" 30 | vpc_id = module.vpc.vpc_id 31 | 32 | ingress_with_cidr_blocks = [ 33 | { 34 | from_port = 5432 35 | to_port = 5432 36 | protocol = "tcp" 37 | description = "PostgreSQL access from within VPC" 38 | cidr_blocks = module.vpc.vpc_cidr_block 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /infra/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "region" { 3 | description = "AWS region to deploy resources to" 4 | default = "eu-west-2" 5 | } 6 | 7 | variable "prefix" { 8 | description = "Prefix to be assigned to resources." 9 | default = "django-k8s" 10 | } 11 | 12 | variable "db_password" { 13 | description = "Password for the RDS database instance." 14 | default = "samplepassword123" 15 | } 16 | -------------------------------------------------------------------------------- /infra/vpc.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "~> 4.0.1" 4 | 5 | name = "${var.prefix}-vpc" 6 | cidr = "10.0.0.0/16" 7 | azs = slice(data.aws_availability_zones.available.names, 0, 3) 8 | 9 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 10 | public_subnets = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"] 11 | database_subnets = ["10.0.20.0/24", "10.0.21.0/24", "10.0.22.0/24"] 12 | 13 | enable_nat_gateway = true 14 | single_nat_gateway = true 15 | enable_dns_hostnames = true 16 | create_database_subnet_group = true 17 | 18 | public_subnet_tags = { 19 | "kubernetes.io/cluster/${var.prefix}-cluster" = "shared" 20 | "kubernetes.io/role/elb" = 1 21 | } 22 | 23 | private_subnet_tags = { 24 | "kubernetes.io/cluster/${var.prefix}-cluster" = "shared" 25 | "kubernetes.io/role/internal-elb" = 1 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginxinc/nginx-unprivileged:1-alpine 2 | LABEL maintainer="londonappdeveloper.com" 3 | 4 | COPY ./default.conf.tpl /etc/nginx/default.conf.tpl 5 | COPY ./headers.conf /etc/nginx/headers.conf 6 | COPY ./run.sh /run.sh 7 | 8 | ENV LISTEN_PORT=8000 9 | ENV APP_HOST=app 10 | ENV APP_PORT=8080 11 | 12 | USER root 13 | 14 | RUN mkdir -p /vol/static && \ 15 | chmod 755 /vol/static && \ 16 | touch /etc/nginx/conf.d/default.conf && \ 17 | chown nginx:nginx /etc/nginx/conf.d/default.conf && \ 18 | chmod +x /run.sh 19 | 20 | VOLUME /vol/static 21 | 22 | USER nginx 23 | 24 | CMD ["/run.sh"] 25 | -------------------------------------------------------------------------------- /proxy/default.conf.tpl: -------------------------------------------------------------------------------- 1 | server { 2 | listen ${LISTEN_PORT}; 3 | 4 | location /static { 5 | alias /vol/web; 6 | } 7 | 8 | location / { 9 | include /etc/nginx/headers.conf; 10 | 11 | # we don't want nginx trying to do something clever with 12 | # redirects, we set the Host: header above already. 13 | proxy_redirect off; 14 | proxy_pass http://${APP_HOST}:${APP_PORT}; 15 | client_max_body_size 10M; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /proxy/headers.conf: -------------------------------------------------------------------------------- 1 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 2 | proxy_set_header X-Forwarded-Proto $scheme; 3 | proxy_set_header Host $http_host; 4 | -------------------------------------------------------------------------------- /proxy/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | envsubst < /etc/nginx/default.conf.tpl > /etc/nginx/conf.d/default.conf 6 | nginx -g 'daemon off;' 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==4.1.7 2 | gunicorn==20.1.0 3 | psycopg2==2.9.5 4 | Pillow==9.4.0 5 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | python manage.py collectstatic --noinput 5 | python manage.py migrate 6 | gunicorn -b :8080 --chdir /app app.wsgi:application 7 | --------------------------------------------------------------------------------