From d02180c9bf91f48c6103758acd09ccfc19fd3453 Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Thu, 19 Oct 2023 15:20:36 -0300 Subject: [PATCH] Refactor (infra): Changes the strategy for deploying, removes traefik and uses nginx-proxy. The previous strategy had traefik and nginx (nginx just serving the static files). --- README.md | 58 ++++++++++++- infra/nginx/conf.d/custom_proxy.conf | 1 + infra/nginx/conf.d/fallback_server.conf | 8 ++ infra/nginx/nginx.conf.template | 51 ----------- infra/server/3rd_party/production.yml | 19 +++++ infra/server/3rd_party/staging.yml | 34 ++++++++ infra/server/start_nginx_acme.sh | 30 +++++++ infra/traefik/traefik.Dockerfile | 3 - infra/traefik/traefik.yml | 43 ---------- local.yml | 3 +- production.yml | 86 +++---------------- staging.yml | 109 +++--------------------- 12 files changed, 171 insertions(+), 274 deletions(-) create mode 100644 infra/nginx/conf.d/custom_proxy.conf create mode 100644 infra/nginx/conf.d/fallback_server.conf delete mode 100644 infra/nginx/nginx.conf.template create mode 100644 infra/server/3rd_party/production.yml create mode 100644 infra/server/3rd_party/staging.yml create mode 100644 infra/server/start_nginx_acme.sh delete mode 100644 infra/traefik/traefik.Dockerfile delete mode 100644 infra/traefik/traefik.yml diff --git a/README.md b/README.md index 61d35ee..803a772 100644 --- a/README.md +++ b/README.md @@ -811,9 +811,9 @@ Após alguns segundos você terá duas instâncias da aplicação Django rodando ## Deploy -Normalmente o deploy das aplicações são realizados em uma VPS, como a Amazon EC2. O Boilerplate já vem configurado com CI/CD para o ambiente de Staging, porém é simples copiar e criar um outro worfklow para o ambiente de produção. +Normalmente o deploy das aplicações são realizados em uma VPS, como a Amazon EC2. O Boilerplate já vem configurado com CI/CD para o ambiente de Staging e Produção. Porém o primeiro deploy é necessário a configuração pelo usuário. -> Apesar de apenas copiar e colar o workflow seja o suficiente para o ambiente de produção, perceba que o deploy atual possui downtime de cerca de 30 segundos. +> O formato de deploy atual possui downtime de cerca de 5 segundos. Antes do primeiro deploy por CI/CD será necessário: - Criar a máquina virtual; @@ -821,13 +821,63 @@ Antes do primeiro deploy por CI/CD será necessário: - Instalar as dependências (git, docker compose) na máquina; - Configurar o acesso via SSH ao Github (quando criar sua chave SSH não coloque um passphrase, caso contrário o processo de CD irá falhar); - Configurar as variáveis de ambiente; -- Subir a aplicação manualmente. +- [Subir a aplicação manualmente](#subir-a-aplicação-pela-primeira-vez). Após estes passos, o fluxo de CI/CD fará deploys automaticamente, após a configuração das Secrets no Github (Dentro do repositório -> `Settings` -> `Secrets`). -Vá até o arquivo [deploy_to_staging.yml](./.github/workflows/deploy_to_staging.yml) e verifique as variáveis de ambiente necessárias. Elas provavelmente são: +Vá até o arquivo [deploy_to_staging.yml](./.github/workflows/deploy_to_staging.yml) e verifique as variáveis de ambiente necessárias. Elas são: - `STAGING_SSH_PRIVATE_KEY`: Chave SSH Privada utilizada para conectar na máquina - `STAGING_SSH_HOSTNAME`: Nome do HOST para acesso SSH - `STAGING_USER_NAME`: Nome do usuário da máquina para acesso SSH > Perceba que os valores das variáveis de ambiente são prefixados para o ambiente de deploy, caso esteja criando um novo arquivo de deploy para outro ambiente, garanta que as variáveis estarão prefixadas com o nome do ambiente. Exemplo: `PRODUCTION_SSH_PRIVATE_KEY`. + +Para o ambiente de produção, o fluxo é praticamente o mesmo, porém as variáveis de ambiente são: +- `PRODUCTION_SSH_PRIVATE_KEY`: Chave SSH Privada utilizada para conectar na máquina +- `PRODUCTION_SSH_HOSTNAME`: Nome do HOST para acesso SSH +- `PRODUCTION_USER_NAME`: Nome do usuário da máquina para acesso SSH + + +### Subir a aplicação pela primeira vez + +Para subir a aplicação pela primeira vez será necessário subir alguns serviços na máquina, porém já existem scripts que fazem a maior parte do trabalho. +Primeiro passo é rodar o script [start_nginx_acme.sh](./infra/server/start_nginx_acme.sh) a partir do root do repositório: +```sh +./infra/server/start_nginx_acme.sh +``` +Esse comando irá iniciar dois containers: o [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy) e [nginx-proxy-acme](https://github.com/nginx-proxy/acme-companion). Que tem como objetivo: +- nginx-proxy: Reverse-proxy que ficará na frente da aplicação Django e demais serviços; +- nginx-proxy-acme: Configura automaticamente HTTPs; + +Após isso, será necessário subir a infraestrutura de serviços de terceiros, dependendo do ambiente. + +Para o ambiente de staging, utilize o compose [infra/server/3rd_party/staging.yml](./infra/server/3rd_party/staging.yml), com o comando: +```sh +docker compose -f ./infra/server/3rd_party/staging.yml --env-file .env up -d +``` + +Para o ambiente de produção, utilize o compose [infra/server/3rd_party/production.yml](./infra/server/3rd_party/production.yml), com o comando: +```sh +docker compose -f ./infra/server/3rd_party/production.yml --env-file .env up -d +``` + +Após isso é possível subir o compose da aplicação de acordo com o ambiente. + +Para o ambiente de staging, utilize o compose [staging.yml](./staging.yml), com o comando: +```sh +docker compose -f staging.yml --env-file .env build --no-cache +docker compose -f staging.yml --env-file .env up -d +``` + +Para o ambiente de produção, utilize o compose [production.yml](./production.yml), com o comando: +```sh +docker compose -f production.yml --env-file .env build --no-cache +docker compose -f production.yml --env-file .env up -d +``` + +Feito! A aplicação estará no ar. Após isso o processo de CI/CD irá fazer deploys automaticamente na máquina. + + +### Detalhes sobre os ambientes de staging/produção + +Os arquivos estáticos são servidos pelo nginx em qualquer domínio! Não encontramos um jeito melhor de servir os arquivos estáticos sem ser configurando um fallback para quando não é possível se conectar com um container. Essa configuração está definida no arquivo [infra/nginx/conf.d/fallback_server.conf](./infra/nginx/conf.d/fallback_server.conf). diff --git a/infra/nginx/conf.d/custom_proxy.conf b/infra/nginx/conf.d/custom_proxy.conf new file mode 100644 index 0000000..86bda51 --- /dev/null +++ b/infra/nginx/conf.d/custom_proxy.conf @@ -0,0 +1 @@ +client_max_body_size 100m; diff --git a/infra/nginx/conf.d/fallback_server.conf b/infra/nginx/conf.d/fallback_server.conf new file mode 100644 index 0000000..d01f612 --- /dev/null +++ b/infra/nginx/conf.d/fallback_server.conf @@ -0,0 +1,8 @@ +server { + listen 80 default_server; + root /usr/share/nginx/static; + + location /static { + alias /usr/share/nginx/static; + } +} diff --git a/infra/nginx/nginx.conf.template b/infra/nginx/nginx.conf.template deleted file mode 100644 index 24525d9..0000000 --- a/infra/nginx/nginx.conf.template +++ /dev/null @@ -1,51 +0,0 @@ -worker_processes 1; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - -events { - worker_connections 1024; # increase if you have lots of clients -} - -http { - include mime.types; - # fallback in case we can't determine a type - default_type application/octet-stream; - sendfile on; - - - server { - # if no Host match, close the connection to prevent host spoofing - listen 80 default_server; - return 444; - } - - server { - listen 80; - client_max_body_size 4G; - - server_name ${SERVER_NAME}; - - keepalive_timeout 5; - - # path for static files - root /app/storage/static; - - location /static { - # checks for static file - expires max; - gzip on; - gunzip on; - gzip_types text/css application/javascript image/png font/woff2; - alias /app/storage/static; - } - location /media { - # checks for media file - expires max; - gzip on; - gunzip on; - gzip_types text/css application/javascript image/png font/woff2; - alias /app/storage/media; - } - } -} diff --git a/infra/server/3rd_party/production.yml b/infra/server/3rd_party/production.yml new file mode 100644 index 0000000..51b7721 --- /dev/null +++ b/infra/server/3rd_party/production.yml @@ -0,0 +1,19 @@ +version: "3.3" + +services: + redis: + image: "redis" + ports: + - "${REDIS_PORT}:${REDIS_PORT}" + command: redis-server --requirepass ${REDIS_PASSWORD} --replicaof no one --replica-read-only no + volumes: + - cache:/data + networks: + - shared + +volumes: + cache: + +networks: + shared: + external: true diff --git a/infra/server/3rd_party/staging.yml b/infra/server/3rd_party/staging.yml new file mode 100644 index 0000000..b32f3ad --- /dev/null +++ b/infra/server/3rd_party/staging.yml @@ -0,0 +1,34 @@ +version: "3.3" + +services: + redis: + image: "redis" + ports: + - "${REDIS_PORT}:${REDIS_PORT}" + command: redis-server --requirepass ${REDIS_PASSWORD} --replicaof no one --replica-read-only no + volumes: + - cache:/data + networks: + - shared + + db: + image: postgres:16-alpine + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "${SQL_PORT}:${SQL_PORT}" + command: -p ${SQL_PORT} + environment: + POSTGRES_PASSWORD: ${SQL_PASSWORD} + POSTGRES_USER: ${SQL_USER} + POSTGRES_DB: ${SQL_DATABASE} + networks: + - shared + +volumes: + cache: + postgres_data: + +networks: + shared: + external: true diff --git a/infra/server/start_nginx_acme.sh b/infra/server/start_nginx_acme.sh new file mode 100644 index 0000000..093785b --- /dev/null +++ b/infra/server/start_nginx_acme.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +echo "Creating the shared network..." +docker network create shared + +echo "Starting nginx-proxy..." +docker run --detach \ + --name nginx-proxy \ + --publish 80:80 \ + --publish 443:443 \ + --volume certs:/etc/nginx/certs \ + --volume vhost:/etc/nginx/vhost.d \ + --volume html:/usr/share/nginx/html \ + --volume /var/run/docker.sock:/tmp/docker.sock:ro \ + --volume ./infra/nginx/conf.d/custom_proxy.conf:/etc/nginx/conf.d/custom_proxy.conf \ + --volume ./infra/nginx/conf.d/fallback_server.conf:/etc/nginx/conf.d/fallback_server.conf \ + --volume staticfiles:/usr/share/nginx/static \ + --network shared \ + nginxproxy/nginx-proxy + +echo "Starting nginx-proxy-acme..." +docker run --detach \ + --name nginx-proxy-acme \ + --volumes-from nginx-proxy \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --volume acme:/etc/acme.sh \ + --env "DEFAULT_EMAIL=leandrodesouzadev@gmail.com" \ + --network shared \ + nginxproxy/acme-companion diff --git a/infra/traefik/traefik.Dockerfile b/infra/traefik/traefik.Dockerfile deleted file mode 100644 index 6034228..0000000 --- a/infra/traefik/traefik.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM traefik:v2.4 - -COPY infra/traefik/traefik.yml /etc/traefik/ diff --git a/infra/traefik/traefik.yml b/infra/traefik/traefik.yml deleted file mode 100644 index e4f60cd..0000000 --- a/infra/traefik/traefik.yml +++ /dev/null @@ -1,43 +0,0 @@ -global: - sendAnonymousUsage: false - -providers: - docker: {} - -entryPoints: - http: - address: ":80" - - https: - address: ":443" - -certificatesResolvers: - ssl: - acme: - httpChallenge: - entryPoint: http - email: leandrodesouzadev@gmail.com - storage: /letsencrypt/acme.json - -accessLog: - filePath: "/traefiklogs/access.log" - format: json - fields: - defaultMode: drop - names: - ClientHost: keep - DownstreamStatus: keep - DownstramContentSize: keep - OriginDuration: keep - OriginStatus: keep - RequestMethod: keep - RequestPath: keep - RequestProtocol: keep - ServiceName: keep - StartUTC: keep - - headers: - defaultMode: drop - names: - User-Agent: keep - Content-Type: keep diff --git a/local.yml b/local.yml index 538ff6b..621f4e7 100644 --- a/local.yml +++ b/local.yml @@ -23,8 +23,6 @@ services: command: python3 manage.py runserver_plus 0.0.0.0:${DJANGO_HTTP_PORT:-8000} depends_on: - db: - condition: service_started redis: condition: service_started mailpit: @@ -73,4 +71,5 @@ services: volumes: postgres_data: + mediafiles: cache: diff --git a/production.yml b/production.yml index dcc81b4..2fcc969 100644 --- a/production.yml +++ b/production.yml @@ -1,41 +1,9 @@ version: "3.3" services: - redis: - image: "redis" - ports: - - "${REDIS_PORT}:${REDIS_PORT}" - command: redis-server --requirepass ${REDIS_PASSWORD} --replicaof no one --replica-read-only no - volumes: - - cache:/data - labels: - traefik.enable: "false" - env_file: - - .env - - traefik: - build: - dockerfile: infra/traefik/traefik.Dockerfile - context: . - command: - - "--configFile=traefik.yml" - - "--providers.docker=true" - ports: - - "443:443" - - "80:80" - volumes: - - "./infra/traefik/traefik.yml:/traefik.yml" - - "./storage/letsencrypt:/letsencrypt" - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - "./storage/traefiklogs:/traefiklogs" - django: &django command: python3 -m gunicorn --bind 0.0.0.0:80 app.wsgi:application depends_on: - traefik: - condition: service_started - redis: - condition: service_started db_migration: condition: service_completed_successfully build: @@ -48,21 +16,11 @@ services: env_file: - .env environment: - - REDIS_HOST=redis - labels: - traefik.enable: true - traefik.http.routers.django.rule: Host(`${HOST}`) - traefik.http.routers.django.entrypoints: http - traefik.http.routers.django.middlewares: redirect - traefik.http.middlewares.redirect.redirectscheme.scheme: https - traefik.http.routers.django-secure.rule: Host(`${HOST}`) - traefik.http.routers.django-secure.entrypoints: https - traefik.http.routers.django-secure.tls.certresolver: ssl - traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto: https - traefik.http.services.django-service.loadbalancer.healthcheck.path: /health-check - traefik.http.services.django-service.loadbalancer.healthcheck.timeout: 2s - traefik.http.services.django-service.loadbalancer.healthcheck.interval: 5s - traefik.http.services.django-service.loadbalancer.healthcheck.hostname: ${HOST} + REDIS_HOST: redis + VIRTUAL_HOST: ${HOST} + LETSENCRYPT_HOST: ${HOST} + networks: + - shared db_migration: <<: *django @@ -75,40 +33,18 @@ services: <<: *django command: python -m celery -A app beat ports: [] - labels: - - "traefik.enable=false" celery-worker: <<: *django command: python -m celery -A app worker --events ports: [] - labels: - - "traefik.enable=false" - - nginx: - depends_on: - - traefik - image: nginx:alpine - expose: - - 80 - env_file: - - .env - volumes: - - staticfiles:/app/storage/static - - ./infra/nginx:/tmp - environment: - SERVER_NAME: ${HOST} - command: /bin/sh -c "envsubst '$${SERVER_NAME}' < /tmp/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'" - labels: - traefik.enable: "true" - traefik.http.routers.nginx.rule: Host(`${HOST}`) && (PathPrefix(`/static`) || PathPrefix(`/media`)) - traefik.http.routers.nginx.entrypoints: http - traefik.http.services.nginx.loadbalancer.server.port: "80" - traefik.http.routers.nginx-secure.rule: Host(`${HOST}`) && (PathPrefix(`/static`) || PathPrefix(`/media`)) - traefik.http.routers.nginx-secure.entrypoints: https - traefik.http.routers.nginx-secure.tls.certresolver: ssl - traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto: https volumes: cache: staticfiles: + external: true + name: staticfiles + +networks: + shared: + external: true diff --git a/staging.yml b/staging.yml index 8c13412..7e33f1f 100644 --- a/staging.yml +++ b/staging.yml @@ -1,45 +1,11 @@ version: "3.3" services: - redis: - image: "redis" - ports: - - "${REDIS_PORT}:${REDIS_PORT}" - command: redis-server --requirepass ${REDIS_PASSWORD} --replicaof no one --replica-read-only no - volumes: - - cache:/data - labels: - traefik.enable: "false" - env_file: - - .env - - traefik: - build: - dockerfile: infra/traefik/traefik.Dockerfile - context: . - command: - - "--configFile=traefik.yml" - - "--providers.docker=true" - ports: - - "443:443" - - "80:80" - volumes: - - "./infra/traefik/traefik.yml:/traefik.yml" - - "./storage/letsencrypt:/letsencrypt" - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - "./storage/traefiklogs:/traefiklogs" - django: &django command: python3 -m gunicorn --bind 0.0.0.0:80 app.wsgi:application depends_on: - traefik: - condition: service_started - redis: - condition: service_started db_migration: condition: service_completed_successfully - db: - condition: service_started build: context: . dockerfile: infra/app/Dockerfile @@ -50,84 +16,35 @@ services: env_file: - .env environment: - - REDIS_HOST=redis - - SQL_HOST=db - labels: - traefik.enable: true - traefik.http.routers.django.rule: Host(`${HOST}`) - traefik.http.routers.django.entrypoints: http - traefik.http.routers.django.middlewares: redirect - traefik.http.middlewares.redirect.redirectscheme.scheme: https - traefik.http.routers.django-secure.rule: Host(`${HOST}`) - traefik.http.routers.django-secure.entrypoints: https - traefik.http.routers.django-secure.tls.certresolver: ssl - traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto: https - traefik.http.services.django-service.loadbalancer.healthcheck.path: /health-check - traefik.http.services.django-service.loadbalancer.healthcheck.timeout: 2s - traefik.http.services.django-service.loadbalancer.healthcheck.interval: 5s - traefik.http.services.django-service.loadbalancer.healthcheck.hostname: ${HOST} - - db: - image: postgres:16-alpine - volumes: - - postgres_data:/var/lib/postgresql/data - ports: - - "${SQL_PORT}:${SQL_PORT}" - - command: -p ${SQL_PORT} - environment: - POSTGRES_PASSWORD: ${SQL_PASSWORD} - POSTGRES_USER: ${SQL_USER} - POSTGRES_DB: ${SQL_DATABASE} + REDIS_HOST: redis + SQL_HOST: db + VIRTUAL_HOST: ${HOST} + LETSENCRYPT_HOST: ${HOST} + networks: + - shared db_migration: <<: *django command: python manage.py migrate entrypoint: ["sh", "/app/before_migrate.sh"] ports: [] - depends_on: - db: - condition: service_started + depends_on: [] celery-scheduler: <<: *django command: python -m celery -A app beat ports: [] - labels: - - "traefik.enable=false" celery-worker: <<: *django command: python -m celery -A app worker --events ports: [] - labels: - - "traefik.enable=false" - - nginx: - depends_on: - - traefik - image: nginx:alpine - expose: - - 80 - env_file: - - .env - volumes: - - staticfiles:/app/storage/static - - ./infra/nginx:/tmp - environment: - SERVER_NAME: ${HOST} - command: /bin/sh -c "envsubst '$${SERVER_NAME}' < /tmp/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'" - labels: - traefik.enable: "true" - traefik.http.routers.nginx.rule: Host(`${HOST}`) && (PathPrefix(`/static`) || PathPrefix(`/media`)) - traefik.http.routers.nginx.entrypoints: http - traefik.http.services.nginx.loadbalancer.server.port: "80" - traefik.http.routers.nginx-secure.rule: Host(`${HOST}`) && (PathPrefix(`/static`) || PathPrefix(`/media`)) - traefik.http.routers.nginx-secure.entrypoints: https - traefik.http.routers.nginx-secure.tls.certresolver: ssl - traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto: https volumes: - cache: staticfiles: - postgres_data: + external: true + name: staticfiles + +networks: + shared: + external: true