Add Experimental AWS Elastic Beanstalk support (#817)

Includes:

* First pass at Elastic Beanstalk integration
* Gets code and elasticache working
* Very rudimentary documentation
* Includes post hook cleanup
This commit is contained in:
Daniel Roy Greenfeld 2016-09-22 18:21:00 -07:00 committed by GitHub
parent a950eb82f6
commit 52c0e071da
13 changed files with 223 additions and 6 deletions

View File

@ -42,6 +42,7 @@ Features
* Works with Python 2.7.x or 3.5.x
* Run tests with unittest or py.test
* Customizable PostgreSQL version
* Experimental support for Amazon Elastic Beanstalk
Optional Integrations
@ -146,6 +147,7 @@ Answer the prompts with your own desired options_. For example::
4 - Apache Software License 2.0
5 - Not open source
Choose from 1, 2, 3, 4, 5 [1]: 1
use_elasticbeanstalk_experimental: n
Enter the project and take a look around::

View File

@ -21,5 +21,6 @@
"postgresql_version": ["9.5", "9.4", "9.3", "9.2"],
"js_task_runner": ["Gulp", "Grunt", "None"],
"use_lets_encrypt": "n",
"open_source_license": ["MIT", "BSD", "GPLv3", "Apache Software License 2.0", "Not open source"]
"open_source_license": ["MIT", "BSD", "GPLv3", "Apache Software License 2.0", "Not open source"],
"use_elasticbeanstalk_experimental": "n"
}

View File

@ -0,0 +1,54 @@
Deployment with Elastic Beanstalk
==========================================
.. index:: Elastic Beanstalk
Warning: Experimental
---------------------
This is experimental. For the time being there will be bugs and issues. If you've never used Elastic Beanstalk before, please hold off before trying this option.
On the other hand, we need help cleaning this up. If you do have knowledge of Elastic Beanstalk, we would appreciate the help. :)
Prerequisites
-------------
* awsebcli
Instructions
-------------
```
# creates the directory of environments (servers)
eb init -p python3.4 {{ cookiecutter.project_slug }}
# Creates the environment (server) where the app will run
eb create {{ cookiecutter.project_slug }}
# Note: This will fail on a postgres error, because postgres doesn't exist yet
# Make sure you are in the right environment
eb list
# If you are not in the right environment
eb use {{ cookiecutter.project_slug }}
# Set the environment variables
python ebsetenv.py
# Go to EB AWS config. Create new RDS database (postgres, 9.4.9, db.t2.micro)
# Get some coffee, this is going to take a while
# Deploy again
eb deploy
# Take a look
eb open
```
FAQ
-----
Why Not Use Docker on Elastic Beanstalk?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Because I didn't want to add an abstraction (Docker) on top of an abstraction (Elastic Beanstalk).

View File

@ -25,6 +25,7 @@ Contents:
faq
troubleshooting
my-favorite-cookie
deployment-with-elastic-beanstalk
Indices and tables
==================

View File

@ -115,7 +115,10 @@ def remove_heroku_files():
"""
Removes files needed for heroku if it isn't going to be used
"""
for filename in ["app.json", "Procfile", "requirements.txt", "runtime.txt"]:
filenames = ["app.json", "Procfile", "runtime.txt"]
if '{{ cookiecutter.use_elasticbeanstalk_experimental }}'.lower() != 'y':
filenames.append("requirements.txt")
for filename in ["app.json", "Procfile", "runtime.txt"]:
file_name = os.path.join(PROJECT_DIRECTORY, filename)
remove_file(file_name)
@ -179,6 +182,22 @@ def remove_copying_files():
PROJECT_DIRECTORY, filename
))
def remove_elasticbeanstalk():
"""
Removes elastic beanstalk components
"""
docs_dir_location = os.path.join(PROJECT_DIRECTORY, '.ebextensions')
if os.path.exists(docs_dir_location):
shutil.rmtree(docs_dir_location)
filenames = ["ebsetenv.py", ]
if '{{ cookiecutter.use_heroku }}'.lower() != 'y':
filenames.append("requirements.txt")
for filename in filenames:
os.remove(os.path.join(
PROJECT_DIRECTORY, filename
))
# IN PROGRESS
# def copy_doc_files(project_directory):
# cookiecutters_dir = DEFAULT_CONFIG['cookiecutters_dir']
@ -258,5 +277,6 @@ if '{{ cookiecutter.use_lets_encrypt }}'.lower() == 'y' and '{{ cookiecutter.use
if '{{ cookiecutter.open_source_license}}' != 'GPLv3':
remove_copying_files()
# 4. Copy files from /docs/ to {{ cookiecutter.project_slug }}/docs/
# copy_doc_files(PROJECT_DIRECTORY)
# 12. Remove Elastic Beanstalk files
if '{{ cookiecutter.use_elasticbeanstalk_experimental }}'.lower() != 'y':
remove_elasticbeanstalk()

View File

@ -0,0 +1,5 @@
packages:
yum:
git: []
postgresql94-devel: []
libjpeg-turbo-devel: []

View File

@ -0,0 +1,46 @@
#This sample requires you to create a separate configuration file that defines the custom
# option settings for CacheCluster properties.
Resources:
MyCacheSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "Lock cache down to webserver access only"
SecurityGroupIngress :
- IpProtocol : "tcp"
FromPort :
Fn::GetOptionSetting:
OptionName : "CachePort"
DefaultValue: "6379"
ToPort :
Fn::GetOptionSetting:
OptionName : "CachePort"
DefaultValue: "6379"
SourceSecurityGroupName:
Ref: "AWSEBSecurityGroup"
MyElastiCache:
Type: "AWS::ElastiCache::CacheCluster"
Properties:
CacheNodeType:
Fn::GetOptionSetting:
OptionName : "CacheNodeType"
DefaultValue : "cache.t1.micro"
NumCacheNodes:
Fn::GetOptionSetting:
OptionName : "NumCacheNodes"
DefaultValue : "1"
Engine:
Fn::GetOptionSetting:
OptionName : "Engine"
DefaultValue : "redis"
VpcSecurityGroupIds:
-
Fn::GetAtt:
- MyCacheSecurityGroup
- GroupId
Outputs:
ElastiCache:
Description : "ID of ElastiCache Cache Cluster with Redis Engine"
Value :
Ref : "MyElastiCache"

View File

@ -0,0 +1,6 @@
option_settings:
"aws:elasticbeanstalk:customoption":
CacheNodeType : cache.t1.micro
NumCacheNodes : 1
Engine : redis
CachePort : 6379

View File

@ -0,0 +1,17 @@
container_commands:
01_migrate:
command: "source /opt/python/run/venv/bin/activate && python manage.py migrate"
leader_only: True
02_collectstatic:
command: "source /opt/python/run/venv/bin/activate && python manage.py collectstatic --noinput"
option_settings:
"aws:elasticbeanstalk:application:environment":
DJANGO_SETTINGS_MODULE: "config.settings.production"
REDIS_ENDPOINT_ADDRESS: '`{ "Fn::GetAtt" : [ "MyElastiCache", "RedisEndpoint.Address"]}`'
REDIS_PORT: '`{ "Fn::GetAtt" : [ "MyElastiCache", "RedisEndpoint.Port"]}`'
"aws:elasticbeanstalk:container:python":
WSGIPath: "config/wsgi.py"
NumProcesses: 3
NumThreads: 20
"aws:elasticbeanstalk:container:python:staticfiles":
"/static/": "www/static/"

View File

@ -138,3 +138,13 @@ See detailed `cookiecutter-django Docker documentation`_.
.. _`cookiecutter-django Docker documentation`: http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html
{% endif %}
{% if cookiecutter.use_elasticbeanstalk_experimental %}
Elastic Beanstalk
~~~~~~~~~~~~~~~~~~
See detailed `cookiecutter-django Elastic Beanstalk documentation`_.
.. _`cookiecutter-django Docker documentation`: http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-elastic-beanstalk.html
{% endif %}

View File

@ -177,16 +177,39 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [
# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
{% if cookiecutter.use_elasticbeanstalk_experimental -%}
# Uses Amazon RDS for database hosting, which doesn't follow the Heroku-style spec
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': env('RDS_DB_NAME'),
'USER': env('RDS_USERNAME'),
'PASSWORD': env('RDS_PASSWORD'),
'HOST': env('RDS_HOSTNAME'),
'PORT': env('RDS_PORT'),
}
}
{% else %}
# Use the Heroku-style specification
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
DATABASES['default'] = env.db('DATABASE_URL')
{%- endif %}
# CACHING
# ------------------------------------------------------------------------------
{% if cookiecutter.use_elasticbeanstalk_experimental -%}
REDIS_LOCATION = "redis://{}:{}/0".format(
env('REDIS_ENDPOINT_ADDRESS'),
env('REDIS_PORT')
)
{% else %}
REDIS_LOCATION = '{0}/{1}'.format(env('REDIS_URL', default='redis://127.0.0.1:6379'), 0)
{%- endif %}
# Heroku URL does not pass the DB number, so we parse it in
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': '{0}/{1}'.format(env('REDIS_URL', default='redis://127.0.0.1:6379'), 0),
'LOCATION': REDIS_LOCATION,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'IGNORE_EXCEPTIONS': True, # mimics memcache behavior.

View File

@ -0,0 +1,31 @@
"""Converts a .env file to Elastic Beanstalk environment variables"""
from sys import exit
from subprocess import check_call
try:
import dotenv
except ImportError:
print("Please install the 'dotenv' library: 'pip install dotenv'")
exit()
def main():
command = ['eb', 'setenv']
failures = []
for key, value in dotenv.Dotenv('.env').items():
if key.startswith('POSTGRES'):
continue
if value:
command.append("{}={}".format(key, value))
else:
failures.append(key)
if failures:
for failure in failures:
print("{} requires a value".format(failure))
else:
print(' '.join(command))
check_call(command)
if __name__ == '__main__':
main()

View File

@ -1,3 +1,4 @@
# PostgreSQL
POSTGRES_PASSWORD=mysecretpass
POSTGRES_USER=postgresuser
@ -33,4 +34,4 @@ DJANGO_OPBEAT_SECRET_TOKEN
{% endif %}
{% if cookiecutter.use_compressor == 'y' -%}
COMPRESS_ENABLED=
{% endif %}
{% endif %}