├── config ├── settings │ ├── __init__.py │ ├── test.py │ ├── local.py │ ├── production.py │ └── base.py ├── __init__.py ├── celery_app.py ├── wsgi.py └── urls.py ├── .gitattributes ├── aws_ecs_deploy ├── users │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_models.py │ │ ├── test_tasks.py │ │ ├── test_urls.py │ │ ├── factories.py │ │ ├── test_forms.py │ │ └── test_views.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tasks.py │ ├── apps.py │ ├── urls.py │ ├── models.py │ ├── admin.py │ ├── adapters.py │ ├── forms.py │ └── views.py ├── utils │ ├── __init__.py │ ├── context_processors.py │ └── storages.py ├── static │ ├── fonts │ │ └── .gitkeep │ ├── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss │ ├── js │ │ └── project.js │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ └── css │ │ └── project.css ├── templates │ ├── pages │ │ ├── about.html │ │ └── home.html │ ├── 403.html │ ├── 404.html │ ├── account │ │ ├── base.html │ │ ├── account_inactive.html │ │ ├── password_reset_from_key_done.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ ├── password_set.html │ │ ├── password_reset_done.html │ │ ├── password_change.html │ │ ├── logout.html │ │ ├── signup.html │ │ ├── verified_email_required.html │ │ ├── password_reset.html │ │ ├── email_confirm.html │ │ ├── password_reset_from_key.html │ │ ├── login.html │ │ └── email.html │ ├── 500.html │ ├── users │ │ ├── user_form.html │ │ └── user_detail.html │ └── base.html ├── __init__.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ └── 0001_initial.py └── conftest.py ├── .dockerignore ├── docs ├── __init__.py ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── pytest.ini ├── compose ├── production │ ├── ecs │ │ ├── nginx │ │ │ ├── Dockerfile │ │ │ └── nginx.conf │ │ └── django │ │ │ ├── start │ │ │ ├── entrypoint │ │ │ └── Dockerfile │ └── standalone │ │ ├── postgres │ │ ├── maintenance │ │ │ ├── _sourced │ │ │ │ ├── constants.sh │ │ │ │ ├── yes_no.sh │ │ │ │ ├── countdown.sh │ │ │ │ └── messages.sh │ │ │ ├── backups │ │ │ ├── backup │ │ │ └── restore │ │ └── Dockerfile │ │ ├── django │ │ ├── celery │ │ │ ├── worker │ │ │ │ └── start │ │ │ ├── flower │ │ │ │ └── start │ │ │ └── beat │ │ │ │ └── start │ │ ├── start │ │ ├── entrypoint │ │ └── Dockerfile │ │ ├── traefik │ │ ├── Dockerfile │ │ └── traefik.yml │ │ └── aws │ │ ├── Dockerfile │ │ └── maintenance │ │ ├── download │ │ └── upload └── local │ └── django │ ├── celery │ ├── worker │ │ └── start │ ├── flower │ │ └── start │ └── beat │ │ └── start │ ├── start │ └── Dockerfile ├── locale └── README.rst ├── .pylintrc ├── .envs ├── .local │ ├── .postgres │ └── .django └── .production │ ├── .postgres │ ├── template.env │ └── .django ├── .travis.yml ├── appspec.yaml ├── .pre-commit-config.yaml ├── requirements ├── production.txt ├── base.txt └── local.txt ├── .editorconfig ├── setup.cfg ├── manage.py ├── Dockerfile ├── local.yml ├── .github └── workflows │ ├── nginx.yml │ └── aws.yml ├── production.yml ├── merge_production_dotenvs_in_dotenv.py ├── aws-task-definition.json ├── .gitignore ├── LICENSE └── README.rst /config/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aws_ecs_deploy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aws_ecs_deploy/static/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.coveragerc 3 | !.pylintrc 4 | -------------------------------------------------------------------------------- /aws_ecs_deploy/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /aws_ecs_deploy/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Included so that Django's startproject comment runs against the docs directory 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ds=config.settings.test --reuse-db 3 | python_files = tests.py test_*.py 4 | -------------------------------------------------------------------------------- /compose/production/ecs/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY ./compose/production/ecs/nginx/nginx.conf /etc/nginx/nginx.conf 3 | -------------------------------------------------------------------------------- /compose/local/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery -A config.celery_app worker -l INFO 8 | -------------------------------------------------------------------------------- /aws_ecs_deploy/utils/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def settings_context(_request): 5 | return {"settings": settings} 6 | -------------------------------------------------------------------------------- /locale/README.rst: -------------------------------------------------------------------------------- 1 | Translations 2 | ============ 3 | 4 | Translations will be placed in this folder when running:: 5 | 6 | python manage.py makemessages 7 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /aws_ecs_deploy/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andrew-Chen-Wang/cookiecutter-django-ecs-github/HEAD/aws_ecs_deploy/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /compose/production/standalone/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | celery -A config.celery_app worker -l INFO 9 | -------------------------------------------------------------------------------- /compose/local/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python manage.py migrate 9 | python manage.py runserver_plus 0.0.0.0:8000 10 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery_app import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /aws_ecs_deploy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple( 3 | [ 4 | int(num) if num.isdigit() else num 5 | for num in __version__.replace("-", ".", 1).split(".") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /aws_ecs_deploy/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /aws_ecs_deploy/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /aws_ecs_deploy/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

CSRF verification failed. Request aborted.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

This is not the page you were looking for.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /compose/production/standalone/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python /app/manage.py collectstatic --noinput 9 | 10 | /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app 11 | -------------------------------------------------------------------------------- /compose/local/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery flower \ 8 | --app=config.celery_app \ 9 | --broker="${CELERY_BROKER_URL}" \ 10 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 11 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aws_ecs_deploy.users.models import User 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_user_get_absolute_url(user: User): 9 | assert user.get_absolute_url() == f"/users/{user.username}/" 10 | -------------------------------------------------------------------------------- /compose/production/standalone/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery flower \ 8 | --app=config.celery_app \ 9 | --broker="${CELERY_BROKER_URL}" \ 10 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 11 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11.3 2 | 3 | COPY ./compose/production/standalone/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /compose/production/standalone/traefik/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use this if you do this with plain EC2 2 | 3 | FROM traefik:v2.0 4 | RUN mkdir -p /etc/traefik/acme 5 | RUN touch /etc/traefik/acme/acme.json 6 | RUN chmod 600 /etc/traefik/acme/acme.json 7 | COPY ./compose/production/standalone/traefik/traefik.yml /etc/traefik 8 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tasks.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from config import celery_app 4 | 5 | User = get_user_model() 6 | 7 | 8 | @celery_app.task() 9 | def get_users_count(): 10 | """A pointless Celery task to demonstrate usage.""" 11 | return User.objects.count() 12 | -------------------------------------------------------------------------------- /compose/production/ecs/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | export DJANGO_SETTINGS_MODULE="config.settings.production" 8 | python /app/manage.py collectstatic --noinput 9 | 10 | /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app 11 | -------------------------------------------------------------------------------- /aws_ecs_deploy/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint_django, pylint_celery 3 | 4 | [FORMAT] 5 | max-line-length=120 6 | 7 | [MESSAGES CONTROL] 8 | disable=missing-docstring,invalid-name 9 | 10 | [DESIGN] 11 | max-parents=13 12 | 13 | [TYPECHECK] 14 | generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete 15 | -------------------------------------------------------------------------------- /.envs/.local/.postgres: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | # ------------------------------------------------------------------------------ 3 | POSTGRES_HOST=postgres 4 | POSTGRES_PORT=5432 5 | POSTGRES_DB=aws_ecs_deploy 6 | POSTGRES_USER=ijqMHHwxondYmdEMKIhDpTzNBkuONwKx 7 | POSTGRES_PASSWORD=98Y3d9pH3HTvOCHZ8lQmmOgNuOWWyPxvkYc6HfmfyNGTyOKY6svciDHD5KcGRF6x 8 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% block inner %}{% endblock %} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /aws_ecs_deploy/utils/storages.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class StaticRootS3Boto3Storage(S3Boto3Storage): 5 | location = "static" 6 | default_acl = "public-read" 7 | 8 | 9 | class MediaRootS3Boto3Storage(S3Boto3Storage): 10 | location = "media" 11 | file_overwrite = False 12 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Account Inactive" %}

9 | 10 |

{% trans "This account is inactive." %}

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block inner %} 7 |

{% trans "Change Password" %}

8 |

{% trans 'Your password is now changed.' %}

9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Sign Up Closed" %}

9 | 10 |

{% trans "We are sorry, but the sign up is currently closed." %}

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /aws_ecs_deploy/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aws_ecs_deploy.users.models import User 4 | from aws_ecs_deploy.users.tests.factories import UserFactory 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def media_storage(settings, tmpdir): 9 | settings.MEDIA_ROOT = tmpdir.strpath 10 | 11 | 12 | @pytest.fixture 13 | def user() -> User: 14 | return UserFactory() 15 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Server Error{% endblock %} 4 | 5 | {% block content %} 6 |

Ooops!!! 500

7 | 8 |

Looks like something went wrong!

9 | 10 |

We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.

11 | {% endblock content %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class UsersConfig(AppConfig): 6 | name = "aws_ecs_deploy.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import aws_ecs_deploy.users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /compose/production/standalone/aws/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM garland/aws-cli-docker:1.15.47 2 | 3 | COPY ./compose/production/aws/maintenance /usr/local/bin/maintenance 4 | COPY ./compose/production/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced 5 | 6 | RUN chmod +x /usr/local/bin/maintenance/* 7 | 8 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 9 | && rmdir /usr/local/bin/maintenance 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: xenial 3 | 4 | services: 5 | - docker 6 | 7 | language: python 8 | python: 9 | - "3.7" 10 | 11 | jobs: 12 | include: 13 | - name: "Django Test" 14 | script: 15 | - docker-compose -f local.yml build 16 | - docker-compose -f local.yml up -d 17 | - docker-compose -f local.yml run --rm django pytest 18 | - docker-compose -f local.yml logs 19 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from aws_ecs_deploy.users.views import ( 4 | user_detail_view, 5 | user_redirect_view, 6 | user_update_view, 7 | ) 8 | 9 | app_name = "users" 10 | urlpatterns = [ 11 | path("~redirect/", view=user_redirect_view, name="redirect"), 12 | path("~update/", view=user_update_view, name="update"), 13 | path("/", view=user_detail_view, name="detail"), 14 | ] 15 | -------------------------------------------------------------------------------- /appspec.yaml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | 3 | Resources: 4 | - TargetService: 5 | Type: AWS::ECS::Service 6 | Properties: 7 | # TaskDefinition is just a placeholder, but if you manually deploy, 8 | # then you can follow the Full ARN format below. 9 | # You should NOT show your real account ID. 10 | TaskDefinition: "arn::ecs:::task-definition/application-stack:*" 11 | LoadBalancerInfo: 12 | ContainerName: "nginx" 13 | ContainerPort: 80 14 | -------------------------------------------------------------------------------- /.envs/.production/.postgres: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | # ------------------------------------------------------------------------------ 3 | DATABASE_URL=sqlite:///db.sqlite3 4 | # I've made the database into a SQLite DB 5 | # so you really don't need these. 6 | # Don't worry, I didn't delete the code. 7 | # POSTGRES_HOST=postgres 8 | # POSTGRES_PORT=5432 9 | # POSTGRES_DB=aws_ecs_deploy 10 | # POSTGRES_USER=ijqMHHwxondYmdEMKIhDpTzNBkuONwKx 11 | # POSTGRES_PASSWORD=vXDxfIfAztp2nEhaZvNUgcb4uEwzUzuHG9l7J0Gbu3IaOaqipz37iT7wvZ5vV9nZ 12 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Verify Your E-mail Address" %}

9 | 10 |

{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

11 | 12 | {% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs|node_modules|migrations|.git|.tox' 2 | default_stages: [commit] 3 | fail_fast: true 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: master 8 | hooks: 9 | - id: trailing-whitespace 10 | files: (^|/).+\.(py|html|sh|css|js)$ 11 | 12 | - repo: local 13 | hooks: 14 | - id: flake8 15 | name: flake8 16 | entry: flake8 17 | language: python 18 | types: [python] 19 | args: ['--config=setup.cfg'] 20 | 21 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db.models import CharField 3 | from django.urls import reverse 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class User(AbstractUser): 8 | 9 | # First Name and Last Name do not cover name patterns 10 | # around the globe. 11 | name = CharField(_("Name of User"), blank=True, max_length=255) 12 | 13 | def get_absolute_url(self): 14 | return reverse("users:detail", kwargs={"username": self.username}) 15 | -------------------------------------------------------------------------------- /.envs/.local/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | # Redis 6 | # ------------------------------------------------------------------------------ 7 | REDIS_URL=redis://redis:6379/0 8 | 9 | # Celery 10 | # ------------------------------------------------------------------------------ 11 | 12 | # Flower 13 | CELERY_FLOWER_USER=HZXtuQrSgeqfpgcKODqRaXhLYeXPcZAD 14 | CELERY_FLOWER_PASSWORD=QzXFSJOjQ9bT22pLWePOlQl8dnWhSvKbG72i1qYgbtBinlL54N1RcYmiOP5fPH0d 15 | 16 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/users/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block title %}{{ user.username }}{% endblock %} 5 | 6 | {% block content %} 7 |

{{ user.username }}

8 |
9 | {% csrf_token %} 10 | {{ form|crispy }} 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Set Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Set Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. AWS ECS Deploy documentation master file, created by 2 | sphinx-quickstart. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to AWS ECS Deploy's documentation! 7 | ====================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | pycharm/configuration 14 | 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Password Reset" %}

10 | 11 | {% if user.is_authenticated %} 12 | {% include "account/snippets/already_logged_in.html" %} 13 | {% endif %} 14 | 15 |

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from celery.result import EagerResult 3 | 4 | from aws_ecs_deploy.users.tasks import get_users_count 5 | from aws_ecs_deploy.users.tests.factories import UserFactory 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | def test_user_count(settings): 11 | """A basic test to execute the get_users_count Celery task.""" 12 | UserFactory.create_batch(3) 13 | settings.CELERY_TASK_ALWAYS_EAGER = True 14 | task_result = get_users_count.delay() 15 | assert isinstance(task_result, EagerResult) 16 | assert task_result.result == 3 17 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Change Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import admin as auth_admin 3 | from django.contrib.auth import get_user_model 4 | 5 | from aws_ecs_deploy.users.forms import UserChangeForm, UserCreationForm 6 | 7 | User = get_user_model() 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(auth_admin.UserAdmin): 12 | 13 | form = UserChangeForm 14 | add_form = UserCreationForm 15 | fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets 16 | list_display = ["username", "name", "is_superuser"] 17 | search_fields = ["name"] 18 | -------------------------------------------------------------------------------- /config/celery_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | # set the default Django settings module for the 'celery' program. 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 7 | 8 | app = Celery("aws_ecs_deploy") 9 | 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object("django.conf:settings", namespace="CELERY") 15 | 16 | # Load task modules from all registered Django app configs. 17 | app.autodiscover_tasks() 18 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # PRECAUTION: avoid production dependencies that aren't in development 2 | 3 | -r ./base.txt 4 | 5 | gunicorn==20.0.4 # https://github.com/benoitc/gunicorn 6 | psycopg2==2.8.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 7 | Collectfast==2.1.0 # https://github.com/antonagestam/collectfast 8 | sentry-sdk==0.14.4 # https://github.com/getsentry/sentry-python 9 | 10 | # Django 11 | # ------------------------------------------------------------------------------ 12 | django-storages[boto3]==1.9.1 # https://github.com/jschneier/django-storages 13 | django-anymail[amazon_ses]==7.0.0 # https://github.com/anymail/django-anymail 14 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Out" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Sign Out" %}

9 | 10 |

{% trans 'Are you sure you want to sign out?' %}

11 | 12 |
13 | {% csrf_token %} 14 | {% if redirect_field_value %} 15 | 16 | {% endif %} 17 | 18 |
19 | 20 | 21 | {% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /aws_ecs_deploy/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 5 | from django.conf import settings 6 | from django.http import HttpRequest 7 | 8 | 9 | class AccountAdapter(DefaultAccountAdapter): 10 | def is_open_for_signup(self, request: HttpRequest): 11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 12 | 13 | 14 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): 16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 17 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from aws_ecs_deploy.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_detail(user: User): 10 | assert ( 11 | reverse("users:detail", kwargs={"username": user.username}) 12 | == f"/users/{user.username}/" 13 | ) 14 | assert resolve(f"/users/{user.username}/").view_name == "users:detail" 15 | 16 | 17 | def test_update(): 18 | assert reverse("users:update") == "/users/~update/" 19 | assert resolve("/users/~update/").view_name == "users:update" 20 | 21 | 22 | def test_redirect(): 23 | assert reverse("users:redirect") == "/users/~redirect/" 24 | assert resolve("/users/~redirect/").view_name == "users:redirect" 25 | -------------------------------------------------------------------------------- /aws_ecs_deploy/static/sass/project.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // project specific CSS goes here 6 | 7 | //////////////////////////////// 8 | //Variables// 9 | //////////////////////////////// 10 | 11 | // Alert colors 12 | 13 | $white: #fff; 14 | $mint-green: #d6e9c6; 15 | $black: #000; 16 | $pink: #f2dede; 17 | $dark-pink: #eed3d7; 18 | $red: #b94a48; 19 | 20 | //////////////////////////////// 21 | //Alerts// 22 | //////////////////////////////// 23 | 24 | // bootstrap alert CSS, translated to the django-standard levels of 25 | // debug, info, success, warning, error 26 | 27 | .alert-debug { 28 | background-color: $white; 29 | border-color: $mint-green; 30 | color: $black; 31 | } 32 | 33 | .alert-error { 34 | background-color: $pink; 35 | border-color: $dark-pink; 36 | color: $red; 37 | } 38 | -------------------------------------------------------------------------------- /compose/production/standalone/aws/maintenance/download: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Download a file from your Amazon S3 bucket to the postgres /backups folder 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli <1> 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | aws s3 cp s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH}/${1} ${BACKUP_DIR_PATH}/${1} 22 | 23 | message_success "Finished downloading ${1}." 24 | 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.py] 16 | line_length = 88 17 | known_first_party = aws_ecs_deploy,config 18 | multi_line_output = 3 19 | default_section = THIRDPARTY 20 | recursive = true 21 | skip = venv/ 22 | skip_glob = **/migrations/*.py 23 | include_trailing_comma = true 24 | force_grid_wrap = 0 25 | use_parentheses = true 26 | 27 | [*.{html,css,scss,json,yml}] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | [*.md] 32 | trim_trailing_whitespace = false 33 | 34 | [Makefile] 35 | indent_style = tab 36 | 37 | [nginx.conf] 38 | indent_style = space 39 | indent_size = 2 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules 4 | 5 | [pycodestyle] 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules 8 | 9 | [mypy] 10 | python_version = 3.8 11 | check_untyped_defs = True 12 | ignore_missing_imports = True 13 | warn_unused_ignores = True 14 | warn_redundant_casts = True 15 | warn_unused_configs = True 16 | plugins = mypy_django_plugin.main 17 | 18 | [mypy.plugins.django-stubs] 19 | django_settings_module = config.settings.test 20 | 21 | [mypy-*.migrations.*] 22 | # Django migrations should not produce any errors: 23 | ignore_errors = True 24 | 25 | [coverage:run] 26 | include = aws_ecs_deploy/* 27 | omit = *migrations*, *tests* 28 | plugins = 29 | django_coverage_plugin 30 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Signup" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Sign Up" %}

10 | 11 |

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

12 | 13 | 21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Verify Your E-mail Address" %}

9 | 10 | {% url 'account_email' as email_url %} 11 | 12 |

{% blocktrans %}This part of the site requires us to verify that 13 | you are who you claim to be. For this purpose, we require that you 14 | verify ownership of your e-mail address. {% endblocktrans %}

15 | 16 |

{% blocktrans %}We have sent an e-mail to you for 17 | verification. Please click on the link inside this e-mail. Please 18 | contact us if you do not receive it within a few minutes.{% endblocktrans %}

19 | 20 |

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

21 | 22 | 23 | {% endblock %} 24 | 25 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}User: {{ object.username }}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |
10 |
11 | 12 |

{{ object.username }}

13 | {% if object.name %} 14 |

{{ object.name }}

15 | {% endif %} 16 |
17 |
18 | 19 | {% if object == request.user %} 20 | 21 |
22 | 23 |
24 | My Info 25 | E-Mail 26 | 27 |
28 | 29 |
30 | 31 | {% endif %} 32 | 33 | 34 |
35 | {% endblock content %} 36 | 37 | -------------------------------------------------------------------------------- /compose/production/standalone/aws/maintenance/upload: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Upload the /backups folder to Amazon S3 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli upload 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | message_info "Upload the backups directory to S3 bucket {$AWS_STORAGE_BUCKET_NAME}" 22 | 23 | aws s3 cp ${BACKUP_DIR_PATH} s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH} --recursive 24 | 25 | message_info "Cleaning the directory ${BACKUP_DIR_PATH}" 26 | 27 | rm -rf ${BACKUP_DIR_PATH}/* 28 | 29 | message_success "Finished uploading and cleaning." 30 | 31 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import forms, get_user_model 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserChangeForm(forms.UserChangeForm): 9 | class Meta(forms.UserChangeForm.Meta): 10 | model = User 11 | 12 | 13 | class UserCreationForm(forms.UserCreationForm): 14 | 15 | error_message = forms.UserCreationForm.error_messages.update( 16 | {"duplicate_username": _("This username has already been taken.")} 17 | ) 18 | 19 | class Meta(forms.UserCreationForm.Meta): 20 | model = User 21 | 22 | def clean_username(self): 23 | username = self.cleaned_data["username"] 24 | 25 | try: 26 | User.objects.get(username=username) 27 | except User.DoesNotExist: 28 | return username 29 | 30 | raise ValidationError(self.error_messages["duplicate_username"]) 31 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | from django.contrib.auth import get_user_model 4 | from factory import DjangoModelFactory, Faker, post_generation 5 | 6 | 7 | class UserFactory(DjangoModelFactory): 8 | 9 | username = Faker("user_name") 10 | email = Faker("email") 11 | name = Faker("name") 12 | 13 | @post_generation 14 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 15 | password = ( 16 | extracted 17 | if extracted 18 | else Faker( 19 | "password", 20 | length=42, 21 | special_chars=True, 22 | digits=True, 23 | upper_case=True, 24 | lower_case=True, 25 | ).generate(extra_kwargs={}) 26 | ) 27 | self.set_password(password) 28 | 29 | class Meta: 30 | model = get_user_model() 31 | django_get_or_create = ["username"] 32 | -------------------------------------------------------------------------------- /.envs/.production/template.env: -------------------------------------------------------------------------------- 1 | # These are all the environment variables you should store 2 | # in AWS SSM Parameter Store 3 | 4 | # All variables are String rather than String List for something like ALLOWED_HOSTS. 5 | # All paths have the prefix: /cookiecutter-django/ecs/ 6 | # An example parameter would be: /cookiecutter-django/ecs/DATABASE_URL 7 | 8 | # You can't use SQLite 9 | /cookiecutter-django/ecs/DATABASE_URL= 10 | /cookiecutter-django/ecs/DJANGO_ACCOUNT_ALLOW_REGISTRATION=True 11 | /cookiecutter-django/ecs/DJANGO_ADMIN_URL=gIPs1tFumkSPodIvTkyeYxSvAYbQUhGT/ 12 | /cookiecutter-django/ecs/DJANGO_ALLOWED_HOSTS=.asdfasq.de 13 | # The storage bucket name, not the full url. 14 | /cookiecutter-django/ecs/DJANGO_AWS_STORAGE_BUCKET_NAME= 15 | /cookiecutter-django/ecs/DJANGO_SECRET_KEY=GilqoorvQuWLqaYvJgXjS7ymbcqdCjhiCoKg91y8TDwiHDnkS9O3xYah0wTorwrx 16 | /cookiecutter-django/ecs/DJANGO_SECURE_SSL_REDIRECT=False 17 | /cookiecutter-django/ecs/WEB_CONCURRENCY=4 18 | /cookiecutter-django/ecs/SENTRY_DSN= 19 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% trans "Password Reset" %}

12 | {% if user.is_authenticated %} 13 | {% include "account/snippets/already_logged_in.html" %} 14 | {% endif %} 15 | 16 |

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

17 | 18 |
19 | {% csrf_token %} 20 | {{ form|crispy }} 21 | 22 |
23 | 24 |

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | pytz==2019.3 # https://github.com/stub42/pytz 2 | python-slugify==4.0.0 # https://github.com/un33k/python-slugify 3 | Pillow==7.1.1 # https://github.com/python-pillow/Pillow 4 | argon2-cffi==19.2.0 # https://github.com/hynek/argon2_cffi 5 | redis==3.4.1 # https://github.com/andymccurdy/redis-py 6 | celery==4.4.2 # pyup: < 5.0 # https://github.com/celery/celery 7 | django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat 8 | flower==0.9.4 # https://github.com/mher/flower 9 | 10 | # Django 11 | # ------------------------------------------------------------------------------ 12 | django==3.0.5 # pyup: < 3.1 # https://www.djangoproject.com/ 13 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 14 | django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils 15 | django-allauth==0.41.0 # https://github.com/pennersr/django-allauth 16 | django-crispy-forms==1.9.0 # https://github.com/django-crispy-forms/django-crispy-forms 17 | django-redis==4.11.0 # https://github.com/niwinz/django-redis 18 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 7 | 8 | 9 | {% block inner %} 10 |

{% trans "Confirm E-mail Address" %}

11 | 12 | {% if confirmation %} 13 | 14 | {% user_display confirmation.email_address.user as user_display %} 15 | 16 |

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | {% else %} 24 | 25 | {% url 'account_email' as email_url %} 26 | 27 |

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

28 | 29 | {% endif %} 30 | 31 | {% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

9 | 10 | {% if token_fail %} 11 | {% url 'account_reset_password' as passwd_reset_url %} 12 |

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

13 | {% else %} 14 | {% if form %} 15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 |
20 | {% else %} 21 |

{% trans 'Your password is now changed.' %}

22 | {% endif %} 23 | {% endif %} 24 | {% endblock %} 25 | 26 | -------------------------------------------------------------------------------- /compose/production/standalone/django/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | 9 | # N.B. If only .env files supported variable expansion... 10 | export CELERY_BROKER_URL="${REDIS_URL}" 11 | 12 | 13 | if [ -z "${POSTGRES_USER}" ]; then 14 | base_postgres_image_default_user='postgres' 15 | export POSTGRES_USER="${base_postgres_image_default_user}" 16 | fi 17 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 18 | 19 | postgres_ready() { 20 | python << END 21 | import sys 22 | 23 | import psycopg2 24 | 25 | try: 26 | psycopg2.connect( 27 | dbname="${POSTGRES_DB}", 28 | user="${POSTGRES_USER}", 29 | password="${POSTGRES_PASSWORD}", 30 | host="${POSTGRES_HOST}", 31 | port="${POSTGRES_PORT}", 32 | ) 33 | except psycopg2.OperationalError: 34 | sys.exit(-1) 35 | sys.exit(0) 36 | 37 | END 38 | } 39 | until postgres_ready; do 40 | >&2 echo 'Waiting for PostgreSQL to become available...' 41 | sleep 1 42 | done 43 | >&2 echo 'PostgreSQL is available' 44 | 45 | exec "$@" 46 | -------------------------------------------------------------------------------- /compose/local/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | rm -f './celerybeat.pid' 8 | 9 | postgres_ready() { 10 | python << END 11 | import sys 12 | from time import sleep 13 | 14 | import psycopg2 15 | 16 | try: 17 | conn = psycopg2.connect( 18 | dbname="${POSTGRES_DB}", 19 | user="${POSTGRES_USER}", 20 | password="${POSTGRES_PASSWORD}", 21 | host="${POSTGRES_HOST}", 22 | port="${POSTGRES_PORT}", 23 | ) 24 | 25 | # Check if table exists yet. 26 | # If not, wait for docker-compose up to migrate all tables. 27 | cur = conn.cursor() 28 | cur.execute( 29 | "select exists(select * from ${POSTGRES_DB}.tables where table_name=%s)", 30 | ('django_celery_beat_periodictask',) 31 | ) 32 | 33 | except (psycopg2.OperationalError, psycopg2.errors.UndefinedTable): 34 | sys.exit(-1) 35 | 36 | sys.exit(0) 37 | 38 | END 39 | } 40 | until postgres_ready; do 41 | >&2 echo 'Waiting for celerybeat models to be migrated...' 42 | sleep 1 43 | done 44 | >&2 echo 'PostgreSQL is ready' 45 | 46 | celery -A config.celery_app beat -l INFO 47 | -------------------------------------------------------------------------------- /compose/production/standalone/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | postgres_ready() { 9 | python << END 10 | import sys 11 | from time import sleep 12 | 13 | import psycopg2 14 | 15 | try: 16 | conn = psycopg2.connect( 17 | dbname="${POSTGRES_DB}", 18 | user="${POSTGRES_USER}", 19 | password="${POSTGRES_PASSWORD}", 20 | host="${POSTGRES_HOST}", 21 | port="${POSTGRES_PORT}", 22 | ) 23 | 24 | # Check if table exists yet. 25 | # If not, wait for docker-compose up to migrate all tables. 26 | cur = conn.cursor() 27 | cur.execute( 28 | "select exists(select * from ${POSTGRES_DB}.tables where table_name=%s)", 29 | ('django_celery_beat_periodictask',) 30 | ) 31 | 32 | except (psycopg2.OperationalError, psycopg2.errors.UndefinedTable): 33 | sys.exit(-1) 34 | 35 | sys.exit(0) 36 | 37 | END 38 | } 39 | until postgres_ready; do 40 | >&2 echo 'Waiting for celerybeat models to be migrated...' 41 | sleep 1 42 | done 43 | >&2 echo 'PostgreSQL is ready' 44 | 45 | 46 | celery -A config.celery_app beat -l INFO 47 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 8 | 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | 24 | raise 25 | 26 | # This allows easy placement of apps within the interior 27 | # aws_ecs_deploy directory. 28 | current_path = Path(__file__).parent.resolve() 29 | sys.path.append(str(current_path / "aws_ecs_deploy")) 30 | 31 | execute_from_command_line(sys.argv) 32 | -------------------------------------------------------------------------------- /compose/production/ecs/django/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | 9 | # N.B. If only .env files supported variable expansion... 10 | # export CELERY_BROKER_URL="${REDIS_URL}" 11 | # 12 | # 13 | # if [ -z "${POSTGRES_USER}" ]; then 14 | # base_postgres_image_default_user='postgres' 15 | # export POSTGRES_USER="${base_postgres_image_default_user}" 16 | # fi 17 | # export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 18 | # 19 | # postgres_ready() { 20 | # python << END 21 | # import sys 22 | # 23 | # import psycopg2 24 | # 25 | # try: 26 | # psycopg2.connect( 27 | # dbname="${POSTGRES_DB}", 28 | # user="${POSTGRES_USER}", 29 | # password="${POSTGRES_PASSWORD}", 30 | # host="${POSTGRES_HOST}", 31 | # port="${POSTGRES_PORT}", 32 | # ) 33 | # except psycopg2.OperationalError: 34 | # sys.exit(-1) 35 | # sys.exit(0) 36 | # 37 | # END 38 | # } 39 | # until postgres_ready; do 40 | # >&2 echo 'Waiting for PostgreSQL to become available...' 41 | # sleep 1 42 | # done 43 | # >&2 echo 'PostgreSQL is available' 44 | # 45 | exec "$@" 46 | -------------------------------------------------------------------------------- /aws_ecs_deploy/contrib/sites/migrations/0003_set_site_domain_and_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | from django.conf import settings 7 | from django.db import migrations 8 | 9 | 10 | def update_site_forward(apps, schema_editor): 11 | """Set site domain and name.""" 12 | Site = apps.get_model("sites", "Site") 13 | Site.objects.update_or_create( 14 | id=settings.SITE_ID, 15 | defaults={ 16 | "domain": "example.com", 17 | "name": "AWS ECS Deploy", 18 | }, 19 | ) 20 | 21 | 22 | def update_site_backward(apps, schema_editor): 23 | """Revert site domain and name to default.""" 24 | Site = apps.get_model("sites", "Site") 25 | Site.objects.update_or_create( 26 | id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} 27 | ) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | 32 | dependencies = [("sites", "0002_alter_domain_unique")] 33 | 34 | operations = [migrations.RunPython(update_site_forward, update_site_backward)] 35 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 21 | 22 | 23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 25 | exit 1 26 | fi 27 | 28 | export PGHOST="${POSTGRES_HOST}" 29 | export PGPORT="${POSTGRES_PORT}" 30 | export PGUSER="${POSTGRES_USER}" 31 | export PGPASSWORD="${POSTGRES_PASSWORD}" 32 | export PGDATABASE="${POSTGRES_DB}" 33 | 34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 36 | 37 | 38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 39 | -------------------------------------------------------------------------------- /compose/production/ecs/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN apt-get update \ 6 | # dependencies for building Python packages 7 | && apt-get install -y build-essential \ 8 | # psycopg2 dependencies 9 | && apt-get install -y libpq-dev \ 10 | # Translations dependencies 11 | && apt-get install -y gettext \ 12 | # cleaning up unused files 13 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | RUN addgroup --system django \ 17 | && adduser --system --ingroup django django 18 | 19 | # Requirements are installed here to ensure they will be cached. 20 | COPY ./requirements /requirements 21 | RUN pip install --no-cache-dir -r /requirements/production.txt \ 22 | && rm -rf /requirements 23 | 24 | COPY ./compose/production/ecs/django/entrypoint /entrypoint 25 | RUN sed -i 's/\r$//g' /entrypoint 26 | RUN chmod +x /entrypoint 27 | RUN chown django /entrypoint 28 | 29 | COPY ./compose/production/ecs/django/start /start 30 | RUN sed -i 's/\r$//g' /start 31 | RUN chmod +x /start 32 | RUN chown django /start 33 | 34 | COPY --chown=django:django . /app 35 | 36 | USER django 37 | 38 | WORKDIR /app 39 | 40 | ENTRYPOINT ["/entrypoint"] 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Django Dockerfile for the entire app 2 | FROM python:3.8-slim-buster 3 | 4 | ENV PYTHONUNBUFFERED 1 5 | EXPOSE 5000 6 | 7 | RUN apt-get update \ 8 | # dependencies for building Python packages 9 | && apt-get install -y build-essential \ 10 | # psycopg2 dependencies 11 | && apt-get install -y libpq-dev \ 12 | # Translations dependencies 13 | && apt-get install -y gettext \ 14 | # cleaning up unused files 15 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | RUN addgroup --system django \ 19 | && adduser --system --ingroup django django 20 | 21 | # Requirements are installed here to ensure they will be cached. 22 | COPY ./requirements /requirements 23 | RUN pip install --no-cache-dir -r /requirements/production.txt \ 24 | && rm -rf /requirements 25 | 26 | COPY ./compose/production/ecs/django/entrypoint /entrypoint 27 | RUN sed -i 's/\r$//g' /entrypoint 28 | RUN chmod +x /entrypoint 29 | RUN chown django /entrypoint 30 | 31 | COPY ./compose/production/ecs/django/start /start 32 | RUN sed -i 's/\r$//g' /start 33 | RUN chmod +x /start 34 | RUN chown django /start 35 | 36 | USER django 37 | 38 | WORKDIR /app 39 | 40 | ADD . /app 41 | ENTRYPOINT ["/entrypoint"] 42 | CMD ["/start"] 43 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aws_ecs_deploy.users.forms import UserCreationForm 4 | from aws_ecs_deploy.users.tests.factories import UserFactory 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | class TestUserCreationForm: 10 | def test_clean_username(self): 11 | # A user with proto_user params does not exist yet. 12 | proto_user = UserFactory.build() 13 | 14 | form = UserCreationForm( 15 | { 16 | "username": proto_user.username, 17 | "password1": proto_user._password, 18 | "password2": proto_user._password, 19 | } 20 | ) 21 | 22 | assert form.is_valid() 23 | assert form.clean_username() == proto_user.username 24 | 25 | # Creating a user. 26 | form.save() 27 | 28 | # The user with proto_user params already exists, 29 | # hence cannot be created. 30 | form = UserCreationForm( 31 | { 32 | "username": proto_user.username, 33 | "password1": proto_user._password, 34 | "password2": proto_user._password, 35 | } 36 | ) 37 | 38 | assert not form.is_valid() 39 | assert len(form.errors) == 1 40 | assert "username" in form.errors 41 | -------------------------------------------------------------------------------- /compose/production/ecs/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 768; 3 | } 4 | 5 | http { 6 | # Nginx will handle gzip compression of responses from the app server 7 | gzip on; 8 | gzip_proxied any; 9 | gzip_types text/plain application/json; 10 | gzip_min_length 1000; 11 | 12 | server { 13 | set $my_host $host; 14 | if ($host ~ "\d+\.\d+\.\d+\.\d+") { 15 | set $my_host "asdfasq.de"; 16 | } 17 | 18 | listen 80; 19 | server_name www.asdfasq.de asdfasq.de; 20 | charset utf-8; 21 | error_log /dev/stdout info; 22 | access_log /dev/stdout; 23 | 24 | location / { 25 | # Reject requests with unsupported HTTP method 26 | if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) { 27 | return 405; 28 | } 29 | 30 | # Only requests matching the whitelist expectations will 31 | # get sent to the application server 32 | proxy_pass http://django-app:5000; 33 | proxy_http_version 1.1; 34 | proxy_set_header Upgrade $http_upgrade; 35 | proxy_set_header Connection 'upgrade'; 36 | proxy_set_header Host $host; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_cache_bypass $http_upgrade; 39 | } 40 | 41 | if ($http_x_forwarded_proto != 'https') { 42 | return 301 https://$my_host$request_uri; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /compose/local/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | 6 | RUN apt-get update \ 7 | # dependencies for building Python packages 8 | && apt-get install -y build-essential \ 9 | # psycopg2 dependencies 10 | && apt-get install -y libpq-dev \ 11 | # Translations dependencies 12 | && apt-get install -y gettext \ 13 | # cleaning up unused files 14 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | # Requirements are installed here to ensure they will be cached. 18 | COPY ./requirements /requirements 19 | RUN pip install -r /requirements/local.txt 20 | 21 | COPY ./compose/production/ecs/django/entrypoint /entrypoint 22 | RUN sed -i 's/\r$//g' /entrypoint 23 | RUN chmod +x /entrypoint 24 | 25 | COPY ./compose/local/django/start /start 26 | RUN sed -i 's/\r$//g' /start 27 | RUN chmod +x /start 28 | 29 | COPY ./compose/local/django/celery/worker/start /start-celeryworker 30 | RUN sed -i 's/\r$//g' /start-celeryworker 31 | RUN chmod +x /start-celeryworker 32 | 33 | COPY ./compose/local/django/celery/beat/start /start-celerybeat 34 | RUN sed -i 's/\r$//g' /start-celerybeat 35 | RUN chmod +x /start-celerybeat 36 | 37 | COPY ./compose/local/django/celery/flower/start /start-flower 38 | RUN sed -i 's/\r$//g' /start-flower 39 | RUN chmod +x /start-flower 40 | 41 | WORKDIR /app 42 | 43 | ENTRYPOINT ["/entrypoint"] 44 | -------------------------------------------------------------------------------- /local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | local_postgres_data: {} 5 | local_postgres_data_backups: {} 6 | 7 | services: 8 | django: &django 9 | build: 10 | context: . 11 | dockerfile: ./compose/local/django/Dockerfile 12 | image: aws_ecs_deploy_local_django 13 | depends_on: 14 | - postgres 15 | volumes: 16 | - .:/app 17 | env_file: 18 | - ./.envs/.local/.django 19 | - ./.envs/.local/.postgres 20 | ports: 21 | - "8000:8000" 22 | command: /start 23 | 24 | postgres: 25 | build: 26 | context: . 27 | dockerfile: ./compose/production/standalone/postgres/Dockerfile 28 | image: aws_ecs_deploy_production_postgres 29 | volumes: 30 | - local_postgres_data:/var/lib/postgresql/data 31 | - local_postgres_data_backups:/backups 32 | env_file: 33 | - ./.envs/.local/.postgres 34 | 35 | redis: 36 | image: redis:5.0 37 | 38 | celeryworker: 39 | <<: *django 40 | image: aws_ecs_deploy_local_celeryworker 41 | depends_on: 42 | - redis 43 | - postgres 44 | 45 | ports: [] 46 | command: /start-celeryworker 47 | 48 | celerybeat: 49 | <<: *django 50 | image: aws_ecs_deploy_local_celerybeat 51 | depends_on: 52 | - redis 53 | - postgres 54 | 55 | ports: [] 56 | command: /start-celerybeat 57 | 58 | flower: 59 | <<: *django 60 | image: aws_ecs_deploy_local_flower 61 | ports: 62 | - "5555:5555" 63 | command: /start-flower 64 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.mixins import LoginRequiredMixin 4 | from django.urls import reverse 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.views.generic import DetailView, RedirectView, UpdateView 7 | 8 | User = get_user_model() 9 | 10 | 11 | class UserDetailView(LoginRequiredMixin, DetailView): 12 | 13 | model = User 14 | slug_field = "username" 15 | slug_url_kwarg = "username" 16 | 17 | 18 | user_detail_view = UserDetailView.as_view() 19 | 20 | 21 | class UserUpdateView(LoginRequiredMixin, UpdateView): 22 | 23 | model = User 24 | fields = ["name"] 25 | 26 | def get_success_url(self): 27 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 28 | 29 | def get_object(self): 30 | return User.objects.get(username=self.request.user.username) 31 | 32 | def form_valid(self, form): 33 | messages.add_message( 34 | self.request, messages.INFO, _("Infos successfully updated") 35 | ) 36 | return super().form_valid(form) 37 | 38 | 39 | user_update_view = UserUpdateView.as_view() 40 | 41 | 42 | class UserRedirectView(LoginRequiredMixin, RedirectView): 43 | 44 | permanent = False 45 | 46 | def get_redirect_url(self): 47 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 48 | 49 | 50 | user_redirect_view = UserRedirectView.as_view() 51 | -------------------------------------------------------------------------------- /aws_ecs_deploy/contrib/sites/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.contrib.sites.models import _simple_domain_name_validator 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Site", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ( 24 | "domain", 25 | models.CharField( 26 | max_length=100, 27 | verbose_name="domain name", 28 | validators=[_simple_domain_name_validator], 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50, verbose_name="display name")), 32 | ], 33 | options={ 34 | "ordering": ("domain",), 35 | "db_table": "django_site", 36 | "verbose_name": "site", 37 | "verbose_name_plural": "sites", 38 | }, 39 | bases=(models.Model,), 40 | managers=[("objects", django.contrib.sites.models.SiteManager())], 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import RequestFactory 3 | 4 | from aws_ecs_deploy.users.models import User 5 | from aws_ecs_deploy.users.views import UserRedirectView, UserUpdateView 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | class TestUserUpdateView: 11 | """ 12 | TODO: 13 | extracting view initialization code as class-scoped fixture 14 | would be great if only pytest-django supported non-function-scoped 15 | fixture db access -- this is a work-in-progress for now: 16 | https://github.com/pytest-dev/pytest-django/pull/258 17 | """ 18 | 19 | def test_get_success_url(self, user: User, rf: RequestFactory): 20 | view = UserUpdateView() 21 | request = rf.get("/fake-url/") 22 | request.user = user 23 | 24 | view.request = request 25 | 26 | assert view.get_success_url() == f"/users/{user.username}/" 27 | 28 | def test_get_object(self, user: User, rf: RequestFactory): 29 | view = UserUpdateView() 30 | request = rf.get("/fake-url/") 31 | request.user = user 32 | 33 | view.request = request 34 | 35 | assert view.get_object() == user 36 | 37 | 38 | class TestUserRedirectView: 39 | def test_get_redirect_url(self, user: User, rf: RequestFactory): 40 | view = UserRedirectView() 41 | request = rf.get("/fake-url") 42 | request.user = user 43 | 44 | view.request = request 45 | 46 | assert view.get_redirect_url() == f"/users/{user.username}/" 47 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r ./base.txt 2 | 3 | Werkzeug==1.0.1 # https://github.com/pallets/werkzeug 4 | ipdb==0.13.2 # https://github.com/gotcha/ipdb 5 | Sphinx==3.0.1 # https://github.com/sphinx-doc/sphinx 6 | psycopg2==2.8.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 7 | 8 | # Testing 9 | # ------------------------------------------------------------------------------ 10 | mypy==0.770 # https://github.com/python/mypy 11 | django-stubs==1.5.0 # https://github.com/typeddjango/django-stubs 12 | pytest==5.3.5 # https://github.com/pytest-dev/pytest 13 | pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar 14 | 15 | # Code quality 16 | # ------------------------------------------------------------------------------ 17 | flake8==3.7.9 # https://github.com/PyCQA/flake8 18 | flake8-isort==2.9.1 # https://github.com/gforcada/flake8-isort 19 | coverage==5.1 # https://github.com/nedbat/coveragepy 20 | black==19.10b0 # https://github.com/ambv/black 21 | pylint-django==2.0.14 # https://github.com/PyCQA/pylint-django 22 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 23 | pre-commit==2.2.0 # https://github.com/pre-commit/pre-commit 24 | 25 | # Django 26 | # ------------------------------------------------------------------------------ 27 | factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy 28 | 29 | django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar 30 | django-extensions==2.2.9 # https://github.com/django-extensions/django-extensions 31 | django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin 32 | pytest-django==3.9.0 # https://github.com/pytest-dev/pytest-django 33 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account socialaccount %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Sign In" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% trans "Sign In" %}

12 | 13 | {% get_providers as socialaccount_providers %} 14 | 15 | {% if socialaccount_providers %} 16 |

{% blocktrans with site.name as site_name %}Please sign in with one 17 | of your existing third party accounts. Or, sign up 18 | for a {{ site_name }} account and sign in below:{% endblocktrans %}

19 | 20 |
21 | 22 |
    23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %} 24 |
25 | 26 | 27 | 28 |
29 | 30 | {% include "socialaccount/snippets/login_extra.html" %} 31 | 32 | {% else %} 33 |

{% blocktrans %}If you have not created an account yet, then please 34 | sign up first.{% endblocktrans %}

35 | {% endif %} 36 | 37 | 46 | 47 | {% endblock %} 48 | 49 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for AWS ECS Deploy project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | import sys 18 | from pathlib import Path 19 | 20 | from django.core.wsgi import get_wsgi_application 21 | 22 | # This allows easy placement of apps within the interior 23 | # aws_ecs_deploy directory. 24 | app_path = Path(__file__).parents[1].resolve() 25 | sys.path.append(str(app_path / "aws_ecs_deploy")) 26 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 27 | # if running multiple sites in the same mod_wsgi process. To fix this, use 28 | # mod_wsgi daemon mode with each site in its own daemon process, or use 29 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" 30 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") 31 | 32 | # This application object is used by any WSGI server configured to use this 33 | # file. This includes Django's development server, if the WSGI_APPLICATION 34 | # setting points here. 35 | application = get_wsgi_application() 36 | # Apply WSGI middleware here. 37 | # from helloworld.wsgi import HelloWorldApplication 38 | # application = HelloWorldApplication(application) 39 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from django.views import defaults as default_views 6 | from django.views.generic import TemplateView 7 | 8 | urlpatterns = [ 9 | path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), 10 | path( 11 | "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 12 | ), 13 | # Django Admin, use {% url 'admin:index' %} 14 | path(settings.ADMIN_URL, admin.site.urls), 15 | # User management 16 | path("users/", include("aws_ecs_deploy.users.urls", namespace="users")), 17 | path("accounts/", include("allauth.urls")), 18 | # Your stuff: custom urls includes go here 19 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 20 | 21 | 22 | if settings.DEBUG: 23 | # This allows the error pages to be debugged during development, just visit 24 | # these url in browser to see how these error pages look like. 25 | urlpatterns += [ 26 | path( 27 | "400/", 28 | default_views.bad_request, 29 | kwargs={"exception": Exception("Bad Request!")}, 30 | ), 31 | path( 32 | "403/", 33 | default_views.permission_denied, 34 | kwargs={"exception": Exception("Permission Denied")}, 35 | ), 36 | path( 37 | "404/", 38 | default_views.page_not_found, 39 | kwargs={"exception": Exception("Page not Found")}, 40 | ), 41 | path("500/", default_views.server_error), 42 | ] 43 | if "debug_toolbar" in settings.INSTALLED_APPS: 44 | import debug_toolbar 45 | 46 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 47 | -------------------------------------------------------------------------------- /.github/workflows/nginx.yml: -------------------------------------------------------------------------------- 1 | # Build and push the nginx image 2 | # 3 | # Since my ECR free tier was at stake, and since 4 | # nginx takes a relatively short amount of time to 5 | # build anyways, I decided to move the build and 6 | # push to separate file since nginx configuration 7 | # doesn't change too often. 8 | 9 | name: Build and Push NGINX Image 10 | 11 | on: 12 | push: 13 | branches: 14 | - master 15 | paths: 16 | # Trigger this workflow on master push 17 | - 'compose/production/ecs/nginx/**' 18 | - '.github/workflows/nginx.yml' 19 | 20 | jobs: 21 | build-nginx: 22 | name: Build NGINX Image 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | 29 | - name: Configure AWS credentials 30 | uses: aws-actions/configure-aws-credentials@v1 31 | with: 32 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 33 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 34 | # TODO Change your AWS region here! 35 | aws-region: us-east-2 36 | 37 | - name: Login to Amazon ECR 38 | id: login-ecr 39 | uses: aws-actions/amazon-ecr-login@v1 40 | 41 | # Build nginx 42 | # ------------------------------------------------------- 43 | - name: Build, tag, and push nginx image to Amazon ECR 44 | id: build-nginx-image 45 | env: 46 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 47 | ECR_REPOSITORY: nginx-reverse-proxy 48 | IMAGE_TAG: latest 49 | run: | 50 | # Build a docker container and 51 | # push it to ECR so that it can 52 | # be deployed to ECS. 53 | docker build -f compose/production/ecs/nginx/Dockerfile -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 54 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 55 | -------------------------------------------------------------------------------- /compose/production/standalone/django/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM python:3.8-slim-buster 3 | 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | RUN apt-get update \ 7 | # dependencies for building Python packages 8 | && apt-get install -y build-essential \ 9 | # psycopg2 dependencies 10 | && apt-get install -y libpq-dev \ 11 | # Translations dependencies 12 | && apt-get install -y gettext \ 13 | # cleaning up unused files 14 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | RUN addgroup --system django \ 18 | && adduser --system --ingroup django django 19 | 20 | # Requirements are installed here to ensure they will be cached. 21 | COPY ./requirements /requirements 22 | RUN pip install --no-cache-dir -r /requirements/production.txt \ 23 | && rm -rf /requirements 24 | 25 | COPY ./compose/production/standalone/django/entrypoint /entrypoint 26 | RUN sed -i 's/\r$//g' /entrypoint 27 | RUN chmod +x /entrypoint 28 | RUN chown django /entrypoint 29 | 30 | COPY ./compose/production/standalone/django/start /start 31 | RUN sed -i 's/\r$//g' /start 32 | RUN chmod +x /start 33 | RUN chown django /start 34 | COPY ./compose/production/standalone/django/celery/worker/start /start-celeryworker 35 | RUN sed -i 's/\r$//g' /start-celeryworker 36 | RUN chmod +x /start-celeryworker 37 | RUN chown django /start-celeryworker 38 | 39 | COPY ./compose/production/standalone/django/celery/beat/start /start-celerybeat 40 | RUN sed -i 's/\r$//g' /start-celerybeat 41 | RUN chmod +x /start-celerybeat 42 | RUN chown django /start-celerybeat 43 | 44 | COPY ./compose/production/standalone/django/celery/flower/start /start-flower 45 | RUN sed -i 's/\r$//g' /start-flower 46 | RUN chmod +x /start-flower 47 | COPY --chown=django:django . /app 48 | 49 | USER django 50 | 51 | WORKDIR /app 52 | 53 | ENTRYPOINT ["/entrypoint"] 54 | -------------------------------------------------------------------------------- /compose/production/standalone/postgres/maintenance/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Restore database from a backup. 5 | ### 6 | ### Parameters: 7 | ### <1> filename of an existing backup. 8 | ### 9 | ### Usage: 10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> 11 | 12 | 13 | set -o errexit 14 | set -o pipefail 15 | set -o nounset 16 | 17 | 18 | working_dir="$(dirname ${0})" 19 | source "${working_dir}/_sourced/constants.sh" 20 | source "${working_dir}/_sourced/messages.sh" 21 | 22 | 23 | if [[ -z ${1+x} ]]; then 24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 25 | exit 1 26 | fi 27 | backup_filename="${BACKUP_DIR_PATH}/${1}" 28 | if [[ ! -f "${backup_filename}" ]]; then 29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." 30 | exit 1 31 | fi 32 | 33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." 34 | 35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 37 | exit 1 38 | fi 39 | 40 | export PGHOST="${POSTGRES_HOST}" 41 | export PGPORT="${POSTGRES_PORT}" 42 | export PGUSER="${POSTGRES_USER}" 43 | export PGPASSWORD="${POSTGRES_PASSWORD}" 44 | export PGDATABASE="${POSTGRES_DB}" 45 | 46 | message_info "Dropping the database..." 47 | dropdb "${PGDATABASE}" 48 | 49 | message_info "Creating a new database..." 50 | createdb --owner="${POSTGRES_USER}" 51 | 52 | message_info "Applying the backup to the new database..." 53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" 54 | 55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." 56 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | 5 | from .base import * # noqa 6 | from .base import env 7 | 8 | # GENERAL 9 | # ------------------------------------------------------------------------------ 10 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 11 | SECRET_KEY = env( 12 | "DJANGO_SECRET_KEY", 13 | default="uxFdDSyyiV9Xh50VqRyacuuzkzmXPVGDqVAKYILaOetWvO9hDGecD8DTFtSk3vjH", 14 | ) 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 16 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 17 | 18 | # CACHES 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 21 | CACHES = { 22 | "default": { 23 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 24 | "LOCATION": "", 25 | } 26 | } 27 | 28 | # PASSWORDS 29 | # ------------------------------------------------------------------------------ 30 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 31 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 32 | 33 | # TEMPLATES 34 | # ------------------------------------------------------------------------------ 35 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 36 | ( 37 | "django.template.loaders.cached.Loader", 38 | [ 39 | "django.template.loaders.filesystem.Loader", 40 | "django.template.loaders.app_directories.Loader", 41 | ], 42 | ) 43 | ] 44 | 45 | # EMAIL 46 | # ------------------------------------------------------------------------------ 47 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 48 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 49 | 50 | # Your stuff... 51 | # ------------------------------------------------------------------------------ 52 | -------------------------------------------------------------------------------- /.envs/.production/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | # DJANGO_READ_DOT_ENV_FILE=True 4 | DJANGO_SETTINGS_MODULE=config.settings.production 5 | DJANGO_SECRET_KEY=GilqoorvQuWLqaYvJgXjS7ymbcqdCjhiCoKg91y8TDwiHDnkS9O3xYah0wTorwrx 6 | DJANGO_ADMIN_URL=gIPs1tFumkSPodIvTkyeYxSvAYbQUhGT/ 7 | DJANGO_ALLOWED_HOSTS=.asdfasq.de 8 | 9 | # Security 10 | # ------------------------------------------------------------------------------ 11 | # TIP: better off using DNS, however, redirect is OK too 12 | DJANGO_SECURE_SSL_REDIRECT=False 13 | 14 | # Email 15 | # ------------------------------------------------------------------------------ 16 | # You don't need to put this in if you're just testing 17 | # if the website actually works... I mean that's why I commented 18 | # it out. 19 | DJANGO_SERVER_EMAIL= 20 | 21 | 22 | # AWS 23 | # ------------------------------------------------------------------------------ 24 | # You need to create an S3 bucket 25 | DJANGO_AWS_STORAGE_BUCKET_NAME= 26 | 27 | # Don't use these because your EC2 machines 28 | # should automatically have an IAM role attached already. 29 | # DJANGO_AWS_ACCESS_KEY_ID= 30 | # DJANGO_AWS_SECRET_ACCESS_KEY= 31 | 32 | # django-allauth 33 | # ------------------------------------------------------------------------------ 34 | DJANGO_ACCOUNT_ALLOW_REGISTRATION=True 35 | 36 | # Gunicorn 37 | # ------------------------------------------------------------------------------ 38 | WEB_CONCURRENCY=4 39 | 40 | 41 | # Redis 42 | # ------------------------------------------------------------------------------ 43 | # I've just commented out the Redis portion. 44 | # REDIS_URL=redis://redis:6379/0 45 | 46 | # Celery 47 | # ------------------------------------------------------------------------------ 48 | 49 | # Flower 50 | CELERY_FLOWER_USER=HZXtuQrSgeqfpgcKODqRaXhLYeXPcZAD 51 | CELERY_FLOWER_PASSWORD=4cwIWB9fFWy3Ymtxbw6rpmBtuLNMslkMVpK0XPM2QWwwvMrLrOVzliTzJpmCA49f 52 | 53 | -------------------------------------------------------------------------------- /production.yml: -------------------------------------------------------------------------------- 1 | # If you want to build PostgreSQL and Redis on here... 2 | # Use the task definition for more container builds. 3 | # I recommend just using ElastiCache and RDS. 4 | # However, you can also get away with building everything 5 | # in the GitHub action. 6 | # I've left the images and Dockerfile locations in here instead 7 | # of deleting these. 8 | 9 | version: '3' 10 | 11 | volumes: 12 | production_postgres_data: {} 13 | production_postgres_data_backups: {} 14 | 15 | services: 16 | django: 17 | build: 18 | context: . 19 | dockerfile: ./compose/production/ecs/django/Dockerfile 20 | image: aws_ecs_deploy_production_django 21 | env_file: 22 | - ./.envs/.production/.django 23 | - ./.envs/.production/.postgres 24 | command: /start 25 | 26 | nginx: 27 | build: 28 | context: . 29 | dockerfile: ./compose/production/ecs/nginx/Dockerfile 30 | image: aws_ecs_deploy_production_nginx 31 | ports: 32 | - "0.0.0.0:80:80" 33 | 34 | postgres: 35 | build: 36 | context: . 37 | dockerfile: ./compose/production/postgres/Dockerfile 38 | image: aws_ecs_deploy_production_postgres 39 | volumes: 40 | - production_postgres_data:/var/lib/postgresql/data 41 | - production_postgres_data_backups:/backups 42 | env_file: 43 | - ./.envs/.production/.postgres 44 | 45 | awscli: 46 | build: 47 | context: . 48 | dockerfile: ./compose/production/aws/Dockerfile 49 | env_file: 50 | - ./.envs/.production/.django 51 | volumes: 52 | - production_postgres_data_backups:/backups 53 | 54 | redis: 55 | image: redis:5.0 56 | 57 | # You aren't using these in the Dockerfile. Trust me. 58 | # Read the FAQ section of README to find out how to 59 | # do the celery stuff 60 | 61 | # I don't recommend using Flower. If you use Flower, 62 | # you should just deploy to ONE EC2 instance instead of ECS. 63 | # For EC2 deployment, just SSH and rsync your files over. 64 | # flower: 65 | # <<: *django 66 | # image: aws_ecs_deploy_production_flower 67 | # command: /start-flower 68 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | # import django 17 | # sys.path.insert(0, os.path.abspath('..')) 18 | # os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 19 | # django.setup() 20 | 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = "AWS ECS Deploy" 25 | copyright = """2020, Andrew Chen Wang""" 26 | author = "Andrew Chen Wang" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "alabaster" 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ["_static"] 56 | -------------------------------------------------------------------------------- /compose/production/standalone/traefik/traefik.yml: -------------------------------------------------------------------------------- 1 | log: 2 | level: INFO 3 | 4 | entryPoints: 5 | web: 6 | # http 7 | address: ":80" 8 | 9 | web-secure: 10 | # https 11 | address: ":443" 12 | 13 | flower: 14 | address: ":5555" 15 | 16 | certificatesResolvers: 17 | letsencrypt: 18 | # https://docs.traefik.io/master/https/acme/#lets-encrypt 19 | acme: 20 | email: "acwangpython@gmail.com" 21 | storage: /etc/traefik/acme/acme.json 22 | # https://docs.traefik.io/master/https/acme/#httpchallenge 23 | httpChallenge: 24 | entryPoint: web 25 | 26 | http: 27 | routers: 28 | web-router: 29 | rule: "Host(`asdfasq.de`)" 30 | entryPoints: 31 | - web 32 | middlewares: 33 | - redirect 34 | - csrf 35 | service: django 36 | 37 | web-secure-router: 38 | rule: "Host(`asdfasq.de`)" 39 | entryPoints: 40 | - web-secure 41 | middlewares: 42 | - csrf 43 | service: django 44 | tls: 45 | # https://docs.traefik.io/master/routing/routers/#certresolver 46 | certResolver: letsencrypt 47 | 48 | flower-secure-router: 49 | rule: "Host(`asdfasq.de`)" 50 | entryPoints: 51 | - flower 52 | service: flower 53 | tls: 54 | # https://docs.traefik.io/master/routing/routers/#certresolver 55 | certResolver: letsencrypt 56 | 57 | middlewares: 58 | redirect: 59 | # https://docs.traefik.io/master/middlewares/redirectscheme/ 60 | redirectScheme: 61 | scheme: https 62 | permanent: true 63 | csrf: 64 | # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders 65 | # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax 66 | headers: 67 | hostsProxyHeaders: ["X-CSRFToken"] 68 | 69 | services: 70 | django: 71 | loadBalancer: 72 | servers: 73 | - url: http://django:5000 74 | 75 | flower: 76 | loadBalancer: 77 | servers: 78 | - url: http://flower:5555 79 | 80 | providers: 81 | # https://docs.traefik.io/master/providers/file/ 82 | file: 83 | filename: /etc/traefik/traefik.yml 84 | watch: true 85 | -------------------------------------------------------------------------------- /merge_production_dotenvs_in_dotenv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Sequence 4 | 5 | import pytest 6 | 7 | ROOT_DIR_PATH = Path(__file__).parent.resolve() 8 | PRODUCTION_DOTENVS_DIR_PATH = ROOT_DIR_PATH / ".envs" / ".production" 9 | PRODUCTION_DOTENV_FILE_PATHS = [ 10 | PRODUCTION_DOTENVS_DIR_PATH / ".django", 11 | PRODUCTION_DOTENVS_DIR_PATH / ".postgres", 12 | ] 13 | DOTENV_FILE_PATH = ROOT_DIR_PATH / ".env" 14 | 15 | 16 | def merge( 17 | output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True 18 | ) -> None: 19 | with open(output_file_path, "w") as output_file: 20 | for merged_file_path in merged_file_paths: 21 | with open(merged_file_path, "r") as merged_file: 22 | merged_file_content = merged_file.read() 23 | output_file.write(merged_file_content) 24 | if append_linesep: 25 | output_file.write(os.linesep) 26 | 27 | 28 | def main(): 29 | merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS) 30 | 31 | 32 | @pytest.mark.parametrize("merged_file_count", range(3)) 33 | @pytest.mark.parametrize("append_linesep", [True, False]) 34 | def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool): 35 | tmp_dir_path = Path(str(tmpdir_factory.getbasetemp())) 36 | 37 | output_file_path = tmp_dir_path / ".env" 38 | 39 | expected_output_file_content = "" 40 | merged_file_paths = [] 41 | for i in range(merged_file_count): 42 | merged_file_ord = i + 1 43 | 44 | merged_filename = ".service{}".format(merged_file_ord) 45 | merged_file_path = tmp_dir_path / merged_filename 46 | 47 | merged_file_content = merged_filename * merged_file_ord 48 | 49 | with open(merged_file_path, "w+") as file: 50 | file.write(merged_file_content) 51 | 52 | expected_output_file_content += merged_file_content 53 | if append_linesep: 54 | expected_output_file_content += os.linesep 55 | 56 | merged_file_paths.append(merged_file_path) 57 | 58 | merge(output_file_path, merged_file_paths, append_linesep) 59 | 60 | with open(output_file_path, "r") as output_file: 61 | actual_output_file_content = output_file.read() 62 | 63 | assert actual_output_file_content == expected_output_file_content 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /aws-task-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "containerDefinitions": [ 3 | { 4 | "name": "nginx", 5 | "image": "filled-in-by-gh-action", 6 | "memory": "128", 7 | "cpu": "256", 8 | "essential": true, 9 | "portMappings": [ 10 | { 11 | "containerPort": "80", 12 | "protocol": "tcp" 13 | } 14 | ], 15 | "links": [ 16 | "django-app" 17 | ] 18 | }, 19 | { 20 | "name": "django-app", 21 | "image": "filled-in-by-gh-action", 22 | "secrets": [ 23 | { 24 | "name": "DATABASE_URL", 25 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DATABASE_URL" 26 | }, 27 | { 28 | "name": "DJANGO_ACCOUNT_ALLOW_REGISTRATION", 29 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DJANGO_ACCOUNT_ALLOW_REGISTRATION" 30 | }, 31 | { 32 | "name": "DJANGO_ADMIN_URL", 33 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DJANGO_ADMIN_URL" 34 | }, 35 | { 36 | "name": "DJANGO_ALLOWED_HOSTS", 37 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DJANGO_ALLOWED_HOSTS" 38 | }, 39 | { 40 | "name": "DJANGO_AWS_STORAGE_BUCKET_NAME", 41 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DJANGO_AWS_STORAGE_BUCKET_NAME" 42 | }, 43 | { 44 | "name": "DJANGO_SECRET_KEY", 45 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DJANGO_SECRET_KEY" 46 | }, 47 | { 48 | "name": "DJANGO_SECURE_SSL_REDIRECT", 49 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/DJANGO_SECURE_SSL_REDIRECT" 50 | }, 51 | { 52 | "name": "WEB_CONCURRENCY", 53 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/WEB_CONCURRENCY" 54 | }, 55 | { 56 | "name": "SENTRY_DSN", 57 | "valueFrom": "arn:aws:ssm:::parameter/cookiecutter-django/ecs/SENTRY_DSN" 58 | } 59 | ], 60 | "memory": "512", 61 | "cpu": "512", 62 | "essential": true 63 | } 64 | ], 65 | "executionRoleArn": "arn:aws:iam:::role/ecsInstanceRole", 66 | "volumes": [], 67 | "networkMode": "bridge", 68 | "placementConstraints": [], 69 | "family": "application-stack" 70 | } 71 | -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .base import env 3 | 4 | # GENERAL 5 | # ------------------------------------------------------------------------------ 6 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 7 | DEBUG = True 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 9 | SECRET_KEY = env( 10 | "DJANGO_SECRET_KEY", 11 | default="sjFbygBRmwcExCdTEuYUU7OhvJkILDzMNZvhoSQMyZhq2IRSq9mkgocTpmRvM6L7", 12 | ) 13 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 14 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] 15 | 16 | # CACHES 17 | # ------------------------------------------------------------------------------ 18 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 19 | CACHES = { 20 | "default": { 21 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 22 | "LOCATION": "", 23 | } 24 | } 25 | 26 | # EMAIL 27 | # ------------------------------------------------------------------------------ 28 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 29 | EMAIL_BACKEND = env( 30 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 31 | ) 32 | 33 | # django-debug-toolbar 34 | # ------------------------------------------------------------------------------ 35 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites 36 | INSTALLED_APPS += ["debug_toolbar"] # noqa F405 37 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware 38 | MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 39 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config 40 | DEBUG_TOOLBAR_CONFIG = { 41 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 42 | "SHOW_TEMPLATE_CONTEXT": True, 43 | } 44 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips 45 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] 46 | if env("USE_DOCKER", default="no") == "yes": 47 | import socket 48 | 49 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) 50 | INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips] 51 | 52 | # django-extensions 53 | # ------------------------------------------------------------------------------ 54 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration 55 | INSTALLED_APPS += ["django_extensions"] # noqa F405 56 | # Celery 57 | # ------------------------------------------------------------------------------ 58 | 59 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates 60 | CELERY_TASK_EAGER_PROPAGATES = True 61 | # Your stuff... 62 | # ------------------------------------------------------------------------------ 63 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/account/email.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "account/base.html" %} 3 | 4 | {% load i18n %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Account" %}{% endblock %} 8 | 9 | {% block inner %} 10 |

{% trans "E-mail Addresses" %}

11 | 12 | {% if user.emailaddress_set.all %} 13 |

{% trans 'The following e-mail addresses are associated with your account:' %}

14 | 15 | 44 | 45 | {% else %} 46 |

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

47 | 48 | {% endif %} 49 | 50 | 51 |

{% trans "Add E-mail Address" %}

52 | 53 |
54 | {% csrf_token %} 55 | {{ form|crispy }} 56 | 57 |
58 | 59 | {% endblock %} 60 | 61 | 62 | {% block javascript %} 63 | {{ block.super }} 64 | 79 | {% endblock %} 80 | 81 | -------------------------------------------------------------------------------- /.github/workflows/aws.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and push a new container image to Amazon ECR, 2 | # and then will deploy a new task definition to Amazon ECS, when a release is created 3 | # 4 | # For more configuration, check the action out here: 5 | # https://github.com/aws-actions/amazon-ecs-deploy-task-definition 6 | # From there, you can add an appspec.yaml and other stuff AWS GH Action offers. 7 | # 8 | # Image tags use latest instead of commit sha since we have a repository 9 | # with all commits anyways. We deploy on every master push. 10 | # With that justification, we will simply replace the tags. 11 | 12 | # FIXME For now, let's just deploy on master since I don't wanna wait for Travis. 13 | 14 | name: Deploy to Amazon ECS 15 | 16 | on: 17 | push: 18 | branches: 19 | - master 20 | paths: 21 | # Trigger this workflow on master push 22 | - '**' 23 | # Except when ONLY these dirs are changed 24 | # --------------------------------------- 25 | # Docs 26 | - '!docs/**' 27 | - '!README.rst' 28 | # TODO Add my implementation of non-Docker Travis 29 | - '!production.yml' 30 | 31 | # Only ECR build workflows trigger this workflow. 32 | - '!.github/workflows/**' 33 | - '.github/workflows/aws.yml' 34 | - '.github/workflows/nginx.yml' 35 | 36 | jobs: 37 | deploy: 38 | name: Deploy 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v2 44 | 45 | - name: Configure AWS credentials 46 | uses: aws-actions/configure-aws-credentials@v1 47 | with: 48 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 49 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 50 | # TODO Change your AWS region here! 51 | aws-region: us-east-2 52 | 53 | - name: Login to Amazon ECR 54 | id: login-ecr 55 | uses: aws-actions/amazon-ecr-login@v1 56 | 57 | # Fill task definition with nginx 58 | # ------------------------------------------------------- 59 | # The image ID is latest since we don't know if nginx was actually built 60 | # but we fill this in anyways since Django image takes awhile to build. 61 | - name: Build nginx image tag 62 | id: create-nginx-image-tag 63 | env: 64 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 65 | ECR_REPOSITORY: nginx-reverse-proxy 66 | IMAGE_TAG: latest 67 | run: echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" 68 | 69 | - name: Fill in the nginx image ID in the Amazon ECS task definition 70 | id: fill-nginx-image 71 | uses: aws-actions/amazon-ecs-render-task-definition@v1 72 | with: 73 | task-definition: aws-task-definition.json 74 | container-name: nginx 75 | # FIXME How do you do this manually rather than what I did above. 76 | image: ${{ steps.create-nginx-image-tag.outputs.image }} 77 | 78 | # Build Django app 79 | # ------------------------------------------------------- 80 | - name: Build, tag, and push Django image to Amazon ECR 81 | id: build-django-image 82 | env: 83 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 84 | ECR_REPOSITORY: django-app 85 | IMAGE_TAG: ${{ github.sha }} 86 | run: | 87 | # Build a docker container and 88 | # push it to ECR so that it can 89 | # be deployed to ECS. 90 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 91 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 92 | echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" 93 | 94 | - name: Fill in the new Django image ID in Amazon ECS task definition 95 | id: fill-django-image 96 | uses: aws-actions/amazon-ecs-render-task-definition@v1 97 | with: 98 | task-definition: ${{ steps.fill-nginx-image.outputs.task-definition }} 99 | container-name: django-app 100 | image: ${{ steps.build-django-image.outputs.image }} 101 | 102 | # Deploy 103 | # ------------------------------------------------------- 104 | - name: Deploy Amazon ECS task definition 105 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1 106 | with: 107 | task-definition: ${{ steps.fill-django-image.outputs.task-definition }} 108 | # TODO Change the service and cluster name if you changed it 109 | service: cookiecutter-django-service 110 | cluster: cookiecutter-django 111 | wait-for-service-stability: true 112 | -------------------------------------------------------------------------------- /aws_ecs_deploy/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | {% block title %}AWS ECS Deploy{% endblock title %} 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | {% block css %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endblock %} 32 | 33 | 34 | 35 | 36 | 37 |
38 | 74 | 75 |
76 | 77 |
78 | 79 | {% if messages %} 80 | {% for message in messages %} 81 |
{{ message }}
82 | {% endfor %} 83 | {% endif %} 84 | 85 | {% block content %} 86 |

Use this document as a way to quick start any new project.

87 | {% endblock content %} 88 | 89 |
90 | 91 | {% block modal %}{% endblock modal %} 92 | 93 | 95 | 96 | {% block javascript %} 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {% endblock javascript %} 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | staticfiles/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv 63 | .python-version 64 | 65 | # celery beat schedule file 66 | celerybeat-schedule 67 | 68 | # Environments 69 | .venv 70 | venv/ 71 | ENV/ 72 | 73 | # Rope project settings 74 | .ropeproject 75 | 76 | # mkdocs documentation 77 | /site 78 | 79 | # mypy 80 | .mypy_cache/ 81 | 82 | 83 | ### Node template 84 | # Logs 85 | logs 86 | *.log 87 | npm-debug.log* 88 | yarn-debug.log* 89 | yarn-error.log* 90 | 91 | # Runtime data 92 | pids 93 | *.pid 94 | *.seed 95 | *.pid.lock 96 | 97 | # Directory for instrumented libs generated by jscoverage/JSCover 98 | lib-cov 99 | 100 | # Coverage directory used by tools like istanbul 101 | coverage 102 | 103 | # nyc test coverage 104 | .nyc_output 105 | 106 | # Bower dependency directory (https://bower.io/) 107 | bower_components 108 | 109 | # node-waf configuration 110 | .lock-wscript 111 | 112 | # Compiled binary addons (http://nodejs.org/api/addons.html) 113 | build/Release 114 | 115 | # Dependency directories 116 | node_modules/ 117 | jspm_packages/ 118 | 119 | # Typescript v1 declaration files 120 | typings/ 121 | 122 | # Optional npm cache directory 123 | .npm 124 | 125 | # Optional eslint cache 126 | .eslintcache 127 | 128 | # Optional REPL history 129 | .node_repl_history 130 | 131 | # Output of 'npm pack' 132 | *.tgz 133 | 134 | # Yarn Integrity file 135 | .yarn-integrity 136 | 137 | 138 | ### Linux template 139 | *~ 140 | 141 | # temporary files which can be created if a process still has a handle open of a deleted file 142 | .fuse_hidden* 143 | 144 | # KDE directory preferences 145 | .directory 146 | 147 | # Linux trash folder which might appear on any partition or disk 148 | .Trash-* 149 | 150 | # .nfs files are created when an open file is removed but is still being accessed 151 | .nfs* 152 | 153 | 154 | ### VisualStudioCode template 155 | .vscode/* 156 | !.vscode/settings.json 157 | !.vscode/tasks.json 158 | !.vscode/launch.json 159 | !.vscode/extensions.json 160 | 161 | 162 | 163 | 164 | 165 | ### Windows template 166 | # Windows thumbnail cache files 167 | Thumbs.db 168 | ehthumbs.db 169 | ehthumbs_vista.db 170 | 171 | # Dump file 172 | *.stackdump 173 | 174 | # Folder config file 175 | Desktop.ini 176 | 177 | # Recycle Bin used on file shares 178 | $RECYCLE.BIN/ 179 | 180 | # Windows Installer files 181 | *.cab 182 | *.msi 183 | *.msm 184 | *.msp 185 | 186 | # Windows shortcuts 187 | *.lnk 188 | 189 | 190 | ### macOS template 191 | # General 192 | *.DS_Store 193 | .AppleDouble 194 | .LSOverride 195 | 196 | # Icon must end with two \r 197 | Icon 198 | 199 | # Thumbnails 200 | ._* 201 | 202 | # Files that might appear in the root of a volume 203 | .DocumentRevisions-V100 204 | .fseventsd 205 | .Spotlight-V100 206 | .TemporaryItems 207 | .Trashes 208 | .VolumeIcon.icns 209 | .com.apple.timemachine.donotpresent 210 | 211 | # Directories potentially created on remote AFP share 212 | .AppleDB 213 | .AppleDesktop 214 | Network Trash Folder 215 | Temporary Items 216 | .apdisk 217 | 218 | 219 | ### SublimeText template 220 | # Cache files for Sublime Text 221 | *.tmlanguage.cache 222 | *.tmPreferences.cache 223 | *.stTheme.cache 224 | 225 | # Workspace files are user-specific 226 | *.sublime-workspace 227 | 228 | # Project files should be checked into the repository, unless a significant 229 | # proportion of contributors will probably not be using Sublime Text 230 | # *.sublime-project 231 | 232 | # SFTP configuration file 233 | sftp-config.json 234 | 235 | # Package control specific files 236 | Package Control.last-run 237 | Package Control.ca-list 238 | Package Control.ca-bundle 239 | Package Control.system-ca-bundle 240 | Package Control.cache/ 241 | Package Control.ca-certs/ 242 | Package Control.merged-ca-bundle 243 | Package Control.user-ca-bundle 244 | oscrypto-ca-bundle.crt 245 | bh_unicode_properties.cache 246 | 247 | # Sublime-github package stores a github token in this file 248 | # https://packagecontrol.io/packages/sublime-github 249 | GitHub.sublime-settings 250 | 251 | 252 | ### Vim template 253 | # Swap 254 | [._]*.s[a-v][a-z] 255 | [._]*.sw[a-p] 256 | [._]s[a-v][a-z] 257 | [._]sw[a-p] 258 | 259 | # Session 260 | Session.vim 261 | 262 | # Temporary 263 | .netrwhist 264 | 265 | # Auto-generated tag files 266 | tags 267 | 268 | 269 | ### Project template 270 | 271 | aws_ecs_deploy/media/ 272 | 273 | .pytest_cache/ 274 | 275 | 276 | .ipython/ 277 | !.env 278 | !.envs/* 279 | !.envs/.local/ 280 | -------------------------------------------------------------------------------- /aws_ecs_deploy/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.auth.models 2 | import django.contrib.auth.validators 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [("auth", "0008_alter_user_username_max_length")] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="User", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("password", models.CharField(max_length=128, verbose_name="password")), 27 | ( 28 | "last_login", 29 | models.DateTimeField( 30 | blank=True, null=True, verbose_name="last login" 31 | ), 32 | ), 33 | ( 34 | "is_superuser", 35 | models.BooleanField( 36 | default=False, 37 | help_text="Designates that this user has all permissions without explicitly assigning them.", 38 | verbose_name="superuser status", 39 | ), 40 | ), 41 | ( 42 | "username", 43 | models.CharField( 44 | error_messages={ 45 | "unique": "A user with that username already exists." 46 | }, 47 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 48 | max_length=150, 49 | unique=True, 50 | validators=[ 51 | django.contrib.auth.validators.UnicodeUsernameValidator() 52 | ], 53 | verbose_name="username", 54 | ), 55 | ), 56 | ( 57 | "first_name", 58 | models.CharField( 59 | blank=True, max_length=30, verbose_name="first name" 60 | ), 61 | ), 62 | ( 63 | "last_name", 64 | models.CharField( 65 | blank=True, max_length=150, verbose_name="last name" 66 | ), 67 | ), 68 | ( 69 | "email", 70 | models.EmailField( 71 | blank=True, max_length=254, verbose_name="email address" 72 | ), 73 | ), 74 | ( 75 | "is_staff", 76 | models.BooleanField( 77 | default=False, 78 | help_text="Designates whether the user can log into this admin site.", 79 | verbose_name="staff status", 80 | ), 81 | ), 82 | ( 83 | "is_active", 84 | models.BooleanField( 85 | default=True, 86 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 87 | verbose_name="active", 88 | ), 89 | ), 90 | ( 91 | "date_joined", 92 | models.DateTimeField( 93 | default=django.utils.timezone.now, verbose_name="date joined" 94 | ), 95 | ), 96 | ( 97 | "name", 98 | models.CharField( 99 | blank=True, max_length=255, verbose_name="Name of User" 100 | ), 101 | ), 102 | ( 103 | "groups", 104 | models.ManyToManyField( 105 | blank=True, 106 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 107 | related_name="user_set", 108 | related_query_name="user", 109 | to="auth.Group", 110 | verbose_name="groups", 111 | ), 112 | ), 113 | ( 114 | "user_permissions", 115 | models.ManyToManyField( 116 | blank=True, 117 | help_text="Specific permissions for this user.", 118 | related_name="user_set", 119 | related_query_name="user", 120 | to="auth.Permission", 121 | verbose_name="user permissions", 122 | ), 123 | ), 124 | ], 125 | options={ 126 | "verbose_name_plural": "users", 127 | "verbose_name": "user", 128 | "abstract": False, 129 | }, 130 | managers=[("objects", django.contrib.auth.models.UserManager())], 131 | ) 132 | ] 133 | -------------------------------------------------------------------------------- /config/settings/production.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import sentry_sdk 4 | from sentry_sdk.integrations.django import DjangoIntegration 5 | from sentry_sdk.integrations.logging import LoggingIntegration 6 | 7 | from .base import * # noqa 8 | from .base import env 9 | 10 | # GENERAL 11 | # ------------------------------------------------------------------------------ 12 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 13 | SECRET_KEY = env("DJANGO_SECRET_KEY") 14 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 15 | ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["asdfasq.de"]) 16 | 17 | # DATABASES 18 | # ------------------------------------------------------------------------------ 19 | DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 20 | DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 21 | DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 22 | 23 | # CACHES 24 | # ------------------------------------------------------------------------------ 25 | # CACHES = { 26 | # "default": { 27 | # "BACKEND": "django_redis.cache.RedisCache", 28 | # "LOCATION": env("REDIS_URL"), 29 | # "OPTIONS": { 30 | # "CLIENT_CLASS": "django_redis.client.DefaultClient", 31 | # # Mimicing memcache behavior. 32 | # # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior 33 | # "IGNORE_EXCEPTIONS": True, 34 | # }, 35 | # } 36 | # } 37 | 38 | # SECURITY 39 | # ------------------------------------------------------------------------------ 40 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header 41 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect 43 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) 44 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure 45 | SESSION_COOKIE_SECURE = True 46 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure 47 | CSRF_COOKIE_SECURE = True 48 | # https://docs.djangoproject.com/en/dev/topics/security/#ssl-https 49 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds 50 | # TODO: set this to 60 seconds first and then to 518400 once you prove the former works 51 | SECURE_HSTS_SECONDS = 60 52 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains 53 | SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( 54 | "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True 55 | ) 56 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload 57 | SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) 58 | # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff 59 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool( 60 | "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True 61 | ) 62 | 63 | # STORAGES 64 | # ------------------------------------------------------------------------------ 65 | # https://django-storages.readthedocs.io/en/latest/#installation 66 | INSTALLED_APPS += ["storages"] # noqa F405 67 | 68 | # You won't be needing these credentials since they're attached to the instance. 69 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 70 | # AWS_ACCESS_KEY_ID = env("DJANGO_AWS_ACCESS_KEY_ID") 71 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 72 | # AWS_SECRET_ACCESS_KEY = env("DJANGO_AWS_SECRET_ACCESS_KEY") 73 | 74 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 75 | AWS_STORAGE_BUCKET_NAME = env("DJANGO_AWS_STORAGE_BUCKET_NAME") 76 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 77 | AWS_QUERYSTRING_AUTH = False 78 | # DO NOT change these unless you know what you're doing. 79 | _AWS_EXPIRY = 60 * 60 * 24 * 7 80 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 81 | AWS_S3_OBJECT_PARAMETERS = { 82 | "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" 83 | } 84 | # Must specify this now 85 | AWS_S3_SIGNATURE_VERSION = "s3v4" 86 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 87 | AWS_DEFAULT_ACL = None 88 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 89 | AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None) 90 | # STATIC 91 | # ------------------------ 92 | STATICFILES_STORAGE = "aws_ecs_deploy.utils.storages.StaticRootS3Boto3Storage" 93 | COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy" 94 | STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/" 95 | # MEDIA 96 | # ------------------------------------------------------------------------------ 97 | DEFAULT_FILE_STORAGE = "aws_ecs_deploy.utils.storages.MediaRootS3Boto3Storage" 98 | MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/" 99 | 100 | # TEMPLATES 101 | # ------------------------------------------------------------------------------ 102 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 103 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 104 | ( 105 | "django.template.loaders.cached.Loader", 106 | [ 107 | "django.template.loaders.filesystem.Loader", 108 | "django.template.loaders.app_directories.Loader", 109 | ], 110 | ) 111 | ] 112 | 113 | # EMAIL 114 | # ------------------------------------------------------------------------------ 115 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email 116 | DEFAULT_FROM_EMAIL = env( 117 | "DJANGO_DEFAULT_FROM_EMAIL", default="AWS ECS Deploy " 118 | ) 119 | # https://docs.djangoproject.com/en/dev/ref/settings/#server-email 120 | SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) 121 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix 122 | EMAIL_SUBJECT_PREFIX = env( 123 | "DJANGO_EMAIL_SUBJECT_PREFIX", default="[AWS ECS Deploy]" 124 | ) 125 | 126 | # ADMIN 127 | # ------------------------------------------------------------------------------ 128 | # Django Admin URL regex. 129 | ADMIN_URL = env("DJANGO_ADMIN_URL") 130 | 131 | # Anymail 132 | # ------------------------------------------------------------------------------ 133 | # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail 134 | INSTALLED_APPS += ["anymail"] # noqa F405 135 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 136 | # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference 137 | # https://anymail.readthedocs.io/en/stable/esps/amazon_ses/ 138 | EMAIL_BACKEND = "anymail.backends.amazon_ses.EmailBackend" 139 | ANYMAIL = {} 140 | 141 | # Collectfast 142 | # ------------------------------------------------------------------------------ 143 | # https://github.com/antonagestam/collectfast#installation 144 | INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa F405 145 | 146 | # LOGGING 147 | # ------------------------------------------------------------------------------ 148 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 149 | # See https://docs.djangoproject.com/en/dev/topics/logging for 150 | # more details on how to customize your logging configuration. 151 | # A sample logging configuration. The only tangible logging 152 | # performed by this configuration is to send an email to 153 | # the site admins on every HTTP 500 error when DEBUG=False. 154 | LOGGING = { 155 | "version": 1, 156 | "disable_existing_loggers": True, 157 | "formatters": { 158 | "verbose": { 159 | "format": "%(levelname)s %(asctime)s %(module)s " 160 | "%(process)d %(thread)d %(message)s" 161 | } 162 | }, 163 | "handlers": { 164 | "console": { 165 | "level": "DEBUG", 166 | "class": "logging.StreamHandler", 167 | "formatter": "verbose", 168 | } 169 | }, 170 | "root": {"level": "INFO", "handlers": ["console"]}, 171 | "loggers": { 172 | "django.db.backends": { 173 | "level": "ERROR", 174 | "handlers": ["console"], 175 | "propagate": False, 176 | }, 177 | # Errors logged by the SDK itself 178 | "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False}, 179 | "django.security.DisallowedHost": { 180 | "level": "ERROR", 181 | "handlers": ["console"], 182 | "propagate": False, 183 | }, 184 | }, 185 | } 186 | 187 | # Sentry 188 | # ------------------------------------------------------------------------------ 189 | SENTRY_DSN = env("SENTRY_DSN") 190 | SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.DEBUG) 191 | 192 | sentry_logging = LoggingIntegration( 193 | level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs 194 | event_level=logging.ERROR, # Send errors as events 195 | ) 196 | sentry_sdk.init(dsn=SENTRY_DSN, integrations=[sentry_logging, DjangoIntegration()]) 197 | 198 | # Your stuff... 199 | # ------------------------------------------------------------------------------ 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Andrew Chen Wang 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base settings to build other settings files upon. 3 | """ 4 | from pathlib import Path 5 | 6 | import environ 7 | 8 | ROOT_DIR = Path(__file__).parents[2] 9 | # aws_ecs_deploy/) 10 | APPS_DIR = ROOT_DIR / "aws_ecs_deploy" 11 | env = environ.Env() 12 | 13 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) 14 | if READ_DOT_ENV_FILE: 15 | # OS environment variables take precedence over variables from .env 16 | env.read_env(str(ROOT_DIR / ".env")) 17 | 18 | # GENERAL 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 21 | DEBUG = env.bool("DJANGO_DEBUG", False) 22 | # Local time zone. Choices are 23 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 24 | # though not all of them may be available with every OS. 25 | # In Windows, this must be set to your system time zone. 26 | TIME_ZONE = "UTC" 27 | # https://docs.djangoproject.com/en/dev/ref/settings/#language-code 28 | LANGUAGE_CODE = "en-us" 29 | # https://docs.djangoproject.com/en/dev/ref/settings/#site-id 30 | SITE_ID = 1 31 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 32 | USE_I18N = True 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 34 | USE_L10N = True 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 36 | USE_TZ = True 37 | # https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths 38 | LOCALE_PATHS = [str(ROOT_DIR / "locale")] 39 | 40 | # DATABASES 41 | # ------------------------------------------------------------------------------ 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 43 | DATABASES = {"default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3")} 44 | DATABASES["default"]["ATOMIC_REQUESTS"] = True 45 | 46 | # URLS 47 | # ------------------------------------------------------------------------------ 48 | # https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 49 | ROOT_URLCONF = "config.urls" 50 | # https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 51 | WSGI_APPLICATION = "config.wsgi.application" 52 | 53 | # APPS 54 | # ------------------------------------------------------------------------------ 55 | DJANGO_APPS = [ 56 | "django.contrib.auth", 57 | "django.contrib.contenttypes", 58 | "django.contrib.sessions", 59 | "django.contrib.sites", 60 | "django.contrib.messages", 61 | "django.contrib.staticfiles", 62 | # "django.contrib.humanize", # Handy template tags 63 | "django.contrib.admin", 64 | "django.forms", 65 | ] 66 | THIRD_PARTY_APPS = [ 67 | "crispy_forms", 68 | "allauth", 69 | "allauth.account", 70 | "allauth.socialaccount", 71 | "django_celery_beat", 72 | ] 73 | 74 | LOCAL_APPS = [ 75 | "aws_ecs_deploy.users.apps.UsersConfig", 76 | # Your stuff: custom apps go here 77 | ] 78 | # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 79 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 80 | 81 | # MIGRATIONS 82 | # ------------------------------------------------------------------------------ 83 | # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules 84 | MIGRATION_MODULES = {"sites": "aws_ecs_deploy.contrib.sites.migrations"} 85 | 86 | # AUTHENTICATION 87 | # ------------------------------------------------------------------------------ 88 | # https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends 89 | AUTHENTICATION_BACKENDS = [ 90 | "django.contrib.auth.backends.ModelBackend", 91 | "allauth.account.auth_backends.AuthenticationBackend", 92 | ] 93 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model 94 | AUTH_USER_MODEL = "users.User" 95 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 96 | LOGIN_REDIRECT_URL = "users:redirect" 97 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-url 98 | LOGIN_URL = "account_login" 99 | 100 | # PASSWORDS 101 | # ------------------------------------------------------------------------------ 102 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 103 | PASSWORD_HASHERS = [ 104 | # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django 105 | "django.contrib.auth.hashers.Argon2PasswordHasher", 106 | "django.contrib.auth.hashers.PBKDF2PasswordHasher", 107 | "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 108 | "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", 109 | ] 110 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 111 | AUTH_PASSWORD_VALIDATORS = [ 112 | { 113 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 114 | }, 115 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 116 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 117 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 118 | ] 119 | 120 | # MIDDLEWARE 121 | # ------------------------------------------------------------------------------ 122 | # https://docs.djangoproject.com/en/dev/ref/settings/#middleware 123 | MIDDLEWARE = [ 124 | "django.middleware.security.SecurityMiddleware", 125 | "django.contrib.sessions.middleware.SessionMiddleware", 126 | "django.middleware.locale.LocaleMiddleware", 127 | "django.middleware.common.CommonMiddleware", 128 | "django.middleware.csrf.CsrfViewMiddleware", 129 | "django.contrib.auth.middleware.AuthenticationMiddleware", 130 | "django.contrib.messages.middleware.MessageMiddleware", 131 | "django.middleware.common.BrokenLinkEmailsMiddleware", 132 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 133 | ] 134 | 135 | # STATIC 136 | # ------------------------------------------------------------------------------ 137 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-root 138 | STATIC_ROOT = str(ROOT_DIR / "staticfiles") 139 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-url 140 | STATIC_URL = "/static/" 141 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 142 | STATICFILES_DIRS = [str(APPS_DIR / "static")] 143 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 144 | STATICFILES_FINDERS = [ 145 | "django.contrib.staticfiles.finders.FileSystemFinder", 146 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 147 | ] 148 | 149 | # MEDIA 150 | # ------------------------------------------------------------------------------ 151 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-root 152 | MEDIA_ROOT = str(APPS_DIR / "media") 153 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-url 154 | MEDIA_URL = "/media/" 155 | 156 | # TEMPLATES 157 | # ------------------------------------------------------------------------------ 158 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 159 | TEMPLATES = [ 160 | { 161 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND 162 | "BACKEND": "django.template.backends.django.DjangoTemplates", 163 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs 164 | "DIRS": [str(APPS_DIR / "templates")], 165 | "OPTIONS": { 166 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders 167 | # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types 168 | "loaders": [ 169 | "django.template.loaders.filesystem.Loader", 170 | "django.template.loaders.app_directories.Loader", 171 | ], 172 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 173 | "context_processors": [ 174 | "django.template.context_processors.debug", 175 | "django.template.context_processors.request", 176 | "django.contrib.auth.context_processors.auth", 177 | "django.template.context_processors.i18n", 178 | "django.template.context_processors.media", 179 | "django.template.context_processors.static", 180 | "django.template.context_processors.tz", 181 | "django.contrib.messages.context_processors.messages", 182 | "aws_ecs_deploy.utils.context_processors.settings_context", 183 | ], 184 | }, 185 | } 186 | ] 187 | 188 | # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer 189 | FORM_RENDERER = "django.forms.renderers.TemplatesSetting" 190 | 191 | # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs 192 | CRISPY_TEMPLATE_PACK = "bootstrap4" 193 | 194 | # FIXTURES 195 | # ------------------------------------------------------------------------------ 196 | # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs 197 | FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) 198 | 199 | # SECURITY 200 | # ------------------------------------------------------------------------------ 201 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly 202 | SESSION_COOKIE_HTTPONLY = True 203 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly 204 | CSRF_COOKIE_HTTPONLY = True 205 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter 206 | SECURE_BROWSER_XSS_FILTER = True 207 | # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options 208 | X_FRAME_OPTIONS = "DENY" 209 | 210 | # EMAIL 211 | # ------------------------------------------------------------------------------ 212 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 213 | EMAIL_BACKEND = env( 214 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend" 215 | ) 216 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout 217 | EMAIL_TIMEOUT = 5 218 | 219 | # ADMIN 220 | # ------------------------------------------------------------------------------ 221 | # Django Admin URL. 222 | ADMIN_URL = "admin/" 223 | # https://docs.djangoproject.com/en/dev/ref/settings/#admins 224 | ADMINS = [("""Andrew Chen Wang""", "acwangpython@gmail.com")] 225 | # https://docs.djangoproject.com/en/dev/ref/settings/#managers 226 | MANAGERS = ADMINS 227 | 228 | # LOGGING 229 | # ------------------------------------------------------------------------------ 230 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 231 | # See https://docs.djangoproject.com/en/dev/topics/logging for 232 | # more details on how to customize your logging configuration. 233 | LOGGING = { 234 | "version": 1, 235 | "disable_existing_loggers": False, 236 | "formatters": { 237 | "verbose": { 238 | "format": "%(levelname)s %(asctime)s %(module)s " 239 | "%(process)d %(thread)d %(message)s" 240 | } 241 | }, 242 | "handlers": { 243 | "console": { 244 | "level": "DEBUG", 245 | "class": "logging.StreamHandler", 246 | "formatter": "verbose", 247 | } 248 | }, 249 | "root": {"level": "INFO", "handlers": ["console"]}, 250 | } 251 | 252 | # TODO Add Redis to task definition to use Celery. 253 | # Or use ElastiCache 254 | """ 255 | # Celery 256 | # ------------------------------------------------------------------------------ 257 | if USE_TZ: 258 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone 259 | CELERY_TIMEZONE = TIME_ZONE 260 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url 261 | CELERY_BROKER_URL = env("CELERY_BROKER_URL") 262 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend 263 | CELERY_RESULT_BACKEND = CELERY_BROKER_URL 264 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content 265 | CELERY_ACCEPT_CONTENT = ["json"] 266 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer 267 | CELERY_TASK_SERIALIZER = "json" 268 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer 269 | CELERY_RESULT_SERIALIZER = "json" 270 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit 271 | # TODO: set to whatever value is adequate in your circumstances 272 | CELERY_TASK_TIME_LIMIT = 5 * 60 273 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit 274 | # TODO: set to whatever value is adequate in your circumstances 275 | CELERY_TASK_SOFT_TIME_LIMIT = 60 276 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler 277 | CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" 278 | """ 279 | 280 | # django-allauth 281 | # ------------------------------------------------------------------------------ 282 | ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) 283 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 284 | ACCOUNT_AUTHENTICATION_METHOD = "username" 285 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 286 | ACCOUNT_EMAIL_REQUIRED = True 287 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 288 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 289 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 290 | ACCOUNT_ADAPTER = "aws_ecs_deploy.users.adapters.AccountAdapter" 291 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 292 | SOCIALACCOUNT_ADAPTER = "aws_ecs_deploy.users.adapters.SocialAccountAdapter" 293 | 294 | 295 | # Your stuff... 296 | # ------------------------------------------------------------------------------ 297 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | AWS ECS Deploy 2 | ============== 3 | 4 | AWS ECS Deploy Blue/Green using GitHub Actions. 5 | 6 | Sorry if this didn't work. I tried to do this all in ONE go, or one commit. ~ Andrew C. 29 May 2020. Ripperoni. 7 | 8 | Edit: It works! It only took so long... and 3 days of finally realizing it security groups is a thing... 9 | I'm a beginner, so start-ups, take advantage of this repository! ~ Andrew C. 10 June 2020. 10 | 11 | Much appreciated code taken from awslabs: https://github.com/awslabs/ecs-nginx-reverse-proxy/tree/master/reverse-proxy 12 | 13 | .. image:: https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg 14 | :target: https://github.com/pydanny/cookiecutter-django/ 15 | :alt: Built with Cookiecutter Django 16 | 17 | :License: Apache Software License 2.0 18 | 19 | Table of contents: 20 | 21 | - How does this work? 22 | - Deployment Instructions 23 | - Cleanup 24 | - The Caveats in THIS EXAMPLE (easily avoidable) 25 | - Initial Cookiecutter Generation 26 | - Minimal IAM Credentials for ECS 27 | - Minimal IAM Credentials for Deployment 28 | - FAQ 29 | 30 | How does this work? 31 | ------------------- 32 | 33 | The deployment is done through Travis CI which does a sorta webhook to a 34 | GitHub action which will use several AWS GH Actions to finally deploy 35 | your application. TODO Actually create that webhook. 36 | 37 | The AWS services that we'll be using are: CodeDeploy, ECS + ECR, Parameter Store, 38 | IAM, Application Load Balancer. CodeDeploy is for deploying from the GH 39 | action. ECS + ECR is where our servers will be located. Parameter store will 40 | be where we store our secrets/environment variables. IAM is for proper 41 | security measures of the credentials given to the server AND to GitHub for deployment. 42 | Yes, we need two IAM users, one for GH and the other for ECS. Finally, we need 43 | ALB for proper Blue/Green deployment assuming you have more than one instance 44 | in your ECS cluster. 45 | 46 | I chose ECS because there were GitHub actions for this anyways. However, note 47 | that this is much slower than simply using EC2. The current Travis configuration 48 | uses Docker, but my PR in cookiecutter-django should change the configuration 49 | from Docker based to completely Docker free, erasing 1.5 minutes of CI. 50 | 51 | Why not use Fargate? Personally, I like EC2 over Fargate or Lambda since 52 | I use celery a lot. That's really it; I haven't used AWS much, nor have 53 | I actually deployed an AWS app until now. 54 | 55 | I've set up an nginx reverse proxy to do a lot of heavy weighted work and to minimize 56 | security risks. Additionally, I've left out several key components like PostgreSQL 57 | and Redis. Those can easily be added in the `aws-task-definition.json`. Just look 58 | at how the rest of the GH action is setup to configure PostgreSQL Dockerfile in 59 | `compose/production/postgres`. 60 | 61 | Let's start the deployment process! 62 | 63 | Deployment Instructions 64 | ----------------------- 65 | 66 | The following details how to deploy this application stack to ECS. 67 | If you need to also setup your database and cache, follow 68 | [issue #9](https://github.com/Andrew-Chen-Wang/cookiecutter-django-ecs-github/issues/9). 69 | You may want to get the hang of the following first, specifically security groups in step 1. 70 | Also note that I didn't write that in the tutorial because it's spare and 71 | came from my memory from a couple months back. I didn't think it was detailed 72 | enough for this tutorial. 73 | 74 | It's a lot of instructions since there are so many services: 75 | 76 | 1. You must have an IAM user with the correct permissions that you can find at the 77 | bottom of this README. Make sure you copy the ones from the section labeled 78 | `Minimal IAM Credentials for Deployment`. You can create a new policy 79 | using the JSON below during the Set Permissions section by pressing 80 | "Attach existing policies directly" and pressing "Create Policy." 81 | 82 | Add the IAM user's credentials to your repo's secrets 83 | in the repo's settings. The credentials' names MUST be `AWS_ACCESS_KEY_ID` 84 | and `AWS_SECRET_ACCESS_KEY` 85 | 86 | 2. Security Groups - Exposing your ports 87 | 88 | It took me 10 prolonged days to figure out my security groups were not properly 89 | configured for my ALB and ECS instances. So follow this carefully. 90 | 91 | Create a security group called WebAccess with a description saying it's for ALB. 92 | 93 | The inbound traffic should be of 4 rules: 94 | 95 | - Type HTTP, with custom source type on source 0.0.0.0/0 96 | - Type HTTP, with custom source type on source ::/0 97 | - Type HTTPS, with custom source type on source 0.0.0.0/0 98 | - Type HTTPS, with custom source type on source ::/0 99 | 100 | The outbound rules should be left on default, but just in case: 101 | 102 | - Type All traffic, destination type Custom at 0.0.0.0/0 103 | 104 | Create another security group. This is for your ECS instances. 105 | Name it ECS-reverse-proxy (for another website, I'd recommend 106 | prefixing the name of this with your website name). 107 | 108 | The inbound traffic should be 1 rule only: 109 | 110 | - Type All Traffic, with custom source type, and find your first security 111 | group (it should say the name of the security group you just created). 112 | 113 | The outbound traffic is the same as the first one. 114 | 115 | 3. Create two ECR repositories to store your images by running the following: 116 | 117 | .. code-block:: shell 118 | 119 | aws ecr create-repository --repository-name nginx-reverse-proxy --region us-east-2 120 | aws ecr create-repository --repository-name django-app --region us-east-2 121 | 122 | Replace the region with your AWS region, and make sure you change it in the 123 | `.github/workflows/aws.yml` file, as well. 124 | 125 | 4. Create an ECS cluster. 126 | Replace the values for `cluster` in the aws workflow 127 | with your cluster name. The default is "cookiecutter-django". 128 | I guess you could just write that one and not need to change 129 | the one in the GH action. 130 | 131 | - Choose EC2 Linux + Networking 132 | - I chose t2.medium for the instance type for enough memory. 133 | 134 | - The task definition uses a limited amount of memory as celery 135 | isn't a main priority here. As you expand, celery will take up 136 | more memory and you'll have to increase the memory capacity for 137 | the Django app, which means you'll have to use a different 138 | instance type. 139 | 140 | - You can just have one instance since Blue/Green deployments 141 | will provision a new instance and deregister the old one. 142 | 143 | - That's the downfall about ECS. You can configure everything 144 | in your Dockerfile, but it's a slow build and start time and 145 | you wish the instance could just simply be updated... 146 | 147 | - I had a key pair from previous EC2 usage. You don't necessarily need it 148 | but it could be helpful to have on in the future. Yes, you can configure 149 | an ssh key pair in the future. 150 | - Create a new VPC. 151 | - Choose a subnet. Remember which subnet it is. 152 | - Use that second security group that I said was for your ECS instances! 153 | - The IAM role can be the one created by them called ecsInstanceRole. 154 | 155 | 5. Grant a service trust relationship for newly created IAM role 156 | 157 | In order to add our environment variables via our task definition, we must 158 | make sure the IAM role (above, hopefully it was ecsInstanceRole) 159 | can even do a task execution. 160 | 161 | Go to your newly created IAM role and click "Trust relationships" 162 | 163 | Edit the trust relationship so that, in the "Service" array, you add 164 | `ecs-tasks.amazonaws.com` 165 | 166 | 5. Buy a website in Route 53. 167 | 168 | I bought a random website with a `.de` ending since that came out to be $8. 169 | My website was `asdfasq.de`. Random, ey? 170 | 171 | The more random the name and extension, the cheaper. 172 | 173 | Change allowed hosts in `config/settings/production.py` to your domain. 174 | 175 | Change every instance of asdfasq.de in `compose/production/ecs/nginx/nginx.conf` 176 | to your domain. 177 | 178 | 6. Configure ACM for https for your domain. 179 | 180 | Find ACM (certificate manager) and add your domain and 181 | its www. format, as well. 182 | 183 | 7. Create the ALB, or Application Load Balancer with ACM 184 | 185 | NOTE: I might be missing a step with the certificate manager. I deployed 186 | a test website on EC2 as a standalone, and I might've done something to 187 | properly configure the certificate. PLEASE open a PR/Patch if I'm missing it. 188 | 189 | Go to the EC2 page. Find the Load Balancers section and create a new balancer. 190 | 191 | - Name your load balancer something like... Joe. 192 | - Add a new listener with HTTPS. The port should autofill itself to be 443. 193 | Click next. 194 | - Your VPC and subnets should be the same as the ones you 195 | SHOULD'VE WRITTEN DOWN in step 3 when creating your cluster. 196 | - I'm seeing my website and certificate. If you're not, then look online 197 | for how to do that and open a PR. 198 | - Your security group is the first one you created in step 2. 199 | - Configure routing: 200 | 201 | - Select new target group 202 | - Name it something 203 | - The protocol should be HTTP. 204 | - Leave health check on default. 205 | 206 | - Don't register any instance. 207 | - Finally, create it. 208 | 209 | 8. Add your load balancer to your hosted zone 210 | 211 | Go back to Route 53. Go to your hosted zone and add 2 A record 212 | sets. Choose yes for use alias. Find your load balancer. 213 | 214 | The difference between each record set is that the first one 215 | for name can be left blank while the other one should have www. 216 | This is also how you can have multiple ECS clusters for different 217 | applications (i.e. with subdomains). 218 | 219 | 9. Create a task definition. 220 | 221 | Go to the `aws-task-definition.json` file and copy its contents. 222 | 223 | In the ECS dashboard, create a new task definition. Scroll to the 224 | bottom until you find "configure via JSON." Paste the contents. 225 | 226 | 10. Create an ECS service. 227 | 228 | After you finished creating your cluster, you should arrive in the service 229 | tab. Create a service. 230 | 231 | - Configure Service 232 | 233 | - Launch type is obviously EC2 234 | - Skip the Task Definition section. 235 | - Choose your cluster if it's not the one you created. 236 | - Enter a service name 237 | 238 | - default in workflow is cookiecutter-django-service. 239 | - If you use the default name, then you don't need to 240 | change the one in the GH action. 241 | 242 | - Number of tasks is 1 243 | - The deployments section! 244 | 245 | - Deployment type: Blue/Green 246 | 247 | - I explained up top why I chose this one. 248 | - Gist of it: CodeDeploy + Websockets + Slow shifting of Traffic. 249 | - Deployment configuration: ECS Linear 10 Percent Every 1 Minute 250 | - Service role for CodeDeploy: This is the IAM role that you should 251 | have for your ECS instances. You can find my configuration down below 252 | in the IAM role configuration sections with the one labeled `ECS` 253 | 254 | - The service role for CodeDeploy should be the same one you created in step 1. 255 | It should also, probably, be the only one in that dropdown. 256 | 257 | - Configure Network 258 | 259 | - Choose application load balancer 260 | - Health check grace period should be 15 seconds. This option is above the "choose ALB." 261 | - For Service IAM Role, I chose AWSServiceRoleForECS. Idk if that'll appear for you though. 262 | - Select your load balancer 263 | - Container to Load Balance: 264 | 265 | - Make sure the container name and port is nginx:80 266 | - Then press `Add to Load Balancer` 267 | 268 | - Disable test listener 269 | 270 | - Choose the target groups you made when making your ALB 271 | for Target Group 1 and create a second target group. 272 | - Service discovery 273 | 274 | - Enable it since you've got a website 275 | - Create a new, verbose private namespace. 276 | 277 | - You want something unique... like cookiecutter-django-namespace1 278 | - The namespace name can just be left as local 279 | 280 | - The cluster VPC should be the one you had all along. 281 | 282 | - Enable ECS task health propagation 283 | - DNS records for service discovery should have the 284 | container with nginx and TTL be 60 seconds. 285 | 286 | - Autoscaling policy. I didn't touch it and just said "Do not adjust". 287 | You can adjust it later. (I honestly have no idea myself. You shouldn't 288 | need to worry about it yet anyways). 289 | - Review and press that shiny blue button to create the service. 290 | 291 | 11. Change your health target ports 292 | 293 | While you're creating the service, the review stage should show your 294 | new target groups. If not, it's fine. The task will stop and regenerate. 295 | 296 | Right click on each target group and change the success codes at the bottom 297 | from `200` to `200,301` (you cannot add spaces). 298 | 299 | 12. Let's add our environment variables. 300 | 301 | Search up Systems Manager. Look for Parameter Store on the left side. 302 | You'll need to add the parameters from `.envs/.production/template.django`. 303 | 304 | I've noted which ones you should add. 305 | 306 | 13. Finally, commit to your repository and let your code be deployed. 307 | 308 | Cleanup 309 | ------- 310 | 311 | If you tested this first on a random GitHub repository, here's how to clean 312 | those resources up: 313 | 314 | - You should delete your created IAM roles or users for this test 315 | - Delete your GitHub secrets 316 | - Delete your AWS services. Here's a list, in order, of deletion: 317 | 318 | - Application Load Balancer 319 | - Target Groups 320 | - EC2 Instances 321 | - ECS Service 322 | - ECS Cluster 323 | - Task definition 324 | - CodeDeploy application 325 | - AWS Cloud Map namespace 326 | 327 | The Caveats in THIS EXAMPLE (easily avoidable) 328 | ---------------------------------------------- 329 | 330 | I didn't want to make ANOTHER image just for Celery; instead, I just used: 331 | 332 | .. code-block:: shell 333 | 334 | >> celery multi start -A config.celery_app worker beat 335 | 336 | I use Sentry to log all my Celery stuff, anyways, and it will come with 337 | cookiecutter-django if you opt-in. 338 | 339 | I also use RDS for PostgreSQL and ElastiCache for Redis. You don't HAVE to, 340 | but that would mean you need to configure some more stuff in the 341 | aws-task-definitions.json. 342 | 343 | In the task definition, you can easily add the redis and PostgreSQL images. If you 344 | follow the GitHub action of how I set up everything and how you can easily use the 345 | Dockerfile in compose/production/postgres, then just follow how I did the Django app. 346 | 347 | Initial Cookiecutter Generation 348 | ------------------------------- 349 | .. code-block:: shell 350 | 351 | project_name [My Awesome Project]: AWS ECS Deploy 352 | project_slug [aws_ecs_deploy]: 353 | description [Behold My Awesome Project!]: AWS ECS Deploy (hopefully with Blue/Green) using GitHub Actions 354 | author_name [Daniel Roy Greenfeld]: Andrew Chen Wang 355 | domain_name [example.com]: 356 | email [andrew-chen-wang@example.com]: acwangpython@gmail.com 357 | version [0.1.0]: 358 | Select open_source_license: 359 | 1 - MIT 360 | 2 - BSD 361 | 3 - GPLv3 362 | 4 - Apache Software License 2.0 363 | 5 - Not open source 364 | Choose from 1, 2, 3, 4, 5 [1]: 4 365 | timezone [UTC]: 366 | windows [n]: 367 | use_pycharm [n]: 368 | use_docker [n]: y 369 | Select postgresql_version: 370 | 1 - 11.3 371 | 2 - 10.8 372 | 3 - 9.6 373 | 4 - 9.5 374 | 5 - 9.4 375 | Choose from 1, 2, 3, 4, 5 [1]: 376 | Select js_task_runner: 377 | 1 - None 378 | 2 - Gulp 379 | Choose from 1, 2 [1]: 380 | Select cloud_provider: 381 | 1 - AWS 382 | 2 - GCP 383 | 3 - None 384 | Choose from 1, 2, 3 [1]: 385 | Select mail_service: 386 | 1 - Mailgun 387 | 2 - Amazon SES 388 | 3 - Mailjet 389 | 4 - Mandrill 390 | 5 - Postmark 391 | 6 - Sendgrid 392 | 7 - SendinBlue 393 | 8 - SparkPost 394 | 9 - Other SMTP 395 | Choose from 1, 2, 3, 4, 5, 6, 7, 8, 9 [1]: 2 396 | use_drf [n]: 397 | custom_bootstrap_compilation [n]: 398 | use_compressor [n]: 399 | use_celery [n]: y 400 | use_mailhog [n]: 401 | use_sentry [n]: 402 | use_whitenoise [n]: 403 | use_heroku [n]: 404 | Select ci_tool: 405 | 1 - None 406 | 2 - Travis 407 | 3 - Gitlab 408 | Choose from 1, 2, 3 [1]: 409 | keep_local_envs_in_vcs [y]: 410 | debug [n]: 411 | 412 | Minimal IAM Credentials for ECS 413 | ------------------------------- 414 | 415 | You'll need these permissions for your ECS: 416 | - S3 Full Access 417 | 418 | Minimal IAM Credentials for Deployment 419 | -------------------------------------- 420 | 421 | You're probably thinking... wtf is with all these brackets. 422 | Security. Besides that, you can use asterisks for demonstration 423 | for demonstration purposes. 424 | 425 | For me, during testing, I just used FullAccess... Shh... 426 | 427 | .. code-block:: json 428 | 429 | { 430 | "Version":"2012-10-17", 431 | "Statement":[ 432 | { 433 | "Sid":"RegisterTaskDefinition", 434 | "Effect":"Allow", 435 | "Action":[ 436 | "ecs:RegisterTaskDefinition" 437 | ], 438 | "Resource":"*" 439 | }, 440 | { 441 | "Sid":"PassRolesInTaskDefinition", 442 | "Effect":"Allow", 443 | "Action":[ 444 | "iam:PassRole" 445 | ], 446 | "Resource":[ 447 | "arn:aws:iam:::role/", 448 | "arn:aws:iam:::role/" 449 | ] 450 | }, 451 | { 452 | "Sid":"DeployService", 453 | "Effect":"Allow", 454 | "Action":[ 455 | "ecs:DescribeServices", 456 | "ecs:UpdateService", 457 | "codedeploy:GetDeploymentGroup", 458 | "codedeploy:CreateDeployment", 459 | "codedeploy:GetDeployment", 460 | "codedeploy:GetDeploymentConfig", 461 | "codedeploy:RegisterApplicationRevision" 462 | ], 463 | "Resource":[ 464 | "arn:aws:ecs:::service//", 465 | "arn:aws:codedeploy:::deploymentgroup:/", 466 | "arn:aws:codedeploy:::deploymentconfig:*", 467 | "arn:aws:codedeploy:::application:" 468 | ] 469 | } 470 | ] 471 | } 472 | 473 | FAQ 474 | --- 475 | 476 | How do I add celery? 477 | 478 | Go to `compose/production/ecs/django/start` and add the line 479 | 480 | `celery multi start worker beat -A config.celery_app` 481 | 482 | If you'd like to troubleshoot your AWS actions, add the 483 | secret `ACTION_STEP_DEBUG` with value `true` to your GitHub repo. 484 | 485 | Here is the AWS action doc specifying this https://github.com/aws-actions/amazon-ecs-deploy-task-definition#troubleshooting 486 | 487 | What's this license? 488 | 489 | Apache 2.0 490 | 491 | Best practices? 492 | 493 | Rotate your keys! 494 | 495 | What if I mess up creating the ECS service? 496 | 497 | Got something there's a service already here? I did too, 498 | lol. Search up AWS Cloud Map. Delete the one that says `local`. 499 | 500 | You may also have to go to CodeDeploy and delete the Application there, too. 501 | 502 | Are you experienced in AWS? 503 | 504 | Absolutely not. This would be my first time actually using AWS besides 505 | self hosting on one instace. This was just a nice learning experience that seems sooooo 506 | painful for start ups. In other words, STARTUPS! Get moving! I just gave 507 | you a free repo to copy off of :) 508 | 509 | I did play around with AWS trying to use the default cookiecutter-django 510 | before which is why I didn't know how I set up ACM in the first place. It 511 | worked after a painful 12 hours of trying to figure out wtf was going wrong. 512 | 513 | Why do you like typing so much? 514 | 515 | I like to train my fingers. 516 | 517 | Plus, it's nice seeing my painful moments and learning from them. 518 | It's like the cliche standing back and being proud of your work. 519 | 520 | But this was a painful 10 hours... I started at 12 and now it's 22:11. 521 | 522 | What did you learn from this? 523 | 524 | Always start small. On 10 June 2020, I finally figured to try and start 525 | small with a single EC2 with a load balancer (however, I will admit that 526 | I suspected the security groups was an issue for the most part). 527 | 528 | On the same day, I finally got it to work. So, always start small, and 529 | then try out this methodology. 530 | --------------------------------------------------------------------------------