Support for 12-factor
We found some quirks getting Meltano spun up in production in a 12-factor environment. Heroku is the most notable environment like this but I believe many Kubernetes environments follow similar patterns, including ours.
- Often a load balancer is used in front of server processes and used as SSL termination. For the UI to accept HTTP traffic from the load balancers,
FORWARDED_ALLOW_IPS=*
is required, as noted in #2015 (closed) . Not really Meltano's fault but worth documenting - Configuration options
SERVER_NAME
,SECRET_KEY
, andSECURITY_PASSWORD_SALT
must be read from aui.cfg
file in the project directory, and cannot be set via environment variables. I'd be happy to send a PR for this, it seems like an easy fix - Specifying
SERVER_NAME
causes Flask to refuse to respond to traffic whose hostname doesn't match this property. However, often load balancers are doing health checks to see if the app is running so that it can replace processes when they die. AWS ALB (and others too I bet) don't actually include the hostname in the health check requests, so they always think the app is unhealthy. Setting it toNone
allows all traffic to be accepted but generates a security warning at boot - Some discrepancies in environment variable names mean there's got to be some remapping. Heroku injects a
PORT
environment variable to tell the app which port to expose. Meltano allows this but usesMELTANO_API_PORT
. Databases get injected asDATABASE_URL
and need to get set toMELTANO_DATABASE_URI
and broken out intoPG_DATABASE
,PG_ADDRESS
, etc. Not a problem but I wanted to note it here just in case
As a workaround, we use a little script like this to boot the ui in production:
import sys
import os
from urllib.parse import urlparse
from meltano.cli import main
os.environ['FORWARDED_ALLOW_IPS'] = '*'
if os.getenv('DATABASE_URL'):
os.environ['MELTANO_DATABASE_URI'] = os.getenv('DATABASE_URL')
os.environ['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
parts = urlparse(os.environ['DATABASE_URL'])
os.environ['PG_DATABASE'] = parts.path[1:]
os.environ['PG_PASSWORD'] = parts.password
os.environ['PG_USERNAME'] = parts.username
os.environ['PG_ADDRESS'] = parts.hostname
os.environ['PG_PORT'] = str(parts.port)
if os.getenv('PORT'):
os.environ['MELTANO_API_PORT'] = os.getenv('PORT')
if not os.path.isfile('ui.cfg'):
f = open('ui.cfg', 'w')
try:
f.write('SERVER_NAME = None\n')
# f.write(f"SERVER_NAME = \"{os.getenv('SERVER_NAME')}\"\n")
f.write(f"SECRET_KEY = \"{os.getenv('SECRET_KEY')}\"\n")
f.write(f"SECURITY_PASSWORD_SALT = \"{os.getenv('SECURITY_PASSWORD_SALT')}\"\n")
finally:
f.close()
sys.argv = ['meltano', 'ui', 'start']
main()