├── VERSION ├── .gitignore ├── {{ cookiecutter.ansible_project_slug }} ├── .gitkeep ├── ansible_vars │ ├── public_keys │ │ ├── app_user_keys │ │ └── root_user_keys │ ├── staging.yml │ ├── production.yml │ └── base.yml ├── ansible.cfg ├── sites.yml ├── .gitignore ├── staging ├── production ├── roles │ ├── nginx │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── requirements.yml │ ├── base │ │ ├── files │ │ │ └── motd.txt │ │ └── tasks │ │ │ └── main.yml │ ├── celery │ │ ├── templates │ │ │ └── celery_upstart.j2 │ │ └── tasks │ │ │ └── main.yml │ └── application │ │ ├── templates │ │ ├── application_upstart.j2 │ │ ├── nginx_http_config.j2 │ │ └── nginx_https_config.j2 │ │ └── tasks │ │ └── main.yml ├── deploy.yml ├── dbservers.yml ├── Vagrantfile ├── vagrant.yml ├── webservers.yml ├── application_vars │ ├── production_env.j2 │ └── staging_env.j2 ├── deploy_tasks │ └── after-cleanup.yml └── README.md ├── requirements.txt ├── .travis.yml ├── cookiecutter.json ├── LICENSE ├── README.md ├── hooks └── post_gen_project.py └── tests └── test_cookiecutter_generation.py /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | __pycache__ 3 | *.swp 4 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/ansible_vars/public_keys/app_user_keys: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/ansible_vars/public_keys/root_user_keys: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cookiecutter==1.4.0 2 | 3 | # Testing 4 | pytest==2.9.2 5 | pytest-cookies==0.2.0 6 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | roles_path=~/.ansible/roles 3 | 4 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/sites.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: dbservers.yml 3 | - include: webservers.yml -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/.gitignore: -------------------------------------------------------------------------------- 1 | # Vagrant 2 | .vagrant 3 | 4 | # Ansible retry files 5 | *.retry 6 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/staging: -------------------------------------------------------------------------------- 1 | [all:vars] 2 | env=staging 3 | 4 | [webservers] 5 | {{ cookiecutter.staging_server_domain }} 6 | 7 | [dbservers] 8 | {{ cookiecutter.staging_server_domain }} 9 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/production: -------------------------------------------------------------------------------- 1 | [all:vars] 2 | env=production 3 | 4 | [webservers] 5 | {{ cookiecutter.production_server_domain }} 6 | 7 | [dbservers] 8 | {{ cookiecutter.production_server_domain }} 9 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | --- 3 | - name: restart nginx 4 | service: name=nginx state=restarted enabled=yes 5 | 6 | - name: reload nginx 7 | service: name=nginx state=reloaded 8 | {% endraw %} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: python 4 | 5 | python: 3.5 6 | 7 | before_install: 8 | - pip install -r requirements.txt 9 | 10 | script: 11 | - py.test 12 | 13 | notifications: 14 | email: 15 | on_success: change 16 | on_failure: always -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/requirements.yml: -------------------------------------------------------------------------------- 1 | - src: ANXS.postgresql 2 | {% if cookiecutter.add_letsencrypt_certificate -%} 3 | - src: thefinn93.letsencrypt 4 | {%- endif %} 5 | - src: carlosbuenosvinos.ansistrano-deploy 6 | - src: carlosbuenosvinos.ansistrano-rollback 7 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/ansible_vars/staging.yml: -------------------------------------------------------------------------------- 1 | server_domain: '{{ cookiecutter.staging_server_domain }}' 2 | {% raw %} 3 | gunicorn_workers: 3 4 | application_vars_file: application_vars/staging_env.j2 5 | letsencrypt_cert_domains: 6 | - '{{ server_domain }}' 7 | {% endraw %} 8 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/ansible_vars/production.yml: -------------------------------------------------------------------------------- 1 | server_domain: '{{ cookiecutter.production_server_domain }}' 2 | {% raw %} 3 | gunicorn_workers: 3 4 | application_vars_file: application_vars/production_env.j2 5 | letsencrypt_cert_domains: 6 | - '{{ server_domain }}' 7 | {% endraw %} 8 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Deploy application 3 | hosts: webservers 4 | remote_user: {{ cookiecutter.application_user }} 5 | 6 | {% raw -%} 7 | vars_files: 8 | - ansible_vars/base.yml 9 | - ansible_vars/{{ env }}.yml 10 | 11 | roles: 12 | - carlosbuenosvinos.ansistrano-deploy 13 | {%- endraw %} 14 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/dbservers.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | --- 3 | - name: Provision db server 4 | hosts: dbservers 5 | become: yes 6 | become_user: root 7 | remote_user: root 8 | 9 | vars_files: 10 | - ansible_vars/base.yml 11 | - ansible_vars/{{ env }}.yml 12 | 13 | roles: 14 | - ANXS.postgresql 15 | - base 16 | {%- endraw %} 17 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/base/files/motd.txt: -------------------------------------------------------------------------------- 1 | █ █ 2 | ██ ██ 3 | ███████████ 4 | ███████████ 5 | █ ███ █ 6 | █ ███ █ 7 | ███████████ 8 | ███████████ 9 | 10 | This machine is maintained ONLY BY ANSIBLE RECIPES. If you want to make any configuration changes you must make them by editing the ansible code! 11 | 12 | Don't make any config changes via ssh! 13 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | --- 3 | - name: Install Nginx 4 | apt: name=nginx state=installed 5 | tags: packages 6 | 7 | - name: Ensure that the default site is disabled 8 | file: path=/etc/nginx/sites-enabled/default state=absent 9 | notify: reload nginx 10 | 11 | - name: Ensure Nginx service is started 12 | service: name=nginx state=started enabled=yes 13 | {% endraw %} -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "ubuntu/trusty64" 6 | 7 | config.vm.provider "virtualbox" do |v| 8 | v.memory = 2048 9 | v.cpus = 2 10 | end 11 | 12 | config.vm.provision "ansible" do |ansible| 13 | ansible.playbook = "vagrant.yml" 14 | ansible.verbose = "vv" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/vagrant.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | - name: Create a {{ application_name }} virtual machine via vagrant 3 | hosts: all 4 | become: yes 5 | become_user: root 6 | remote_user: vagrant 7 | vars_files: 8 | - ansible_vars/base.yml 9 | - ansible_vars/staging.yml 10 | 11 | roles: 12 | - nginx 13 | - ANXS.postgresql 14 | - base 15 | - application 16 | {%- endraw %} 17 | {%- if cookiecutter.add_celery_support == "y" %} 18 | - celery 19 | {%- endif %} 20 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/webservers.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | --- 3 | - name: Provision web server 4 | hosts: webservers 5 | become: yes 6 | become_user: root 7 | remote_user: root 8 | 9 | vars_files: 10 | - ansible_vars/base.yml 11 | - ansible_vars/{{ env }}.yml 12 | 13 | roles: 14 | - nginx 15 | - base 16 | - application 17 | {%- endraw %} 18 | {%- if cookiecutter.add_celery_support == "y" %} 19 | - celery 20 | {%- endif %} 21 | {%- if cookiecutter.add_letsencrypt_certificate == "y" %} 22 | - thefinn93.letsencrypt 23 | {%- endif %} 24 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/application_vars/production_env.j2: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | DATABASE_URL="postgres://{{ application_name }}:{{ postgresql_app_user_password }}@localhost:5432/{{ application_name }}" 3 | DJANGO_ADMIN_URL= 4 | DJANGO_SETTINGS_MODULE="config.settings.production" 5 | DJANGO_SECRET_KEY="jvlza+6=g+&!2fq_wagy873=&nrsc2@d2v3q7q%kh@)zjldyq8" 6 | DJANGO_ALLOWED_HOSTS=".{{ server_domain }}" 7 | DJANGO_SECURE_SSL_REDIRECT="False" 8 | DJANGO_ACCOUNT_ALLOW_REGISTRATION="True" 9 | {%- endraw %} 10 | 11 | {% if cookiecutter.add_celery_support == "y" -%} 12 | BROKER_URL="" 13 | {%- endif %} 14 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/application_vars/staging_env.j2: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | DATABASE_URL="postgres://{{ application_name }}:{{ postgresql_app_user_password }}@localhost:5432/{{ application_name }}" 3 | DJANGO_ADMIN_URL="^admin/$" 4 | DJANGO_SETTINGS_MODULE="config.settings.production" 5 | DJANGO_SECRET_KEY="jvlza+6=g+&!2fq_wagy873=&nrsc2@d2v3q7q%kh@)zjldyq8" 6 | DJANGO_ALLOWED_HOSTS=".{{ server_domain }}" 7 | DJANGO_SECURE_SSL_REDIRECT="False" 8 | DJANGO_ACCOUNT_ALLOW_REGISTRATION="True" 9 | {%- endraw %} 10 | 11 | {% if cookiecutter.add_celery_support == "y" -%} 12 | BROKER_URL="" 13 | {%- endif %} 14 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/celery/templates/celery_upstart.j2: -------------------------------------------------------------------------------- 1 | 2 | description "Celery Application Server for {{ cookiecutter.application_name }}" 3 | {% raw %} 4 | start on runlevel [2345] 5 | stop on runlevel [016] 6 | 7 | setuid {{ application_user }} 8 | setgid {{ application_user }} 9 | 10 | respawn 11 | respawn limit 3 30 12 | 13 | script 14 | eval $(cat /etc/environment | sed 's/^/export /') 15 | 16 | cd {{ application_root }}/current 17 | exec {{ application_root }}/shared/virtualenv/bin/celery -A {{ application_name }} worker -B -E --loglevel=info 18 | end script 19 | {% endraw %} 20 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | - name: Install base packages 3 | apt: name={{ item }} state=installed 4 | with_items: 5 | - build-essential 6 | - htop 7 | - git 8 | - libpq-dev 9 | - python-virtualenv 10 | - python3-dev 11 | - python3-pip 12 | tags: packages 13 | 14 | - pip: name=setuptools 15 | 16 | - name: Set the motd 17 | copy: src=files/motd.txt dest=/etc/motd 18 | 19 | - name: Set ssh keys to root user 20 | authorized_key: user=root key={{ lookup('file', 'ansible_vars/public_keys/root_user_keys') }} state=present exclusive=yes 21 | tags: environment 22 | {% endraw %} 23 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/celery/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | - name: Install os dependencies for celery 3 | apt: name={{ item }} state=installed 4 | with_items: 5 | - rabbitmq-server 6 | 7 | - name: Setup the upstart for celery 8 | template: src=templates/celery_upstart.j2 dest=/etc/init/celery.conf 9 | 10 | - name: Create application user file in sudoers.d 11 | file: path='/etc/sudoers.d/{{ application_user }}' owner=root group=root mode=0440 state=touch 12 | 13 | - name: Give application user permission to restart the celery 14 | lineinfile: "dest='/etc/sudoers.d/{{ application_user }}' line='{{ application_user }} ALL = (root) NOPASSWD: /sbin/start celery, /sbin/stop celery, /sbin/restart celery'" 15 | {% endraw %} -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "ansible_project_name": "Name of the ansible project", 3 | "ansible_project_slug": "{{ cookiecutter.ansible_project_name.lower().replace(' ', '_') }}", 4 | "application_name": "Name of the project", 5 | "application_slug": "{{ cookiecutter.application_name.lower().replace(' ', '_') }}", 6 | "application_user": "hack", 7 | "application_root": "/{{ cookiecutter.application_user }}/{{ cookiecutter.application_slug }}", 8 | "staging_server_domain": "staging.example.com", 9 | "production_server_domain": "example.com", 10 | "add_your_pulic_key": "y", 11 | "add_celery_support": "n", 12 | "add_letsencrypt_certificate": "y", 13 | "letsencrypt_email": "support@production_server_domain", 14 | "project_git_remote": "git@github.com:HackSoftware/cookiecutter-django-ansible.git" 15 | } 16 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/deploy_tasks/after-cleanup.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | - name: Install dependencies in virtualenv 3 | pip: virtualenv="{{ ansistrano_shared_path}}/virtualenv" 4 | requirements={{ ansistrano_release_path.stdout }}/requirements/production.txt 5 | virtualenv_python=python3.4 6 | 7 | - name: Collect statics 8 | shell: "{{ ansistrano_shared_path }}/virtualenv/bin/python {{ ansistrano_release_path.stdout }}/manage.py collectstatic --noinput" 9 | 10 | - name: Run Migrations 11 | shell: "{{ ansistrano_shared_path }}/virtualenv/bin/python {{ ansistrano_release_path.stdout }}/manage.py migrate --noinput" 12 | 13 | - name: Restart Application 14 | raw: sudo restart {{ application_name }} || sudo start {{ application_name }} 15 | 16 | {%- endraw %} 17 | 18 | {% if cookiecutter.add_celery_support == "y" -%} 19 | - name: Restart Celery 20 | raw: sudo restart celery || sudo start celery 21 | {%- endif %} 22 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/application/templates/application_upstart.j2: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | description "Application Server" 3 | 4 | start on runlevel [2345] 5 | stop on runlevel [016] 6 | 7 | setuid {{ application_user }} 8 | setgid {{ application_user }} 9 | 10 | respawn 11 | respawn limit 3 30 12 | 13 | script 14 | export LC_ALL=en_US.UTF8 15 | eval $(cat /etc/environment | sed 's/^/export /') 16 | 17 | PORT=8000 18 | TIMEOUT=120 19 | WORKERS={{ gunicorn_workers }} 20 | USER={{ application_user }} 21 | GROUP={{ application_user }} 22 | LOGFILE={{ application_root }}/shared/log/gunicorn.log 23 | 24 | cd {{ application_root }}/current/ 25 | exec {{ application_root }}/shared/virtualenv/bin/gunicorn \ 26 | -w $WORKERS -t $TIMEOUT \ 27 | --user=$USER --group=$GROUP \ 28 | --name=$NAME \ 29 | config.wsgi 30 | end script 31 | respawn limit 3 30 32 | {% endraw %} -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/application/templates/nginx_http_config.j2: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | 3 | # Configuration containing list of application servers 4 | upstream app_servers { 5 | server 127.0.0.1:8000; 6 | } 7 | 8 | server { 9 | disable_symlinks off; 10 | listen 80 default_server; 11 | server_name {{ server_domain }}; 12 | 13 | client_max_body_size 10M; 14 | 15 | keepalive_timeout 5; 16 | 17 | location / { 18 | proxy_pass http://app_servers; 19 | proxy_redirect off; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header X-Forwarded-Host $server_name; 24 | } 25 | 26 | location /static { 27 | autoindex off; 28 | alias {{ application_root }}/shared/staticfiles/; 29 | } 30 | 31 | location /media { 32 | autoindex off; 33 | alias {{ application_root }}/shared/media/; 34 | } 35 | } 36 | {% endraw %} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2016 Ivaylo Bachvarov . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/application/templates/nginx_https_config.j2: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | 3 | server { 4 | listen 80; 5 | server_name {{ server_domain }}; 6 | return 301 https://$server_name$request_uri; 7 | } 8 | 9 | # Configuration containing list of application servers 10 | upstream app_servers { 11 | server 127.0.0.1:8000; 12 | } 13 | 14 | server { 15 | disable_symlinks off; 16 | listen 443; 17 | server_name {{ server_domain }}; 18 | 19 | client_max_body_size 10M; 20 | 21 | ssl on; 22 | ssl_certificate /etc/letsencrypt/live/{{ server_domain }}/fullchain.pem; 23 | ssl_certificate_key /etc/letsencrypt/live/{{ server_domain }}/privkey.pem; 24 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 25 | 26 | location / { 27 | proxy_pass http://app_servers; 28 | proxy_redirect off; 29 | proxy_set_header Host $host; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 32 | proxy_set_header X-Forwarded-Host $server_name; 33 | } 34 | 35 | location /static { 36 | autoindex off; 37 | alias {{ application_root }}/shared/staticfiles/; 38 | } 39 | 40 | location /media { 41 | autoindex off; 42 | alias {{ application_root }}/shared/media/; 43 | } 44 | 45 | location ~ /.well-known { 46 | allow all; 47 | } 48 | } 49 | {% endraw %} 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cookiecutter Django Ansible 2 | =========================== 3 | [![Build Status](https://travis-ci.org/HackSoftware/cookiecutter-django-ansible.svg?branch=travisCI)](https://travis-ci.org/HackSoftware/cookiecutter-django-ansible) 4 | 5 | Powered by [Cookiecutter](https://github.com/audreyr/cookiecutter), Cookiecutter Django Ansible is a framework for jumpstarting an ansible project for provisioning a server that is ready for your [cookiecutter-django](https://github.com/pydanny/cookiecutter-django) application. 6 | 7 | Features 8 | -------- 9 | - Works with latest cookiecutter-django 10 | - Fully automates the provisioning project 11 | - Sets up a postgres server 12 | - Sets up a NGINX server 13 | - Sets up a upstart job 14 | - Sets up env files 15 | - Sets up ability to separate db server from application server. For easier scaling. 16 | - Sets up staging and production server. For easier development cycle. 17 | - Sets up celery with rabbitmq-server 18 | - Sets up a letsencrypt configuration (Comming soon!) 19 | - Works for 20 | - Ubuntu 14.04 21 | 22 | Requirements 23 | ------------ 24 | Install `cookiecutter` command line: `pip install cookiecutter` 25 | 26 | Usage 27 | ----- 28 | Generate a new Cookiecutter template layout: `cookiecutter gh:HackSoftware/cookiecutter-django-ansible` 29 | 30 | License 31 | ------- 32 | This project is licensed under the terms of the [MIT License](/LICENSE) 33 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/ansible_vars/base.yml: -------------------------------------------------------------------------------- 1 | --- 2 | application_name: '{{ cookiecutter.application_slug }}' 3 | application_root: '{{ cookiecutter.application_root }}' 4 | application_user: '{{ cookiecutter.application_user }}' 5 | 6 | postgresql_version: 9.3 7 | postgresql_encoding: 'UTF-8' 8 | postgresql_locale: 'en_US.UTF-8' 9 | postgresql_cluster_name: 'main' 10 | postgresql_app_user_password: 'POSTGRES_PASSWORD!!!' 11 | 12 | {% raw %} 13 | postgresql_databases: 14 | - name: '{{ application_name }}' 15 | 16 | postgresql_users: 17 | - name: '{{ application_name }}' 18 | pass: '{{ postgresql_app_user_password }}' 19 | 20 | postgresql_user_privileges: 21 | - name: '{{ application_name }}' 22 | db: '{{ application_name }}' 23 | {% endraw %} 24 | 25 | letsencrypt_webroot_path: '/var/www/html' 26 | letsencrypt_email: '{{ cookiecutter.letsencrypt_email }}' 27 | letsencrypt_renewal_command_args: '--renew-hook "systemctl restart nginx"' 28 | 29 | # Ansistrano deploy settings 30 | ansistrano_git_repo: '{{ cookiecutter.project_git_remote }}' # Location of the git repository 31 | {% raw %} 32 | ansistrano_deploy_via: "git" 33 | ansistrano_keep_releases: 5 34 | ansistrano_deploy_to: '{{ application_root }}' 35 | ansistrano_git_identity_key_path: '~/.ssh/id_rsa' 36 | ansistrano_shared_paths: ['media', 'staticfiles'] 37 | ansistrano_shared_files: [] 38 | ansistrano_after_cleanup_tasks_file: "{{ playbook_dir }}/deploy_tasks/after-cleanup.yml" 39 | {% endraw %} 40 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | import random 2 | import shutil 3 | import os 4 | 5 | 6 | PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) 7 | 8 | 9 | def get_random_string( 10 | length=50, 11 | allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789!@%^&*(-_=+)'): 12 | return ''.join(random.choice(allowed_chars) for i in range(length)) 13 | 14 | 15 | def remove_not_needed_nginx_config_file(project_location): 16 | if '{{ cookiecutter.add_letsencrypt_certificate }}'.lower() == 'y': 17 | # Remove celery upstart job if celery not needed 18 | http_file_location = os.path.join( 19 | project_location, 20 | 'roles/application/templates/nginx_http_config.j2' 21 | ) 22 | 23 | os.remove(http_file_location) 24 | 25 | else: 26 | https_file_location = os.path.join( 27 | project_location, 28 | 'roles/application/templates/nginx_https_config.j2' 29 | ) 30 | 31 | os.remove(https_file_location) 32 | 33 | 34 | def postgres_set_password(): 35 | settings_content = '' 36 | settings_file_name = 'ansible_vars/base.yml' 37 | 38 | with open(settings_file_name, 'r') as f: 39 | # Read the file content 40 | settings_content = f.read() 41 | 42 | with open(settings_file_name, 'w') as f: 43 | # Empty the file, replace the password, end write the content again. 44 | postgres_password = get_random_string(length=20) 45 | settings_content = settings_content.replace('POSTGRES_PASSWORD!!!', postgres_password, 1) 46 | 47 | f.write(settings_content) 48 | 49 | 50 | def set_personal_public_key(): 51 | app_user_keys = 'ansible_vars/public_keys/app_user_keys' 52 | root_user_keys = 'ansible_vars/public_keys/root_user_keys' 53 | 54 | with open(os.path.expanduser('~/.ssh/id_rsa.pub'), 'r') as f: 55 | publick_key = f.read() 56 | 57 | with open(app_user_keys, 'w') as f: 58 | f.write(publick_key) 59 | 60 | with open(root_user_keys, 'w') as f: 61 | f.write(publick_key) 62 | 63 | 64 | def remove_celery(project_location): 65 | # Remove celery upstart job if celery not needed 66 | celery_role_location = os.path.join( 67 | project_location, 68 | 'roles/celery' 69 | ) 70 | 71 | shutil.rmtree(celery_role_location) 72 | 73 | 74 | if '{{ cookiecutter.add_celery_support }}'.lower() != 'y': 75 | remove_celery(PROJECT_DIRECTORY) 76 | 77 | if '{{ cookiecutter.add_your_pulic_key }}'.lower() == 'y': 78 | set_personal_public_key() 79 | 80 | postgres_set_password() 81 | remove_not_needed_nginx_config_file(PROJECT_DIRECTORY) 82 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/roles/application/tasks/main.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | --- 3 | - name: Add application user 4 | user: name={{ application_user }} 5 | 6 | - name: Make application dir structure 7 | file: path={{ item }} state=directory owner={{ application_user }} group={{ application_user }} 8 | with_items: 9 | - "{{ application_root }}" 10 | - "{{ application_root }}/releases" 11 | - "{{ application_root }}/shared" 12 | - "{{ application_root }}/shared/log" 13 | - "{{ application_root }}/shared/media" 14 | - "{{ application_root }}/shared/staticfiles" 15 | 16 | - name: Set ssh keys to application user 17 | authorized_key: user={{ application_user }} key="{{ lookup('file', 'ansible_vars/public_keys/app_user_keys') }}" state=present exclusive=yes 18 | tags: sshkeys 19 | 20 | - name: Create application virtualenv 21 | pip: virtualenv="{{ application_root }}/shared/virtualenv" 22 | virtualenv_python=python3.4 23 | name=virtualenv 24 | 25 | - name: Set virtualenv owner to {{ application_user }} 26 | file: path={{ application_root }}/shared/virtualenv state=directory owner={{ application_user }} group={{ application_user }} recurse=yes 27 | 28 | - name: Create django env file 29 | template: src={{ application_vars_file }} 30 | dest=/etc/environment 31 | tags: environment 32 | 33 | - name: Install os dependencies for application 34 | apt: name={{ item }} state=installed 35 | with_items: 36 | - libpq-dev 37 | - libtiff4-dev 38 | - libjpeg8-dev 39 | - libfreetype6-dev 40 | - liblcms1-dev 41 | - libwebp-dev 42 | tags: packages 43 | 44 | - name: Setup the upstart for application 45 | template: src=templates/application_upstart.j2 dest=/etc/init/{{ application_name }}.conf 46 | {%- endraw %} 47 | 48 | {% if cookiecutter.add_letsencrypt_certificate == "y" -%} 49 | {% raw -%} 50 | - name: Create the Nginx secure configuration file 51 | template: src=templates/nginx_https_config.j2 52 | dest=/etc/nginx/sites-available/{{ application_name }} 53 | backup=yes 54 | notify: reload nginx 55 | {%- endraw %} 56 | 57 | {%- else -%} 58 | 59 | {% raw -%} 60 | - name: Create the Nginx configuration file 61 | template: src=templates/nginx_http_config.j2 62 | dest=/etc/nginx/sites-available/{{ application_name }} 63 | backup=yes 64 | notify: reload nginx 65 | {%- endraw %} 66 | 67 | {%- endif %} 68 | 69 | {% raw -%} 70 | - name: Ensure that the application site is enabled 71 | file: src=/etc/nginx/sites-available/{{ application_name }} 72 | dest=/etc/nginx/sites-enabled/{{ application_name }} 73 | state=link 74 | notify: reload nginx 75 | 76 | - name: Create application user file in sudoers.d 77 | file: path='/etc/sudoers.d/{{ application_user }}' owner=root group=root mode=0440 state=touch 78 | 79 | - name: Give application user permission to restart the application 80 | lineinfile: "dest='/etc/sudoers.d/{{ application_user }}' line='{{ application_user }} ALL = (root) NOPASSWD: /sbin/start {{ application_name }}, /sbin/stop {{ application_name }}, /sbin/restart {{ application_name }}'" 81 | {%- endraw %} 82 | -------------------------------------------------------------------------------- /{{ cookiecutter.ansible_project_slug }}/README.md: -------------------------------------------------------------------------------- 1 | # Ansible documentation. 2 | [![Cookiecutter Django Ansible Badge](https://img.shields.io/badge/built%20with-Cookiecutter%20Django%20Ansible-green.svg)](https://github.com/HackSoftware/cookiecutter-django-ansible) 3 | 4 | ## What is this repository? 5 | 6 | Our machines are provisioned and maintained **ONLY with ansible roles**. So if you want to make any changes to the machines **DONT DO IT VIA SSH**. Make changes in this repo and run the ansible code to change the server state. 7 | 8 | Ansible is really handy for deployment too. Thanks to the guys from [ansistrano](https://github.com/ansistrano/deploy) we can deploy our project in a nice and smooth way. 9 | 10 | **What this ansible code do?** 11 | 12 | - It installs python. 13 | - It sends the team keys to the server. 14 | - It installs the postgresql server and configures it. 15 | - It installs the nginx server and configures the vhost. 16 | - It configures everyting that django needs: directory structure, upstart jobs, env vars, etc. 17 | - It can deploy you project. 18 | 19 | ## Why do we use ansible? 20 | Using ansible, we have dramatically reduced the time it takes us to deliver applications into production, from weeks to days and even hours. 21 | 22 | Eliminate Configuration Drift - With ansible, our servers remain in the state we set for them. 23 | 24 | Visibility - Ansible gives us rich data sets not only of infrastructure configuration but also of any changes to that infrastructure. We have much more visibility into the changes occurring in our infrastructure over time and their impact to service levels. 25 | 26 | Ansible can provision a fully working server in 20 minutes. That would have taken close to a full day of work without ansible! 27 | 28 | ## How to run the ansible code? 29 | First of all you need to have latest ansible installed. 30 | 31 | Keep in mind that ansible works **only with python2 for now**. So create a python2 virtualenv for it. 32 | 33 | ``` 34 | $ pip install ansible 35 | ``` 36 | 37 | Then you have to install all ansible roles. ``ansible-galaxy`` is the package manager here. 38 | 39 | ``` 40 | $ ansible-galaxy install -r roles/requirements.yml 41 | ``` 42 | 43 | ### And now you are ready to run the ansible code 44 | 45 | You can run the ansible code in a vagrant virtual box just to test it. **Always test your code in a virtual box before running it in production!** 46 | 47 | ``` 48 | vagrant up 49 | ``` 50 | 51 | **Lets run the vagrant code in production** 52 | 53 | Provision the staging server 54 | 55 | ``` 56 | ansible-playbook -i staging sites.yml 57 | ``` 58 | 59 | Provision the production server 60 | 61 | ``` 62 | ansible-playbook -i production sites.yml 63 | ``` 64 | 65 | ## How to deploy your project? 66 | 67 | ``` 68 | ansible-playbook -i staging deploy.yml 69 | ``` 70 | 71 | ## Common things that you can change here. 72 | 73 | ### Change the machine IP address or add a new machine 74 | 75 | We have two types of machines: webservers and dbservers. That makes scaling easyer but you can use the same machine for both types. 76 | 77 | Here are the machine addresses: 78 | - [Staging machines](/staging) 79 | - [Production machines](/production) 80 | 81 | ### Nginx configuration 82 | 83 | Maybe you want to change the nginx config? [It is here.](/roles/application/templates/nginx_config.j2) 84 | 85 | ### SSH Keys 86 | 87 | Maybe you want to give someone access to the server? [Look at this dir.](/ansible_vars/public_keys/) 88 | 89 | ### Django related changes 90 | 91 | #### Change some env vars 92 | 93 | There are two env configurations. One for your production server and one for your staging server. [The env files are located here.](/application_vars/) 94 | 95 | #### See sensible information 96 | All sensible information like passwords for postgres and rabbitmq can be changed from [here.](/ansible_vars/) 97 | 98 | ## How to run only a certain piece of code 99 | 100 | You can filter tasks based on tags from the command line with `--tags` or `--skip-tags`. You can list available tags with `--list-tags` 101 | 102 | ```bash 103 | ansible-playbook -i staging sites.yml --tags "sshkeys,packages" 104 | ``` 105 | The existing tags are: 106 | 107 | **Upload new ssh public keys from git:** 108 | 109 | ```bash 110 | ansible-playbook -i staging sites.yml --tags "sshkeys" 111 | ``` 112 | 113 | **Update env vars:** 114 | 115 | ```bash 116 | ansible-playbook -i staging sites.yml --tags "environment" 117 | ``` 118 | 119 | **Update or install new apt packages:** 120 | 121 | ```bash 122 | ansible-playbook -i staging sites.yml --tags "packages" 123 | ``` 124 | -------------------------------------------------------------------------------- /tests/test_cookiecutter_generation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import pytest 5 | from binaryornot.check import is_binary 6 | 7 | 8 | @pytest.fixture 9 | def context(): 10 | return { 11 | "ansible_project_name": "store ansible", 12 | "ansible_project_slug": "store_ansible", 13 | "application_name": "store", 14 | "application_slug": "store", 15 | "application_user": "hack", 16 | "application_root": "/hack/store", 17 | "add_your_pulic_key": "n", 18 | "add_letsencrypt_certificate": "y", 19 | } 20 | 21 | 22 | def build_files_list(root_dir): 23 | """Build a list containing absolute paths to the generated files.""" 24 | return [ 25 | os.path.join(dirpath, file_path) 26 | for dirpath, subdirs, files in os.walk(root_dir) 27 | for file_path in files 28 | ] 29 | 30 | 31 | def check_substitutions(paths): 32 | """Method to check all paths have correct substitutions, 33 | used by other tests cases 34 | """ 35 | # Assert that no match is found in any of the files 36 | 37 | PATTERN = '{{(\s?cookiecutter)[.](.*?)}}' 38 | RE_OBJ = re.compile(PATTERN) 39 | 40 | for path in paths: 41 | if is_binary(path): 42 | continue 43 | for line in open(path, 'r'): 44 | match = RE_OBJ.search(line) 45 | msg = 'cookiecutter variable not replaced in {}' 46 | assert match is None, msg.format(path) 47 | 48 | 49 | def test_default_configuration(cookies, context): 50 | result = cookies.bake(extra_context=context) 51 | assert result.exit_code == 0 52 | assert result.exception is None 53 | assert result.project.basename == context['ansible_project_slug'] 54 | assert result.project.isdir() 55 | 56 | paths = build_files_list(str(result.project)) 57 | assert paths 58 | check_substitutions(paths) 59 | 60 | 61 | def check_password_replaced(paths): 62 | PATTERN = 'POSTGRES_PASSWORD!!!' 63 | RE_OBJ = re.compile(PATTERN) 64 | 65 | for path in paths: 66 | if not is_binary(path): 67 | for line in open(path, 'r'): 68 | match = RE_OBJ.search(line) 69 | msg = 'password variable not replaced in {}' 70 | assert match is None, msg.format(path) 71 | 72 | 73 | def test_postgres_password_hook(cookies, context): 74 | result = cookies.bake(extra_context=context) 75 | 76 | assert result.exit_code == 0 77 | 78 | paths = build_files_list(str(result.project)) 79 | assert paths 80 | check_password_replaced(paths) 81 | 82 | 83 | @pytest.mark.skipif('TRAVIS' in os.environ, 84 | reason="Travis does not have public key") 85 | def test_public_key_placement(cookies, context): 86 | context['add_your_pulic_key'] = 'y' 87 | result = cookies.bake(extra_context=context) 88 | 89 | assert result.exit_code == 0 90 | # Check if files are not empty 91 | assert os.path.getsize(str(result.project) + '/ansible_vars/public_keys/app_user_keys') > 0 92 | assert os.path.getsize(str(result.project) + '/ansible_vars/public_keys/root_user_keys') > 0 93 | 94 | 95 | @pytest.mark.skipif('TRAVIS' in os.environ, 96 | reason="Travis does not have public key") 97 | def test_public_key_placement_disabled(cookies, context): 98 | context['add_your_pulic_key'] = 'n' 99 | result = cookies.bake(extra_context=context) 100 | 101 | assert result.exit_code == 0 102 | # Check if files are empty 103 | assert os.path.getsize(str(result.project) + '/ansible_vars/public_keys/app_user_keys') == 0 104 | assert os.path.getsize(str(result.project) + '/ansible_vars/public_keys/root_user_keys') == 0 105 | 106 | 107 | def test_added_celery_role(cookies, context): 108 | context['add_celery_support'] = 'y' 109 | result = cookies.bake(extra_context=context) 110 | 111 | assert result.exit_code == 0 112 | assert os.path.isdir(str(result.project) + '/roles/celery') 113 | 114 | 115 | def test_removed_celery_role(cookies, context): 116 | context['add_celery_support'] = 'n' 117 | result = cookies.bake(extra_context=context) 118 | 119 | assert result.exit_code == 0 120 | assert not os.path.isdir(str(result.project) + '/roles/celery') 121 | 122 | 123 | def test_remove_http_config(cookies, context): 124 | context['add_letsencrypt_certificate'] = 'y' 125 | result = cookies.bake(extra_context=context) 126 | 127 | assert result.exit_code == 0 128 | assert not os.path.isfile(str(result.project) + '/roles/application/templates/nginx_http_config.j2') 129 | 130 | 131 | def test_remove_https_config(cookies, context): 132 | context['add_letsencrypt_certificate'] = 'n' 133 | result = cookies.bake(extra_context=context) 134 | 135 | assert result.exit_code == 0 136 | assert not os.path.isfile(str(result.project) + '/roles/application/templates/nginx_https_config.j2') 137 | --------------------------------------------------------------------------------