diff --git a/README.rst b/README.rst index a0aa5077..b129a215 100644 --- a/README.rst +++ b/README.rst @@ -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:: diff --git a/cookiecutter.json b/cookiecutter.json index 9b9c6707..ababd56b 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -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" } diff --git a/docs/deployment-with-elastic-beanstalk.rst b/docs/deployment-with-elastic-beanstalk.rst new file mode 100644 index 00000000..e88decfc --- /dev/null +++ b/docs/deployment-with-elastic-beanstalk.rst @@ -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). diff --git a/docs/index.rst b/docs/index.rst index 07cedc4a..bfa88f18 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ Contents: faq troubleshooting my-favorite-cookie + deployment-with-elastic-beanstalk Indices and tables ================== diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index dd6a3f07..0736c375 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -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() diff --git a/{{cookiecutter.project_slug}}/.ebextensions/10_packages.config b/{{cookiecutter.project_slug}}/.ebextensions/10_packages.config new file mode 100644 index 00000000..c0774efa --- /dev/null +++ b/{{cookiecutter.project_slug}}/.ebextensions/10_packages.config @@ -0,0 +1,5 @@ +packages: + yum: + git: [] + postgresql94-devel: [] + libjpeg-turbo-devel: [] diff --git a/{{cookiecutter.project_slug}}/.ebextensions/20_elasticcache.config b/{{cookiecutter.project_slug}}/.ebextensions/20_elasticcache.config new file mode 100644 index 00000000..539f5509 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.ebextensions/20_elasticcache.config @@ -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" diff --git a/{{cookiecutter.project_slug}}/.ebextensions/30_options.config b/{{cookiecutter.project_slug}}/.ebextensions/30_options.config new file mode 100644 index 00000000..d7135c3f --- /dev/null +++ b/{{cookiecutter.project_slug}}/.ebextensions/30_options.config @@ -0,0 +1,6 @@ +option_settings: + "aws:elasticbeanstalk:customoption": + CacheNodeType : cache.t1.micro + NumCacheNodes : 1 + Engine : redis + CachePort : 6379 diff --git a/{{cookiecutter.project_slug}}/.ebextensions/40_python.config b/{{cookiecutter.project_slug}}/.ebextensions/40_python.config new file mode 100644 index 00000000..02636e1a --- /dev/null +++ b/{{cookiecutter.project_slug}}/.ebextensions/40_python.config @@ -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/" diff --git a/{{cookiecutter.project_slug}}/README.rst b/{{cookiecutter.project_slug}}/README.rst index f760a1e2..b248a2c3 100644 --- a/{{cookiecutter.project_slug}}/README.rst +++ b/{{cookiecutter.project_slug}}/README.rst @@ -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 %} diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index 04a9426f..85edea05 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -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. diff --git a/{{cookiecutter.project_slug}}/ebsetenv.py b/{{cookiecutter.project_slug}}/ebsetenv.py new file mode 100644 index 00000000..fbb8dc2d --- /dev/null +++ b/{{cookiecutter.project_slug}}/ebsetenv.py @@ -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() diff --git a/{{cookiecutter.project_slug}}/env.example b/{{cookiecutter.project_slug}}/env.example index 95a6a1d9..356e9c4e 100644 --- a/{{cookiecutter.project_slug}}/env.example +++ b/{{cookiecutter.project_slug}}/env.example @@ -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 %} \ No newline at end of file +{% endif %}