├── .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 |
6 |
7 |
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 |
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 |
--------------------------------------------------------------------------------