From 416d111e89270580725de288dbecf0c1ae70a56a Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Tue, 24 Sep 2019 15:42:48 -0500 Subject: [PATCH 01/12] Initialize Heroku deployment documentation --- README.md | 2 + heroku/README.md | 10 ++ heroku/deploy-a-django-app.md | 216 ++++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 heroku/README.md create mode 100644 heroku/deploy-a-django-app.md diff --git a/README.md b/README.md index 6f24014..c30fb13 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ _In alphabetical order and including links to external repository-based document - [Mapping](mapping/) - [Google APIs (Maps, Geocoding)](mapping/google-apis.md) - [Environment setup](environment-setup.md) +- [Heroku](/heroku/) + - [Deploy a Django app to Heroku](/heroku/deploy-a-django-app.md) - [PostgreSQL](postgres/) - [A quick and dirty introduction to `sqlalchemy`](postgres/quick-n-dirty-sqlalchemy.md) - [Interacting with a remote database](postgres/Interacting-with-a-remote-database.md) diff --git a/heroku/README.md b/heroku/README.md new file mode 100644 index 0000000..ae207f1 --- /dev/null +++ b/heroku/README.md @@ -0,0 +1,10 @@ +# Heroku + +This directory records best practices for working with [Heroku](https://heroku.com), +DataMade's preferred platform for hosting dynamic (i.e. non-static) applications. + +## Contents + +- [Deploy a Django app to Heroku](./deploy-a-django-app.md) +- [Research](./research/) + - [Recommendation of adoption](./research/recommendation-of-adoption.md) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md new file mode 100644 index 0000000..31978e2 --- /dev/null +++ b/heroku/deploy-a-django-app.md @@ -0,0 +1,216 @@ +# Deploy a Django app to Heroku + +This guide provides instructions on deploying a Django app to Heroku, DataMade's +preferred platform for hosting dynamic applications. + +## Contents + +- [Set up application code for Heroku](#set-up-application-code-for-heroku) + - [Containerize your app](#containerize-your-app) + - [Serve static files with WhiteNoise](#serve-static-files-with-whitenoise) + - [Read settings and secret variables from the environment](#read-settings-and-secret-variables-from-the-environment) +- [Provision Heroku resources](#provision-heroku-resources) + - [Ensure that the Heroku CLI is installed](#ensure-that-the-heroku-cli-is-installed) + - [Create Heroku config files](#create-heroku-config-files) + - [`heroku.yml`](#heroku.yml) + - [`release.sh`](#release.sh) + - [`app.json`](#app.json) + - [Create apps and pipelines for your project](#create-apps-and-pipelines-for-your-project) +- [Troubleshooting](#troubleshooting) + +## Set up application code for Heroku + +In order to deploy a Django application to Heroku, a few specific configurations +need to be enabled in your application code. We provide details on these +configurations below. + +### Containerize your app + +We use Heroku as a platform for deploying containerized apps, which means that +your app must be containerized in order to use Heroku properly. If your app +is not yet containerized, [follow our instructions for containerizing Django +apps](/docker/local-development.md) before moving on. + +### Serve static files with WhiteNoise + +Apps deployed on Heroku don't typically use Nginx to serve content, so they need some +other way of serving static files. Since our apps tend to have relatively low traffic, +we prefer configuring [WhiteNoise](http://whitenoise.evans.io/) +to allow Django to serve static files in production. + +Follow the [setup instructions for WhiteNoise in +Django](http://whitenoise.evans.io/en/stable/django.html) to ensure that your +Django apps can serve static files. + +### Read settings and secret variables from the environment + +Heroku doesn't allow us to decrypt content with GPG, so we can't use Blackbox +to decrypt application secrets. Instead, we can store these secrets as [config +vars](https://devcenter.heroku.com/articles/config-vars), which Heroku will thread +into our container environment. + +The three most basic config vars that you'll want to set for every app include +the Django `DEBUG`, `SECRET_KEY`, and `ALLOWED_HOSTS` variables. Update `settings.py` +to read these variables from the environment: + +```python +SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] +DEBUG = os.getenv('DJANGO_DEBUG', True) DEBUG = False if os.getenv('DJANGO_DEBUG', True) == 'False' else True +allowed_hosts = os.getenv('DJANGO_ALLOWED_HOSTS', []) +ALLOWED_HOSTS = allowed_hosts.split(',') if allowed_hosts else [] +``` + +Make sure to update your app service in `docker-compose.yml` to thread any variables +that don't have defaults into your local environment: + +```yml +services: + app: + environment: + - DJANGO_SECRET_KEY=really-super-secret +``` + +## Provision Heroku resources + +Once your application is properly configured for Heroku, the following instructions +will help you deploy your application to the platform. + +### Ensure that the Heroku CLI is installed + +The fastest way to get a project up and running on Heroku is to use the +[Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli). Before you start, +make sure you have the CLI installed locally. In addition, confirm that you have the +manifest plugin) installed: + +``` +heroku plugins | grep manifest +``` + +If you don't see any output, [follow the official instructions for installing the +plugin](https://devcenter.heroku.com/changelog-items/1441). + +### Create Heroku config files + +Define config files relative to the root of your repo. + +#### `heroku.yml` + +The `heroku.yml` manifest file tells Heroku how to build and network the services +and containers that comprise your application. This file should live in the root of your project repo. +For background and syntax, see [the +documentation](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml). +Use the following baseline to get started: + +```yml +# Define addons that you need for your project, such as Postgres, Redis, or Solr. +setup: + addons: + - plan: heroku-postgresql +# Define your application's Docker containers. +build: + docker: + web: Dockerfile +# Define any scripts that you'd like to run every time the app deploys. +release: + command: + - ./scripts/release.sh + image: web +# The command that runs your application. Replace 'app' with the name of your app. +run: + web: gunicorn -t 180 --log-level debug app.wsgi:application +``` + +#### `release.sh` + +In your app's `scripts` folder, define a script `release.sh` to run every time the app deploys. +Use the following baseline script: + +```bash +#!/bin/bash +python manage.py collectstatic --noinput +python manage.py migrate --noinput +``` + +#### `app.json` + +In order to enable [review apps](https://devcenter.heroku.com/articles/github-integration-review-apps) +for your project, the repo must contain an `app.json` config file in the root directory. +For background and syntax, see [the documentation](https://devcenter.heroku.com/articles/app-json-schema). +Use the following baseline to get started: + +```json +{ + "name": "your-app", + "description": "Short description of your app.", + "scripts": {}, + "env": { + "DATABASE_URL": { + "required": true + }, + "DJANGO_SECRET_KEY": { + "required": true + } + }, + "formation": { + "web": { + "quantity": 1, + "size": "hobby" + } + }, + "addons": [ + "heroku-postgresql" + ], + "buildpacks": [], + "stack": "container" +} +``` + +### Create apps and pipelines for your project + +In order to deploy your project, you need to create Heroku apps for staging +and production, and tie them together in a [pipeline](https://devcenter.heroku.com/articles/pipelines). +To create these resources, start by defining the name of your app: + +```bash +# This should be the same slug as your project's GitHub repo. +export APP_NAME= +``` + +Then, run the following Heroku CLI commands: + +```bash +heroku create ${APP_NAME} -t datamade --manifest +heroku pipelines:create -t datamade ${APP_NAME} -a ${APP_NAME} -s production +heroku create ${APP_NAME}-staging -t datamade --manifest +heroku pipelines:add ${APP_NAME}-staging -a ${APP_NAME}-staging -s staging +heroku pipelines:connect ${APP_NAME} -r datamade/${APP_NAME} +heroku reviewapps:enable -a ${APP_NAME}-staging -p ${APP_NAME} +``` + +Next, configure the GitHub integration to set up [automatic +deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys) +for both Heroku apps (staging and production). Choose "Wait for CI to pass before deploy" for each app. + +Finally, configure environment variables for both Heroku apps. These can be set +using either the dashboard or the CLI. [Follow the Heroku +documentation](https://devcenter.heroku.com/articles/config-vars#managing-config-vars) +to set up your config vars. + +## Troubleshooting + +### I see `Application error` or a `Welcome to Heroku` page on my site + +If your app isn't loading at all, check the dashboard to make sure that the most +recent build and release cycle passed. If the build and release both passed, +check the `Dyno formation` widget on the app `Overview` page to make sure that +dynos are enabled for your `web` process. + +### Debugging Heroku CLI errors + +Sometimes a Heroku CLI command will fail without showing much output (e.g. `Build failed`). +In these cases, you can set the following debug flag to show the raw HTTP responses +from the Heroku API: + +```bash +export HEROKU_DEBUG=1 +``` From 2b05ef66da2a9d2895479428aca2a354a75de662 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Fri, 27 Sep 2019 09:25:16 -0500 Subject: [PATCH 02/12] Fix typos in Heroku setup docs --- heroku/deploy-a-django-app.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 31978e2..028da32 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -55,7 +55,7 @@ to read these variables from the environment: ```python SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] -DEBUG = os.getenv('DJANGO_DEBUG', True) DEBUG = False if os.getenv('DJANGO_DEBUG', True) == 'False' else True +DEBUG = False if os.getenv('DJANGO_DEBUG', True) == 'False' else True allowed_hosts = os.getenv('DJANGO_ALLOWED_HOSTS', []) ALLOWED_HOSTS = allowed_hosts.split(',') if allowed_hosts else [] ``` @@ -144,9 +144,6 @@ Use the following baseline to get started: "description": "Short description of your app.", "scripts": {}, "env": { - "DATABASE_URL": { - "required": true - }, "DJANGO_SECRET_KEY": { "required": true } From 2382b6d06c91b31409386d03b476e91699e3f9f2 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Thu, 24 Oct 2019 16:07:21 -0500 Subject: [PATCH 03/12] Update Heroku docs with initial dbload instructions --- heroku/deploy-a-django-app.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 028da32..0a6b696 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -131,6 +131,16 @@ python manage.py collectstatic --noinput python manage.py migrate --noinput ``` +If your app uses a `dbload` script to load initial data into the database, you can use `release.sh` +to check if the initial data exists and run the data loading scripts if not. For example: + +```bash +# Set ${TABLE} to the name of a table that you expect to have data in it. +if [ `psql ${DATABASE_URL} -tAX -c "SELECT COUNT(*) FROM ${TABLE}"` -eq "0" ]; then + make all +fi +``` + #### `app.json` In order to enable [review apps](https://devcenter.heroku.com/articles/github-integration-review-apps) @@ -188,7 +198,7 @@ Next, configure the GitHub integration to set up [automatic deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys) for both Heroku apps (staging and production). Choose "Wait for CI to pass before deploy" for each app. -Finally, configure environment variables for both Heroku apps. These can be set +Finally, configure environment variables for staging, production, and review apps. These can be set using either the dashboard or the CLI. [Follow the Heroku documentation](https://devcenter.heroku.com/articles/config-vars#managing-config-vars) to set up your config vars. From ed0b26a66e2af46a91f11b1c87ee04485bb72349 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Tue, 5 Nov 2019 16:50:18 -0600 Subject: [PATCH 04/12] Update logging, static files, and deploy branch sections in heroku docs --- heroku/deploy-a-django-app.md | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 0a6b696..60eae8b 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -9,6 +9,7 @@ preferred platform for hosting dynamic applications. - [Containerize your app](#containerize-your-app) - [Serve static files with WhiteNoise](#serve-static-files-with-whitenoise) - [Read settings and secret variables from the environment](#read-settings-and-secret-variables-from-the-environment) + - [Configure Django loggine](#configure-django-logging) - [Provision Heroku resources](#provision-heroku-resources) - [Ensure that the Heroku CLI is installed](#ensure-that-the-heroku-cli-is-installed) - [Create Heroku config files](#create-heroku-config-files) @@ -42,6 +43,14 @@ Follow the [setup instructions for WhiteNoise in Django](http://whitenoise.evans.io/en/stable/django.html) to ensure that your Django apps can serve static files. +In order to be able to serve static files when `DEBUG` is `False`, you'll also want +to make sure that `RUN python manage.py collectstatic` is included as a step in your +Dockerfile. This will ensure that the static files are baked into the container. +Since we typically mount application code into the container during local development, +you'll also need to make sure that your static files are stored outside of the root +project folder so that the mounted files don't overwrite them. One easy way to do this +is to set `STATIC_ROOT = '/static'` in your `settings.py` file. + ### Read settings and secret variables from the environment Heroku doesn't allow us to decrypt content with GPG, so we can't use Blackbox @@ -70,6 +79,39 @@ services: - DJANGO_SECRET_KEY=really-super-secret ``` +### Configure Django logging + +When Gunicorn is running our app on Heroku, we generally want it to log to stdout +and stderr instead of logging to files, so that we can let Heroku capture the logs +and see view them with the `heroku logs` CLI command (or in the web console). + +By default, Django will not log errors to the console when `DEBUG` is `False` +([as documented here](https://docs.djangoproject.com/en/2.2/topics/logging/#django-s-default-logging-configuration)). +To make sure that errors get logged appropriately in Heroku, set the following +baseline logging settings in your `settings.py` file: + +```python +import os + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, # Preserve default loggers + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), + }, + }, +} +``` + +For more detail on Django's logging framework, [see the documentation](https://docs.djangoproject.com/en/2.2/topics/logging/). + ## Provision Heroku resources Once your application is properly configured for Heroku, the following instructions @@ -198,6 +240,13 @@ Next, configure the GitHub integration to set up [automatic deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys) for both Heroku apps (staging and production). Choose "Wait for CI to pass before deploy" for each app. +Heroku needs to deploy from specific branches in order to deploy to different environments +(e.g. staging vs. production). In order to properly enable automatic deployments, then, +you'll need to deploy to production from a branch instead of tagged commits (a practice +which we've used in the past for deploying to production). We recommend creating a long-lived +`deploy` branch off of `master` immediately after setting up your repo so that you can +use `master` to deploy to staging and `deploy` to deploy to production. + Finally, configure environment variables for staging, production, and review apps. These can be set using either the dashboard or the CLI. [Follow the Heroku documentation](https://devcenter.heroku.com/articles/config-vars#managing-config-vars) From 9921ecb2449e60aea6d0633b446f7d08643eeb90 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Fri, 3 Jan 2020 11:29:30 -0600 Subject: [PATCH 05/12] Update Heroku docs based on @hancush's feedback --- heroku/deploy-a-django-app.md | 101 ++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 60eae8b..30e9ebc 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -17,6 +17,9 @@ preferred platform for hosting dynamic applications. - [`release.sh`](#release.sh) - [`app.json`](#app.json) - [Create apps and pipelines for your project](#create-apps-and-pipelines-for-your-project) + - [Enable additional services](#enable-additional-services) + - [Solr](#solr) + - [PostGIS](#postgis) - [Troubleshooting](#troubleshooting) ## Set up application code for Heroku @@ -79,6 +82,12 @@ services: - DJANGO_SECRET_KEY=really-super-secret ``` +For a full example of this pattern in a production app, see the [Docker Compose +file](https://github.com/datamade/mn-election-archive/blob/master/docker-compose.yml) +and [Django settings +file](https://github.com/datamade/mn-election-archive/blob/master/elections/settings.py) +in the [UofM Election Archive project](https://github.com/datamade/mn-election-archive). + ### Configure Django logging When Gunicorn is running our app on Heroku, we generally want it to log to stdout @@ -165,7 +174,8 @@ run: #### `release.sh` In your app's `scripts` folder, define a script `release.sh` to run every time the app deploys. -Use the following baseline script: +Use the following baseline script and make sure to run `chmod u+x scripts/release.sh` +to make it executable: ```bash #!/bin/bash @@ -183,6 +193,10 @@ if [ `psql ${DATABASE_URL} -tAX -c "SELECT COUNT(*) FROM ${TABLE}"` -eq "0" ]; t fi ``` +Note that logs for the release phase won't be viewable in the Heroku console unless `curl` +is installed in your application container. Make sure your Dockerfile installs +`curl` to see logs as `scripts/release.sh` runs. + #### `app.json` In order to enable [review apps](https://devcenter.heroku.com/articles/github-integration-review-apps) @@ -225,17 +239,23 @@ To create these resources, start by defining the name of your app: export APP_NAME= ``` -Then, run the following Heroku CLI commands: +Then, run the following Heroku CLI commands to create a staging app and a pipeline: ```bash -heroku create ${APP_NAME} -t datamade --manifest -heroku pipelines:create -t datamade ${APP_NAME} -a ${APP_NAME} -s production heroku create ${APP_NAME}-staging -t datamade --manifest -heroku pipelines:add ${APP_NAME}-staging -a ${APP_NAME}-staging -s staging +heroku pipelines:create -t datamade ${APP_NAME} -a ${APP_NAME}-staging -s staging heroku pipelines:connect ${APP_NAME} -r datamade/${APP_NAME} heroku reviewapps:enable -a ${APP_NAME}-staging -p ${APP_NAME} ``` +If you would like to set up a production app as well, run the following commands +to create one and add it to your pipeline: + +```bash +heroku create ${APP_NAME} -t datamade --manifest +heroku pipelines:add ${APP_NAME} -a ${APP_NAME} -s production +``` + Next, configure the GitHub integration to set up [automatic deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys) for both Heroku apps (staging and production). Choose "Wait for CI to pass before deploy" for each app. @@ -252,6 +272,54 @@ using either the dashboard or the CLI. [Follow the Heroku documentation](https://devcenter.heroku.com/articles/config-vars#managing-config-vars) to set up your config vars. +## Enable additional services + +If your app requires additional services, like Solr or PostGIS, you'll need +to perform some extra steps to set them up for your pipeline. + +### Solr + +Solr can be configured as a separate service using the [Websolr +add-on for Heroku](https://devcenter.heroku.com/articles/websolr). We recommend +following the instructions for configuring [Websolr with +Haystack](https://devcenter.heroku.com/articles/websolr#haystack-for-django). +In addition to following these instructions, complete the following two steps: + +1. Update your `heroku.yml` and `app.json` config files to add `websolr` to your + add-ons configuration attributes, so Websolr will be enabled for review apps +2. Define `WEBSOLR_URL` as an environment variable for your `app` + service in your `docker-compose.yml` file in order to point your app to your + Solr service in local development + +For help setting up Haystack for local development, see [our guide to +Haystack](https://github.com/datamade/how-to/blob/master/search/03-heavyweight.md#getting-started). +For an example of a working Solr installation in a Heroku app, see the [`2.5_deploy` +branch of LA Metro Councilmatic](https://github.com/datamade/la-metro-councilmatic/tree/2.5_deploy). + +Note that the Websolr add-on [can be expensive](https://elements.heroku.com/addons/websolr#pricing), +with staging instances costing a minimum of $20/mo and the smallest production +instance costing $60/mo. Refer to our [guide to searching +data](https://github.com/datamade/how-to/blob/master/search/03-heavyweight.md#heavyweight) +to make sure you really need Solr before going forward with installing it on your project. + +### PostGIS + +If your app requires PostGIS, you'll need to [manually enable it in your +database](https://devcenter.heroku.com/articles/postgis). Once your database +has been provisioned, run the following command to connect to your database and +enable PostGIS: + +``` +heroku psql -a -c "CREATE EXTENSION postgis" +``` + +To automate this process, you can include a step like this in `scripts/release.sh` +to make sure PostGIS is always enabled in your databases: + +```bash +psql ${DATABASE_URL} -c "CREATE EXTENSION IF NOT EXISTS postgis" +``` + ## Troubleshooting ### I see `Application error` or a `Welcome to Heroku` page on my site @@ -261,7 +329,7 @@ recent build and release cycle passed. If the build and release both passed, check the `Dyno formation` widget on the app `Overview` page to make sure that dynos are enabled for your `web` process. -### Debugging Heroku CLI errors +### Heroku CLI commands are failing without useful error messages Sometimes a Heroku CLI command will fail without showing much output (e.g. `Build failed`). In these cases, you can set the following debug flag to show the raw HTTP responses @@ -270,3 +338,24 @@ from the Heroku API: ```bash export HEROKU_DEBUG=1 ``` + +### The release phase of my build is failing but the logs are empty + +Heroku can't stream release logs unless `curl` is installed in the application +container. Double-check to make sure your Dockerfile installs `curl`. + +If `curl` is installed and you still see no release logs, try viewing all of your app's logs by +running `heroku logs -a `. Typically this stream represents the most +complete archive of logs for an app. + +### I need to connect to an app database from the command line + +The Heroku CLI provides a command, `heroku psql`, that you can use to connect +to your database in a `psql` shell. See the [docs for using this +command](https://devcenter.heroku.com/articles/connecting-to-heroku-postgres-databases-from-outside-of-heroku). + +### I need to share a database between multiple apps + +You can use the Heroku CLI to accomplish this task. See the Heroku docs on +[sharing databases between +applications](https://devcenter.heroku.com/articles/heroku-postgresql#sharing-heroku-postgres-between-applications). From 4f43561f16e36015e03444303aecef94afe8a475 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Fri, 24 Jan 2020 11:46:16 -0600 Subject: [PATCH 06/12] Add a note about leaving "Wait for CI to pass" off --- heroku/deploy-a-django-app.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 30e9ebc..5ecbf10 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -258,7 +258,11 @@ heroku pipelines:add ${APP_NAME} -a ${APP_NAME} -s production Next, configure the GitHub integration to set up [automatic deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys) -for both Heroku apps (staging and production). Choose "Wait for CI to pass before deploy" for each app. +for both Heroku apps (staging and production). Ideally we would choose +"Wait for CI to pass before deploy" for each app, but in our experience so far it will +prevent Heroku from automatically creating new review apps for each new PR, so for now +we recommend leaving it off. We have an open ticket with Heroku support and will update +this recommendation in the future if the situation changes. Heroku needs to deploy from specific branches in order to deploy to different environments (e.g. staging vs. production). In order to properly enable automatic deployments, then, From 0f56fe95bfa1323dfc82ceef8028fc861a2b3841 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Fri, 14 Feb 2020 15:19:08 -0600 Subject: [PATCH 07/12] Fix links in Heroku Contents Co-Authored-By: Hannah Cushman --- heroku/deploy-a-django-app.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 5ecbf10..cb30e2a 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -9,13 +9,13 @@ preferred platform for hosting dynamic applications. - [Containerize your app](#containerize-your-app) - [Serve static files with WhiteNoise](#serve-static-files-with-whitenoise) - [Read settings and secret variables from the environment](#read-settings-and-secret-variables-from-the-environment) - - [Configure Django loggine](#configure-django-logging) + - [Configure Django logging](#configure-django-logging) - [Provision Heroku resources](#provision-heroku-resources) - [Ensure that the Heroku CLI is installed](#ensure-that-the-heroku-cli-is-installed) - [Create Heroku config files](#create-heroku-config-files) - - [`heroku.yml`](#heroku.yml) - - [`release.sh`](#release.sh) - - [`app.json`](#app.json) + - [`heroku.yml`](#herokuyml) + - [`release.sh`](#releasesh) + - [`app.json`](#appjson) - [Create apps and pipelines for your project](#create-apps-and-pipelines-for-your-project) - [Enable additional services](#enable-additional-services) - [Solr](#solr) From d405e3d392ea7328d31c9dae4c2b024537d8ddb6 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Fri, 14 Feb 2020 15:48:15 -0600 Subject: [PATCH 08/12] Clarify setup of Heroku review apps, config vars, and Slack notifications --- heroku/deploy-a-django-app.md | 85 +++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index cb30e2a..dcc613f 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -6,20 +6,21 @@ preferred platform for hosting dynamic applications. ## Contents - [Set up application code for Heroku](#set-up-application-code-for-heroku) - - [Containerize your app](#containerize-your-app) - - [Serve static files with WhiteNoise](#serve-static-files-with-whitenoise) - - [Read settings and secret variables from the environment](#read-settings-and-secret-variables-from-the-environment) - - [Configure Django logging](#configure-django-logging) + - [Containerize your app](#containerize-your-app) + - [Serve static files with WhiteNoise](#serve-static-files-with-whitenoise) + - [Read settings and secret variables from the environment](#read-settings-and-secret-variables-from-the-environment) + - [Configure Django logging](#configure-django-logging) - [Provision Heroku resources](#provision-heroku-resources) - - [Ensure that the Heroku CLI is installed](#ensure-that-the-heroku-cli-is-installed) - - [Create Heroku config files](#create-heroku-config-files) - - [`heroku.yml`](#herokuyml) - - [`release.sh`](#releasesh) - - [`app.json`](#appjson) - - [Create apps and pipelines for your project](#create-apps-and-pipelines-for-your-project) - - [Enable additional services](#enable-additional-services) - - [Solr](#solr) - - [PostGIS](#postgis) + - [Ensure that the Heroku CLI is installed](#ensure-that-the-heroku-cli-is-installed) + - [Create Heroku config files](#create-heroku-config-files) + - [`heroku.yml`](#herokuyml) + - [`release.sh`](#releasesh) + - [`app.json`](#appjson) + - [Create apps and pipelines for your project](#create-apps-and-pipelines-for-your-project) +- [Set up Slack notifications](#set-up-slack-notifications) +- [Enable additional services](#enable-additional-services) + - [Solr](#solr) + - [PostGIS](#postgis) - [Troubleshooting](#troubleshooting) ## Set up application code for Heroku @@ -256,7 +257,44 @@ heroku create ${APP_NAME} -t datamade --manifest heroku pipelines:add ${APP_NAME} -a ${APP_NAME} -s production ``` -Next, configure the GitHub integration to set up [automatic +Once you have the environments you need, enable [review +apps](https://devcenter.heroku.com/articles/github-integration-review-apps) +for your pipeline: + +```bash +# If you only have a staging app, change the value of the -a flag to ${APP_NAME}-staging +# to set the staging app as the parent app for all review apps. +heroku reviewapps:enable -p ${APP_NAME} -a ${APP_NAME} +``` + +Next, configure environment variables for staging, production, and review apps. These can be set +using either the dashboard or the CLI. [Follow the Heroku +documentation](https://devcenter.heroku.com/articles/config-vars#managing-config-vars) +to set up your config vars. Note that you need to define config vars for each environment +in your application: for staging and production environments, you can define these +variables under `Settings > Config Vars` on the application page, but since review apps +don't have an application page until they're created, you can define config vars +that will be inherited by each review app by navigating to the pipeline home page +and visiting `Settings > Review Apps > Review app config vars` in the nav. + +If you followed the setup instructions in "[Read settings and secret variables from the +environment](#read-settings-and-secret-variables-from-the-environment)" above, +you should need to set at least the following variables: + +- `DJANGO_SECRET_KEY`: This needs to be a random string, unique to each environment. + We often use the [XKCD password generator](https://preshing.com/20110811/xkcd-password-generator/) + to generate random strings. +- `DJANGO_ALLOWED_HOSTS`: This variable should be a comma-separated list of hostnames + that are valid for your application. For review apps and the staging environment, + you can set this to `.herokuapp.com` to automatically safelist any subdomains + of the `herokuapp.com` root domain. + +Note that while `DATABASE_URL` is probably required by your application, you don't actually +need to set it yourself. The Heroku Postgres add-on will [automatically define this +variable](https://devcenter.heroku.com/articles/heroku-postgresql#designating-a-primary-database) +when it provisions a database for your application. + +After your config vars are set up, configure the GitHub integration to set up [automatic deploys](https://devcenter.heroku.com/articles/github-integration#automatic-deploys) for both Heroku apps (staging and production). Ideally we would choose "Wait for CI to pass before deploy" for each app, but in our experience so far it will @@ -271,10 +309,21 @@ which we've used in the past for deploying to production). We recommend creating `deploy` branch off of `master` immediately after setting up your repo so that you can use `master` to deploy to staging and `deploy` to deploy to production. -Finally, configure environment variables for staging, production, and review apps. These can be set -using either the dashboard or the CLI. [Follow the Heroku -documentation](https://devcenter.heroku.com/articles/config-vars#managing-config-vars) -to set up your config vars. +## Set up Slack notifications + +Heroku can send build notifications to Slack via the Heroku ChatOps integration. +This integration should already be set up in our Slack channel, but if you need +to install it again, see the [official documentation](https://devcenter.heroku.com/articles/chatops). + +To enable notifications for an app, run the following Slack command in the +corresponding channel: + +```bash +/h route ${PIPELINE_NAME} to ${CHANNEL_NAME} +``` + +For example, to enable notifications for the `parserator` pipeline in the `#parserator` +channel, we would run `/h route parserator to #parserator`. ## Enable additional services From e755dbb8262d57f1daefa3c2ab7c999f188edb94 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Fri, 14 Feb 2020 17:04:48 -0600 Subject: [PATCH 09/12] Clarify advice on Dockerfile setup for Heroku deployments --- heroku/deploy-a-django-app.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index dcc613f..0d9afb1 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -36,6 +36,18 @@ your app must be containerized in order to use Heroku properly. If your app is not yet containerized, [follow our instructions for containerizing Django apps](/docker/local-development.md) before moving on. +There are two commands you should make sure to add to your Dockerfile in order to +properly deploy with Heroku: + +1. In the section of your Dockerfile where you install OS-level dependencies with `apt-get`, + make sure to install `curl` so that Heroku can stream logs during releases (if you're + inheriting from the official `python` images, `curl` will already be installed by default) +2. Toward the end of your Dockerfile, run `python manage.py collecstatic --noinput` so that + static files will be baked into your container on deployment + +For an example of a Django Dockerfile that is properly set up for Heroku, see +the [Minnesota Election Archive project](https://github.com/datamade/mn-election-archive/blob/master/Dockerfile). + ### Serve static files with WhiteNoise Apps deployed on Heroku don't typically use Nginx to serve content, so they need some From f3e3acdf042914025c05960ec3c4423f43eaa882 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Fri, 14 Feb 2020 17:07:53 -0600 Subject: [PATCH 10/12] Update example Heroku config files --- heroku/deploy-a-django-app.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index 0d9afb1..d5e7b05 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -225,6 +225,9 @@ Use the following baseline to get started: "env": { "DJANGO_SECRET_KEY": { "required": true + }, + "DJANGO_ALLOWED_HOSTS": { + "required": true } }, "formation": { @@ -236,6 +239,11 @@ Use the following baseline to get started: "addons": [ "heroku-postgresql" ], + "environments": { + "review": { + "addons": ["heroku-postgresql:hobby-basic"] + } + }, "buildpacks": [], "stack": "container" } From 72d1732619a545f6574e88497263a95233e1a7b5 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Tue, 18 Feb 2020 11:59:06 -0600 Subject: [PATCH 11/12] Add docs for configuring custom domains in Heroku --- heroku/deploy-a-django-app.md | 71 +++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index d5e7b05..bd97ca2 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -21,6 +21,10 @@ preferred platform for hosting dynamic applications. - [Enable additional services](#enable-additional-services) - [Solr](#solr) - [PostGIS](#postgis) +- [Set up a custom domain](#set-up-a-custom-domain) + - [Step 1: Configure a custom domain on Heroku](#step-1-configure-a-custom-domain-on-heroku) + - [Step 2: Configure a custom domain on a DNS provider](#step-2-configure-a-custom-domain-on-a-dns-provider) + - [General guidelines for custom domains](#general-guidelines-for-custom-domains) - [Troubleshooting](#troubleshooting) ## Set up application code for Heroku @@ -393,6 +397,73 @@ to make sure PostGIS is always enabled in your databases: psql ${DATABASE_URL} -c "CREATE EXTENSION IF NOT EXISTS postgis" ``` +## Set up a custom domain + +All Heroku apps are automatically delegated a subdomain under the `heroku.com` +root domain, like `example.heroku.com`. This automatic Heroku subdomain +is usually fine for review apps and staging apps, but production apps almost +always require a dedicated custom domain like `example.com`. + +When you're ready to deploy to production and publish your app publicly, you'll +need to set up a custom domain. In order to do this, you need to register the custom +domain in two places: in the Heroku dashboard, and in your (or your client's) DNS provider. + +For detailed documentation on setting up custom domains, see the [Heroku +docs](https://devcenter.heroku.com/articles/custom-domains). + +### Step 1: Configure a custom domain on Heroku + +The first step to setting up a custom domain is to instruct Heroku to use the +domain for your app. Navigate to `Settings > Domains` in your app dashboard, choose +`Add domain`, and enter the name of the custom domain you would like to use. + +When you save the domain, Heroku should display the DNS target for your domain. +Copy this string and use it in the next step to delegate the domain with your +DNS provider. + +### Step 2: Configure a custom domain on a DNS provider + +_Note: If you're not comfortable with basic DNS terminology and you're finding this +section to be confusing, refer to the CloudFlare docs on [how DNS +works](https://www.cloudflare.com/learning/dns/what-is-dns/)._ + +Once you have a DNS target for Heroku, you need to instruct your DNS provider to +direct traffic for your custom domain to Heroku. + +If you're setting up a custom **subdomain**, like `www.example.com` or `app.example.com`, +you'll need to create a `CNAME` record pointing to your DNS target with your DNS +provider. For more details, see the Heroku docs on [configuring DNS for +subdomains](https://devcenter.heroku.com/articles/custom-domains#configuring-dns-for-subdomains). + +If you're setting up a custom **root domain**, like `example.com`, you'll need +to create the equivalent of an `ALIAS` record with your DNS provider. Not all +DNS providers offer the same type of `ALIAS` record, so to provision this record +you should visit the Heroku docs on [configuring DNS for root +domains](https://devcenter.heroku.com/articles/custom-domains#configuring-dns-for-root-domains) +and follow the instruction for your provider. At DataMade we typically use Namecheap, +which allows you to [create `ALIAS` +records](https://www.namecheap.com/support/knowledgebase/article.aspx/10128/2237/how-to-create-an-alias-record). + +After creating the appropriate DNS record with your DNS provider, wait a few +minutes for DNS to propagate and confirm that you can load your app by visiting +your custom domain. Remember that Django will only serve domains that are listed +in its `ALLOWED_HOSTS` settings variable, so you may have to update your `DJANGO_ALLOWED_HOSTS` +config var on Heroku to accomodate your custom domain. + +### General guidelines for custom domains + +When setting up custom domains, follow these general guidelines: + +- Where possible, let the client register the domain name so we don't have to manage it. +- Shorter domains are always better, but we usually defer to our clients' preferences + when choosing a custom domain name. +- If the client has a pre-existing root domain, always advise deploying on a subdomain + like `app.example.com` instead of a path like `example.com/app`. Clients often + ask for paths off of root domains, but they are typically quite hard to deploy. +- If using a root domain, make sure to set up a `www` subdomain to redirect to the root. +- Don't allow `.herokuapp.com` in `DJANGO_ALLOWED_HOSTS` in production, since we want + the custom domain to be canonical for search engine optimization. + ## Troubleshooting ### I see `Application error` or a `Welcome to Heroku` page on my site From a52849f13e01f77c1174c874abadada2c2e54163 Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Wed, 19 Feb 2020 11:56:46 -0600 Subject: [PATCH 12/12] Add section on enabling SSL for Heroku apps --- heroku/deploy-a-django-app.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/heroku/deploy-a-django-app.md b/heroku/deploy-a-django-app.md index bd97ca2..9551f6a 100644 --- a/heroku/deploy-a-django-app.md +++ b/heroku/deploy-a-django-app.md @@ -24,6 +24,7 @@ preferred platform for hosting dynamic applications. - [Set up a custom domain](#set-up-a-custom-domain) - [Step 1: Configure a custom domain on Heroku](#step-1-configure-a-custom-domain-on-heroku) - [Step 2: Configure a custom domain on a DNS provider](#step-2-configure-a-custom-domain-on-a-dns-provider) + - [Step 3: Enable SSL](#step-3-enable-ssl) - [General guidelines for custom domains](#general-guidelines-for-custom-domains) - [Troubleshooting](#troubleshooting) @@ -407,6 +408,7 @@ always require a dedicated custom domain like `example.com`. When you're ready to deploy to production and publish your app publicly, you'll need to set up a custom domain. In order to do this, you need to register the custom domain in two places: in the Heroku dashboard, and in your (or your client's) DNS provider. +Then, you'll need to instruct Heroku to enable SSL for your domain. For detailed documentation on setting up custom domains, see the [Heroku docs](https://devcenter.heroku.com/articles/custom-domains). @@ -450,6 +452,27 @@ your custom domain. Remember that Django will only serve domains that are listed in its `ALLOWED_HOSTS` settings variable, so you may have to update your `DJANGO_ALLOWED_HOSTS` config var on Heroku to accomodate your custom domain. +### Step 3: Enable SSL + +Once your custom domain is properly resolving to your app, navigate to +`Settings > SSL Certificates` in your app dashboard, select `Configure SSL`, +and Choose `Automatic Certificate Management (ACM)`. Your app should now load +properly when you visit it with the `https://` protocol. + +As a final step, we want to make sure that the app always redirects HTTP traffic +to HTTPS. Heroku [can't do this for us](https://help.heroku.com/J2R1S4T8/can-heroku-force-an-application-to-use-ssl-tls), +so we need to configure the app code to do it. Add the following settings to your +`settings.py` file: + +```python +if DEBUG is False: + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + SECURE_SSL_REDIRECT = True +``` + +When you deploy this change and try to load your app with the `http://` protocol, +it should now automatically redirect you to `https://` and display a valid certificate. + ### General guidelines for custom domains When setting up custom domains, follow these general guidelines: