├── deploy ├── ansible │ ├── roles │ │ ├── deploy │ │ │ ├── templates │ │ │ │ └── build_info.txt │ │ │ └── tasks │ │ │ │ ├── build.yml │ │ │ │ ├── checkout.yml │ │ │ │ └── main.yml │ │ ├── users │ │ │ └── tasks │ │ │ │ ├── main.yml │ │ │ │ ├── localuser.yml │ │ │ │ └── users.yml │ │ ├── nginx │ │ │ ├── templates │ │ │ │ ├── gzip_params │ │ │ │ ├── site.conf │ │ │ │ └── ssl_params │ │ │ └── tasks │ │ │ │ ├── ssl.yml │ │ │ │ ├── certbot.yml │ │ │ │ └── main.yml │ │ ├── configure │ │ │ └── tasks │ │ │ │ ├── swap.yml │ │ │ │ └── main.yml │ │ └── base │ │ │ └── tasks │ │ │ └── main.yml │ ├── inventory │ │ ├── production │ │ ├── vagrant │ │ └── staging │ ├── deploy.yml │ ├── init.yml │ ├── provision.yml │ ├── configure.yml │ ├── vagrant-link.yml │ ├── host_vars │ │ └── vagrant │ └── group_vars │ │ └── all.yml ├── run-playbook.sh └── README.md ├── .gitignore ├── AUTHORS ├── package.json ├── LICENSE-MIT ├── README.md ├── Vagrantfile └── Gruntfile.js /deploy/ansible/roles/deploy/templates/build_info.txt: -------------------------------------------------------------------------------- 1 | date: {{ansible_date_time.iso8601}} 2 | sha: {{sha.stdout}} 3 | env: {{env}} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specific to the example project build process 2 | /node_modules 3 | /public 4 | 5 | # Used by the deployment workflow 6 | .vagrant 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Brian J Brennan (http://bjb.io/) 2 | "Cowboy" Ben Alman (http://benalman.com/) 3 | Matt Surabian (http://mattsurabian.com/) 4 | Tyler Kellen (http://goingslowly.com/) 5 | -------------------------------------------------------------------------------- /deploy/ansible/inventory/production: -------------------------------------------------------------------------------- 1 | # Specify your production app server here. This should match the ansible 2 | # group_vars/all site_fqdn setting. 3 | 4 | deployment-workflow.bocoup.com 5 | -------------------------------------------------------------------------------- /deploy/ansible/roles/users/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Add a user account for the currently logged-in user (development), otherwise 2 | # add all users defined in group_vars/all (production). 3 | 4 | - include: localuser.yml 5 | when: "{{ env == 'development' }}" 6 | 7 | - include: users.yml 8 | when: "{{ env != 'development' }}" 9 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/templates/gzip_params: -------------------------------------------------------------------------------- 1 | gzip on; 2 | gzip_disable "msie6"; 3 | 4 | gzip_vary on; 5 | gzip_proxied any; 6 | gzip_comp_level 6; 7 | gzip_buffers 16 8k; 8 | gzip_http_version 1.1; 9 | gzip_types text/plain text/css image/png image/gif image/jpeg application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 10 | -------------------------------------------------------------------------------- /deploy/ansible/inventory/vagrant: -------------------------------------------------------------------------------- 1 | # This inventory allows you to run playbooks granularly via ansible-playbook 2 | # instead of having to rely on the (rather limited) vagrant provision command. 3 | # The ansible_ssh_host value should match the host_vars/vagrant site_fqdn and 4 | # Vagrantfile config.hostsupdater.aliases settings. 5 | 6 | vagrant ansible_ssh_host=deployment-workflow.loc 7 | -------------------------------------------------------------------------------- /deploy/ansible/inventory/staging: -------------------------------------------------------------------------------- 1 | # Specify your staging app server here. Note that setting the site_fqdn variable 2 | # here will allow nginx to respond to requests made to that FQDN instead of the 3 | # production server's FQDN. This is likely the only change you will need to 4 | # make between production and staging. 5 | 6 | deployment-workflow-staging.bocoup.com site_fqdn=deployment-workflow-staging.bocoup.com 7 | -------------------------------------------------------------------------------- /deploy/ansible/deploy.yml: -------------------------------------------------------------------------------- 1 | # Clone, build, and deploy, restarting nginx if necessary. This playbook must 2 | # be run after provision and configure, and is used to deploy and build the 3 | # specified commit (overridable via extra vars) on the server. Running this 4 | # playbook in Vagrant will override the vagrant-link playbook, and vice-versa. 5 | 6 | - hosts: all 7 | become: yes 8 | become_method: sudo 9 | roles: 10 | - deploy 11 | -------------------------------------------------------------------------------- /deploy/ansible/init.yml: -------------------------------------------------------------------------------- 1 | # This playbook saves the trouble of running each of the following playbooks 2 | # individually, and is provided for convenience. After "vagrant up", this 3 | # playbook will be run on the new Vagrant box. 4 | 5 | - include: provision.yml 6 | - include: configure.yml 7 | 8 | # Because this playbook targets the "vagrant" inventory host, it will only be 9 | # run for the Vagrant box, and skipped otherwise. 10 | - include: vagrant-link.yml 11 | -------------------------------------------------------------------------------- /deploy/ansible/provision.yml: -------------------------------------------------------------------------------- 1 | # Provision server. This playbook must be run when a server is first created 2 | # and is typically only run once. It may be run again if you make server-level 3 | # changes or need to update any installed apt modules to their latest versions. 4 | # If you were creating a new AMI or base box, you'd do so after running only 5 | # this playbook. 6 | 7 | - hosts: all 8 | become: yes 9 | become_method: sudo 10 | roles: 11 | - {role: base, tags: base} 12 | -------------------------------------------------------------------------------- /deploy/ansible/configure.yml: -------------------------------------------------------------------------------- 1 | # Configure server. This playbook is run after a server is provisioned but 2 | # before a project is deployed, to configure the system, add user accounts, 3 | # and setup long-running processes like nginx, postgres, etc. 4 | 5 | - hosts: all 6 | become: yes 7 | become_method: sudo 8 | roles: 9 | - {role: configure, tags: configure} 10 | - {role: users, tags: users} 11 | - {role: nginx, tags: nginx} 12 | handlers: 13 | - name: reload nginx 14 | service: name=nginx state=reloaded 15 | - name: restart sshd 16 | service: name=ssh state=restarted 17 | -------------------------------------------------------------------------------- /deploy/ansible/roles/users/tasks/localuser.yml: -------------------------------------------------------------------------------- 1 | # In development, create an account for the currently logged-in user, and 2 | # copy their public key to the server. This makes it possible to run other 3 | # playbooks without specifying a user or private key on the command line. 4 | 5 | - name: ensure local user is synced 6 | user: 7 | state: present 8 | name: "{{ lookup('env', 'USER') }}" 9 | shell: /bin/bash 10 | groups: sudo 11 | register: user 12 | 13 | - name: ensure the local user's public key is synced 14 | authorized_key: 15 | user: "{{ user.name }}" 16 | key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deployment-workflow", 3 | "version": "1.0.0", 4 | "description": "Modern Web Deployment Workflow", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "grunt dev", 8 | "build": "grunt build" 9 | }, 10 | "author": "Bocoup", 11 | "license": "MIT", 12 | "private": true, 13 | "devDependencies": { 14 | "git-rev": "^0.2.1", 15 | "grunt": "^0.4.5", 16 | "grunt-cli": "^0.1.13", 17 | "grunt-contrib-clean": "^0.6.0", 18 | "grunt-contrib-copy": "^0.8.0", 19 | "grunt-contrib-watch": "^0.6.1", 20 | "grunt-markdown": "^0.7.0", 21 | "js-yaml": "^3.3.1", 22 | "toc": "^0.4.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /deploy/ansible/vagrant-link.yml: -------------------------------------------------------------------------------- 1 | # Instead of cloning the Git repo and building like the "deploy" playbook, this 2 | # playbook links your local working project directory into the Vagrant box so 3 | # that you can instantly preview your local changes on the server, for 4 | # convenience while developing. While in this mode, all building will have to 5 | # be done manually, at the command line of your development machine. Running 6 | # this playbook will override the deploy playbook, and vice-versa. 7 | 8 | - hosts: vagrant 9 | become: yes 10 | become_method: sudo 11 | tasks: 12 | - name: link vagrant synced directory to make it live 13 | file: path={{site_path}} state=link src={{synced_folder}} force=yes 14 | -------------------------------------------------------------------------------- /deploy/ansible/roles/users/tasks/users.yml: -------------------------------------------------------------------------------- 1 | # In production, ensure all users have been added, along with any public keys. 2 | # If any user's state is "absent", they will be removed. If any keys are 3 | # removed, they will be deleted. 4 | 5 | - name: ensure users are synced 6 | user: 7 | name: "{{item.name}}" 8 | force: yes 9 | remove: yes 10 | password: "{{ item.shadow_pass | default(omit) }}" 11 | state: "{{ item.state | default(omit) }}" 12 | shell: "{{ item.shell | default('/bin/bash') }}" 13 | groups: "{{ item.groups | default('sudo') }}" 14 | with_items: "{{ users }}" 15 | 16 | - name: ensure user public keys are synced 17 | authorized_key: 18 | user: "{{item.name}}" 19 | key: "{{ item.public_keys | join('\n') }}" 20 | state: present 21 | exclusive: yes 22 | with_items: "{{ users }}" 23 | when: item.public_keys is defined and item.public_keys | length > 0 24 | -------------------------------------------------------------------------------- /deploy/ansible/host_vars/vagrant: -------------------------------------------------------------------------------- 1 | # All variables defined here override those in group_vars/all, for the 2 | # purposes of developing and testing deployment in Vagrant. 3 | 4 | # Certain tasks may operate in a less secure (but more convenient) manner, eg. 5 | # enabling passwordless sudo or generating self-signed ssl certs, when testing 6 | # locally, in Vagrant. But not in production! 7 | env: development 8 | 9 | # Vagrant box synced folder. This should match the config.vm.synced_folder 10 | # setting in the Vagrantfile, and should be different than the site_path, 11 | # clone_path or public_path vars. 12 | synced_folder: "{{base_path}}/vagrant" 13 | 14 | # Vagrant box hostname and FQDN. The site_fqdn setting should match the vagrant 15 | # inventory ansible_ssh_host and Vagrantfile config.hostsupdater.aliases 16 | # settings. 17 | hostname: ansible-vagrant 18 | site_fqdn: "{{project_name}}.loc" 19 | 20 | # Should the nginx server use HTTPS instead of HTTP? 21 | ssl: false 22 | -------------------------------------------------------------------------------- /deploy/ansible/roles/deploy/tasks/build.yml: -------------------------------------------------------------------------------- 1 | # All project build tasks go here. These tasks will only be run for the 2 | # specified commit if the commit hasn't been deployed before or if "force" 3 | # is true. 4 | 5 | # Modify as-needed! 6 | 7 | - name: compare package.json of current deploy with previous deploy 8 | command: diff {{site_path}}/package.json {{clone_path}}/package.json 9 | register: package_diff 10 | ignore_errors: true 11 | no_log: true 12 | 13 | - name: copy existing npm modules 14 | command: cp -R {{site_path}}/node_modules {{clone_path}} 15 | when: package_diff.rc == 0 16 | 17 | - name: install npm modules 18 | npm: path="{{clone_path}}" 19 | when: package_diff.rc != 0 20 | 21 | - name: build production version 22 | shell: npm run build 23 | args: 24 | chdir: "{{clone_path}}" 25 | 26 | - name: generate build info file 27 | template: src=build_info.txt dest={{clone_path}}/{{build_info_path}} 28 | when: build_info_path is defined 29 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/tasks/ssl.yml: -------------------------------------------------------------------------------- 1 | # Generate strong dhe param and self-signed ssl cert/key (in development only!). 2 | # In production, you'll need your own valid ssl cert/key. 3 | 4 | - name: generate strong dhe parameter 5 | shell: openssl dhparam -dsaparam -out {{dhe_param_path}} 4096 6 | args: 7 | creates: "{{dhe_param_path}}" 8 | notify: reload nginx 9 | 10 | - name: create self-signed ssl cert/key 11 | command: > 12 | openssl req -new -nodes -x509 13 | -subj "/C=US/ST=Oregon/L=Portland/O=IT/CN={{site_fqdn}}" -days 3650 14 | -keyout {{ssl_key_path}} -out {{ssl_cert_path}} -extensions v3_ca 15 | args: 16 | creates: "{{ssl_cert_path}}" 17 | notify: reload nginx 18 | 19 | - name: ensure ssl cert/key exist 20 | stat: path={{item}} 21 | register: ssl_files 22 | with_items: 23 | - "{{ssl_cert_path}}" 24 | - "{{ssl_key_path}}" 25 | 26 | - fail: msg="ssl cert file {{ssl_cert_path}} missing" 27 | when: not ssl_files.results[0].stat.exists 28 | 29 | - fail: msg="ssl key file {{ssl_key_path}} missing" 30 | when: not ssl_files.results[1].stat.exists 31 | -------------------------------------------------------------------------------- /deploy/ansible/roles/configure/tasks/swap.yml: -------------------------------------------------------------------------------- 1 | - name: check if swap file exists 2 | stat: path={{swap_file_path}} 3 | register: swap_file_check 4 | 5 | - name: ensure swapfile exists 6 | command: fallocate -l {{swap_file_size}} /swap 7 | when: not swap_file_check.stat.exists 8 | args: 9 | creates: "{{swap_file_path}}" 10 | 11 | - name: ensure swap file has correct permissions 12 | file: path={{swap_file_path}} owner=root group=root mode=0600 13 | 14 | - name: ensure swapfile is formatted 15 | command: mkswap {{swap_file_path}} 16 | when: not swap_file_check.stat.exists 17 | 18 | # the quotes around integers here can be removed when this is resolved 19 | # https://github.com/ansible/ansible-modules-core/issues/1861 20 | - name: ensure swap file can be mounted 21 | mount: 22 | name: none 23 | src: "{{swap_file_path}}" 24 | fstype: swap 25 | opts: sw 26 | passno: "0" 27 | dump: "0" 28 | state: present 29 | 30 | - name: ensure swap is activited 31 | command: swapon -a 32 | 33 | - name: ensure swap is used as a last resort 34 | sysctl: 35 | name: vm.swappiness 36 | value: 0 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Bocoup, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/templates/site.conf: -------------------------------------------------------------------------------- 1 | server { 2 | {% if ssl %} 3 | listen 443 ssl; 4 | include ssl_params; 5 | {% else %} 6 | listen 80; 7 | {% endif %} 8 | include gzip_params; 9 | 10 | server_name {{site_fqdn}}; 11 | root {{public_path}}; 12 | index index.html; 13 | error_page 404 /404.html; 14 | {% if ssl and inventory_hostname != 'vagrant' %} 15 | 16 | # This allows certbot to get letsencrypt certs. 17 | location /.well-known { 18 | alias {{base_path}}/certbot/.well-known; 19 | } 20 | {% endif %} 21 | 22 | # If you want to redirect everything to index.html (eg. for a web app), 23 | # remove the error_page line above and uncomment this block: 24 | # location / { 25 | # try_files $uri /index.html; 26 | # } 27 | } 28 | 29 | {% if ssl %} 30 | # Force HTTPS for all connections. 31 | server { 32 | listen 80; 33 | server_name {{site_fqdn}}; 34 | return 301 https://$server_name$request_uri; 35 | } 36 | {% endif %} 37 | 38 | # Catchall, force unknown domains to redirect to site_fqdn. 39 | server { 40 | listen 80 default_server; 41 | server_name _; 42 | return 301 $scheme://{{site_fqdn}}$request_uri; 43 | } 44 | -------------------------------------------------------------------------------- /deploy/ansible/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Get the box up and running. These tasks run before the box is configured 2 | # or the project is cloned or built. All system dependencies should be 3 | # installed here. 4 | 5 | - name: ensure apt cache is updated 6 | apt: update_cache=yes cache_valid_time=3600 7 | 8 | - name: ensure all packages are upgraded safely 9 | apt: upgrade=safe 10 | when: env != "development" 11 | 12 | # Can't use ansible's apt_repository module because we need to fetch gpg keys 13 | # that are hosted on SNI-enabled servers. Python doesn't support SNI natively 14 | # until 2.7.9 and ubuntu 14.04 ships with 2.7.6. Updating the system python is 15 | # not a road we'll be going down. 16 | - name: add keys to apt 17 | shell: wget -qO - {{item}} | apt-key add - 18 | with_items: "{{ apt_keys }}" 19 | # If you are running this on a system with python 2.7.9+, use this instead 20 | #- name: add keys to apt 21 | # apt_key: url={{item}} state=present 22 | # with_items: apt_keys 23 | 24 | - name: add ppas to apt 25 | apt_repository: 26 | repo: "{{item}}" 27 | state: present 28 | with_items: "{{ apt_ppas }}" 29 | 30 | - name: install apt packages 31 | apt: 32 | name: "{{item}}" 33 | state: latest 34 | with_items: "{{ apt_packages }}" 35 | 36 | - name: update npm to latest 37 | npm: name=npm state=latest global=yes 38 | -------------------------------------------------------------------------------- /deploy/ansible/roles/deploy/tasks/checkout.yml: -------------------------------------------------------------------------------- 1 | # Clone the repo, checking out the specified commit. If a canonical-sha-named 2 | # directory for that commit doesn't already exist, or if "force" is true, 3 | # clone the repo and build it. 4 | 5 | - name: ensure pre-existing temp directory is removed 6 | file: path={{clone_path}} state=absent 7 | 8 | - name: clone git repo into temp directory 9 | git: 10 | repo: "{{synced_folder if local else git_repo}}" 11 | dest: "{{clone_path}}" 12 | version: "{{commit}}" 13 | 14 | - name: get sha of cloned repo 15 | command: git rev-parse HEAD 16 | args: 17 | chdir: "{{clone_path}}" 18 | register: sha 19 | changed_when: false 20 | 21 | - name: check if specified commit sha has already been deployed 22 | stat: path={{base_path}}/{{sha.stdout}} get_checksum=no get_md5=no 23 | register: sha_dir 24 | 25 | - include: build.yml 26 | when: force or not sha_dir.stat.exists 27 | 28 | - name: delete pre-existing sha-named directory 29 | file: path={{base_path}}/{{sha.stdout}} state=absent 30 | when: force and sha_dir.stat.exists 31 | 32 | - name: move cloned repo to sha-named directory 33 | command: mv {{clone_path}} {{base_path}}/{{sha.stdout}} 34 | when: force or not sha_dir.stat.exists 35 | 36 | - name: ensure just-created temp directory is removed 37 | file: path={{clone_path}} state=absent 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern Web Deployment Workflow 2 | 3 | The files and directory you care about are: 4 | 5 | * [Vagrantfile](Vagrantfile) 6 | * [ansible.cfg](ansible.cfg) 7 | * [.gitignore](.gitignore) 8 | * [deploy/](deploy/) 9 | 10 | Everything else is used to generate the [project home page][home], which is just 11 | a stylized version of the main project documentation 12 | ([deploy/README.md](deploy/README.md)). And yes, the deployment workflow 13 | homepage is deployed using the deployment workflow. 14 | 15 | [home]: https://deployment-workflow.bocoup.com/ 16 | 17 | If this is your first time here, you should start by reading [the 18 | documentation][home]. 19 | 20 | ## Developing the project homepage 21 | 22 | There are 2 ways to develop the example project. 23 | 24 | Without vagrant, which is easier and faster: 25 | 26 | 1. Ensure node.js and npm are installed. 27 | 1. `npm install` 28 | 1. `npm run dev` 29 | 1. Check the main page: `open public/index.html` 30 | 1. Check the 404 page: `open public/404.html` 31 | 1. Edit `build/*` and `deploy/README.md` files locally, pages should auto-reload 32 | 1. Repeat steps 4-6 33 | 34 | With vagrant, which more accurately reflects site behavior once deployed: 35 | 36 | 1. Ensure the [deployment workflow 37 | dependencies](https://deployment-workflow.bocoup.com/#dependencies) as well as 38 | node.js and npm are installed. 39 | 1. `vagrant up` 40 | 1. `npm install` 41 | 1. `npm run dev` 42 | 1. Check the main page: 43 | 1. Check the 404 page: 44 | 1. Edit `build/*` and `deploy/README.md` files locally, pages should auto-reload 45 | 1. Repeat steps 5-7 46 | 47 | When done, file a PR! 48 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/tasks/certbot.yml: -------------------------------------------------------------------------------- 1 | # Use certbot to generate letsencrypt ssl certs. This will also set up a cron 2 | # job that will ensure the certs are kept up-to-date. 3 | 4 | - name: add certbot ppa to apt 5 | apt_repository: 6 | repo: "ppa:certbot/certbot" 7 | 8 | - name: install certbot 9 | apt: 10 | name: certbot 11 | state: present 12 | 13 | - name: ensure certbot well-known path exists 14 | file: 15 | path: "{{base_path}}/certbot/.well-known" 16 | state: directory 17 | 18 | - name: test if certbot has been initialized 19 | stat: 20 | path: /etc/letsencrypt/live/{{site_fqdn}}/fullchain.pem 21 | register: cert_file 22 | 23 | - name: ensure any pending nginx reload happens immediately 24 | meta: flush_handlers 25 | when: cert_file.stat.exists == false 26 | 27 | - name: intialize certbot 28 | command: > 29 | certbot certonly --webroot --agree-tos --non-interactive 30 | {{ (env == 'production') | ternary('', '--test-cert') }} 31 | --email {{letsencrypt_email}} 32 | -w {{base_path}}/certbot 33 | -d {{site_fqdn}} 34 | when: cert_file.stat.exists == false 35 | 36 | - name: ensure certbot certs are linked 37 | file: 38 | src: '/etc/letsencrypt/live/{{site_fqdn}}/{{item.src}}' 39 | dest: '{{item.dest}}' 40 | state: link 41 | force: true 42 | with_items: 43 | - { src: 'fullchain.pem', dest: '{{ssl_cert_path}}' } 44 | - { src: 'privkey.pem', dest: '{{ssl_key_path}}' } 45 | notify: reload nginx 46 | 47 | - name: Add cron job for cert renewal 48 | cron: 49 | name: Certbot automatic renewal. 50 | job: "/usr/bin/certbot renew --quiet --no-self-upgrade && service nginx reload" 51 | minute: 0 52 | hour: 23 53 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/templates/ssl_params: -------------------------------------------------------------------------------- 1 | ssl_certificate_key {{ssl_key_path}}; 2 | ssl_certificate {{ssl_cert_path}}; 3 | 4 | ## Use a shared session cache for all workers. 5 | ## A 10mb cache will support ~40,000 SSL sessions 6 | ssl_session_cache shared:SSL:10m; 7 | 8 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 9 | ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; 10 | 11 | ## This protocol/cipher list provides maximum security but leaves some 12 | ## extremely old user agents out in the cold, namely Android <= 2.3.7 13 | ## and IE <= 8 on WinXP, and some search engine crawlers. 14 | # ssl_ciphers "AES256+EECDH:AES256+EDH:!aNULL"; 15 | 16 | ## Use OCSP Stapling unless explicitly disabled with the 17 | ## `ssl_disable_oscp_stapling` flag. Resolver is set up to use the OpenDNS 18 | ## public DNS resolution service (208.67.222.222 and 208.67.220.220) 19 | ssl_stapling on; 20 | ssl_stapling_verify on; 21 | resolver 208.67.222.222 208.67.220.220 valid=300s; 22 | resolver_timeout 10s; 23 | 24 | ## Always prefer the server cipher ordering, don't let the client choose. 25 | ssl_prefer_server_ciphers on; 26 | 27 | ## Use a custom parameter for stronger DHE key exchange. Must be 28 | ## generated during the provisioning process with 29 | ## `openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096` 30 | ssl_dhparam {{dhe_param_path}}; 31 | 32 | add_header Strict-Transport-Security max-age=63072000; 33 | add_header X-Content-Type-Options nosniff; 34 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Generate nginx config files (and ssl configuration, if ssl was specified), 2 | # rolling back changes if any part of the config is invalid. 3 | 4 | - include: ssl.yml 5 | when: ssl 6 | 7 | - name: ensure default nginx server is not present 8 | file: path=/etc/nginx/sites-enabled/default state=absent 9 | 10 | - name: ensure nginx config files exist 11 | shell: touch {{item.directory}}/{{item.file}} 12 | args: 13 | creates: "{{item.directory}}/{{item.file}}" 14 | with_flattened: 15 | - "{{ confs }}" 16 | - "{{ shared }}" 17 | 18 | - name: backup existing nginx config files 19 | shell: cp {{item.file}} {{item.file}}.backup 20 | args: 21 | chdir: "{{item.directory}}" 22 | with_flattened: 23 | - "{{ confs }}" 24 | - "{{ shared }}" 25 | 26 | - name: generate new nginx config files 27 | template: src={{item.file}} dest={{item.directory}}/ 28 | register: nginx_conf 29 | with_flattened: 30 | - "{{ confs }}" 31 | - "{{ shared }}" 32 | notify: reload nginx 33 | 34 | - name: ensure nginx config is valid 35 | shell: nginx -t 36 | ignore_errors: yes 37 | register: nginx_test_valid 38 | changed_when: false 39 | when: nginx_conf | changed 40 | 41 | - name: remove temporary backups if new nginx config files are valid 42 | file: path={{item.directory}}/{{item.file}}.backup state=absent 43 | with_flattened: 44 | - "{{ confs }}" 45 | - "{{ shared }}" 46 | when: nginx_test_valid | success 47 | 48 | - name: restore temporary backups if new nginx config files are invalid 49 | shell: mv {{item.file}}.backup {{item.file}} 50 | args: 51 | chdir: "{{item.directory}}" 52 | with_items: "{{ confs }}" 53 | when: nginx_test_valid | failed 54 | 55 | - fail: msg="nginx config is invalid" 56 | when: nginx_test_valid | failed 57 | 58 | - include: certbot.yml 59 | when: ssl and inventory_hostname != 'vagrant' 60 | -------------------------------------------------------------------------------- /deploy/ansible/roles/deploy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Clone the repo and check out the specified "commit" (defaults to master) 2 | # unless it has already been checked out. Specifying "force" will clone and 3 | # build regardless of prior status. When done, symlink the specified commit 4 | # to make it go live, and remove old clones to free up disk space. 5 | 6 | - name: check if specified commit has already been deployed 7 | stat: path={{base_path}}/{{commit}} get_checksum=no get_md5=no 8 | register: commit_dir 9 | 10 | - include: checkout.yml 11 | when: force or not commit_dir.stat.exists 12 | 13 | - name: link sha-named clone to make it live 14 | file: 15 | path: "{{site_path}}" 16 | state: link 17 | src: "{{base_path}}/{{ sha.stdout | default(commit) }}" 18 | force: yes 19 | 20 | - name: update last-modification time of sha-named clone 21 | file: path={{base_path}}/{{ sha.stdout | default(commit) }} state=touch 22 | 23 | - name: remove old clones to free up disk space 24 | shell: | 25 | # Find all 40-char-SHA-named child directories and for each directory, print 26 | # out the last-modified timestamp and the SHA. 27 | find . -mindepth 1 -maxdepth 1 -type d \ 28 | -regextype posix-extended -regex './[0-9a-f]{40}' -printf '%T@ %P\n' | 29 | # Sort numerically in ascending order (on the timestamp), remove the 30 | # timestamp from each line (leaving only the SHA), then remove the most 31 | # recent SHAs from the list (leaving only the old SHAs-to-be-removed). 32 | sort -n | cut -d ' ' -f 2 | head -n -{{keep_n_most_recent}} | 33 | # Remove each remaining SHA-named directory and echo the SHA (so the task 34 | # can display whether or not changes were made). 35 | xargs -I % sh -c 'rm -rf "$1"; echo "$1"' -- % 36 | register: remove_result 37 | changed_when: remove_result.stdout != "" 38 | args: 39 | chdir: "{{base_path}}" 40 | when: keep_n_most_recent is defined 41 | -------------------------------------------------------------------------------- /deploy/ansible/roles/configure/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Configure the box. This happens after the base initialization, but before 2 | # the project is cloned or built. 3 | 4 | - name: set hostname 5 | hostname: name={{hostname}} 6 | 7 | - name: ensure unattended upgrades are running 8 | copy: 9 | content: | 10 | APT::Periodic::Update-Package-Lists "1"; 11 | APT::Periodic::Download-Upgradeable-Packages "1"; 12 | APT::Periodic::AutocleanInterval "7"; 13 | APT::Periodic::Unattended-Upgrade "1"; 14 | dest: /etc/apt/apt.conf.d/10periodic 15 | 16 | - name: add loopback references to our domain in /etc/hosts 17 | lineinfile: 18 | dest: /etc/hosts 19 | state: present 20 | line: "127.0.0.1 {{hostname}} {{site_fqdn}}" 21 | 22 | - name: disallow password authentication 23 | lineinfile: 24 | dest: /etc/ssh/sshd_config 25 | state: present 26 | regexp: "^PasswordAuthentication" 27 | line: "PasswordAuthentication no" 28 | notify: restart sshd 29 | 30 | - name: disallow challenge response authentication 31 | lineinfile: 32 | dest: /etc/ssh/sshd_config 33 | state: present 34 | regexp: "^ChallengeResponseAuthentication" 35 | line: "ChallengeResponseAuthentication no" 36 | notify: restart sshd 37 | 38 | - name: ensure github.com is a known host 39 | lineinfile: 40 | dest: /etc/ssh/ssh_known_hosts 41 | state: present 42 | create: yes 43 | regexp: "^github\\.com" 44 | line: "{{ lookup('pipe', 'ssh-keyscan -t rsa github.com') }}" 45 | 46 | - name: ensure ssh agent socket environment variable persists when sudoing 47 | lineinfile: 48 | dest: /etc/sudoers 49 | state: present 50 | insertafter: "^Defaults" 51 | line: "Defaults\tenv_keep += \"SSH_AUTH_SOCK\"" 52 | validate: "visudo -cf %s" 53 | 54 | - name: allow passwordless sudo - development only! 55 | lineinfile: 56 | dest: /etc/sudoers 57 | state: present 58 | regexp: "^%sudo" 59 | line: "%sudo\tALL=(ALL:ALL) NOPASSWD:ALL" 60 | validate: "visudo -cf %s" 61 | when: "{{ env == 'development' }}" 62 | 63 | - include: swap.yml 64 | when: swap_file_path is defined and swap_file_size is defined 65 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = '2' 5 | 6 | # look up system cpu and ram so we can use more intelligent defaults 7 | LINUX = RUBY_PLATFORM =~ /linux/ 8 | OSX = RUBY_PLATFORM =~ /darwin/ 9 | if OSX 10 | CPUS = `sysctl -n hw.ncpu`.to_i 11 | MEM = `sysctl -n hw.memsize`.to_i / 1024 / 1024 / 4 12 | elsif LINUX 13 | CPUS = `nproc`.to_i 14 | MEM = `sed -n -e '/^MemTotal/s/^[^0-9]*//p' /proc/meminfo`.to_i / 1024 / 4 15 | end 16 | 17 | # use (faster) nfs sharing on osx only 18 | SHARING = OSX ? { nfs: true } : nil 19 | 20 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 21 | config.vm.box = 'ubuntu/trusty64' 22 | 23 | # Allow the project directory to be accessible inside the Vagrant box. 24 | # This should match the Ansible host_vars/vagrant synced_folder value. 25 | config.vm.synced_folder '.', '/mnt/vagrant', SHARING 26 | 27 | # Ideally, this IP will be unique, so the entry added to /etc/hosts won't 28 | # conflict with that of another project. 29 | config.vm.network :private_network, ip: '192.168.33.99' 30 | 31 | # Automatically add an entry to /etc/hosts for this Vagrant box (requires 32 | # sudo). This should match the Ansible host_vars/vagrant site_fqdn value. 33 | config.hostsupdater.aliases = ['deployment-workflow.loc'] 34 | 35 | # give vm access to 1/4 total system memory and all cpu 36 | config.vm.provider 'virtualbox' do |v| 37 | v.customize ['modifyvm', :id, '--memory', MEM] if defined?(MEM) 38 | v.customize ['modifyvm', :id, '--cpus', CPUS] if defined?(CPUS) 39 | end 40 | 41 | # A specific name looks much better than "default" in ansible output. 42 | config.vm.define 'vagrant' 43 | 44 | # The Vagrant ansible provisioner is used here for convenience. Instead of 45 | # the following code, the Vagrant box may be provisioned manually with 46 | # ansible-playbook (like in production), but adding this code saves the 47 | # trouble of having to run ansible-playbook manually after "vagrant up". 48 | config.vm.provision 'ansible' do |ansible| 49 | # Run init playbook (which runs base, configure, vagrant-link playbooks). 50 | ansible.playbook = 'deploy/ansible/init.yml' 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /deploy/run-playbook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bin=ansible-playbook 4 | 5 | function usage() { 6 | cat < "templates" 31 | var processTemplates = function(methods) { 32 | return [//g, function(src, all, method, args) { 33 | var fn = methods[method] || function() { return all; }; 34 | return fn.apply(null, [src].concat(args.split(' '))); 35 | }]; 36 | }; 37 | 38 | // Replacements to make before the markdown is parsed into HTML. 39 | var preReplacements = [ 40 | processTemplates({ 41 | // Display specified role task/template files. 42 | 'role-files': function(src, role) { 43 | var glob, files; 44 | var rolebase = 'ansible/roles/' + role; 45 | var tasks = []; 46 | glob = rolebase + '/tasks/*.yml'; 47 | files = grunt.file.expand({cwd: 'deploy'}, glob); 48 | files.forEach(function(filepath) { 49 | var filename = path.basename(filepath); 50 | var lines = grunt.file.read('deploy/' + filepath).split('\n'); 51 | var memo = {}; 52 | var i = lines.length - 1; 53 | var doc; 54 | while (i--) { 55 | try { 56 | doc = yaml.safeLoad(lines.slice(i).join('\n')); 57 | } catch (err) {} 58 | var name = doc && doc[0] && doc[0].name; 59 | if (name && !memo[name]) { 60 | memo[name] = true; 61 | tasks.unshift(' * [' + name + '](' + filepath + '#L' + (i + 1) + ')'); 62 | } 63 | } 64 | tasks.unshift('* [' + filename + '](' + filepath + ')'); 65 | }); 66 | var templates = []; 67 | glob = rolebase + '/templates/*'; 68 | files = grunt.file.expand({cwd: 'deploy'}, glob); 69 | files.forEach(function(filepath) { 70 | var filename = path.basename(filepath); 71 | templates.push('* [' + filename + '](' + filepath + ')'); 72 | }); 73 | var output = [ 74 | 'This role contains the following files and tasks:', 75 | '', 76 | ].concat(tasks); 77 | if (templates.length > 0) { 78 | output = output.concat([ 79 | '', 80 | 'And the following templates:', 81 | '', 82 | ]).concat(templates); 83 | } 84 | output.push( 85 | '', 86 | '_(Browse the [' + rolebase + '](' + rolebase + ') directory for more information)_' 87 | ); 88 | return output.join('\n'); 89 | }, 90 | }), 91 | ]; 92 | 93 | // Replacements to make after the markdown is parsed into HTML. 94 | var postReplacements = [ 95 | // Change relative project URLs to absolute 96 | [/(]+>)([^<]+)/g, function(src, all, pre, href, post, text) { 97 | if (/^(?:https?:\/\/|#)/.test(href)) { return all; } 98 | var baseHref = githubUrl + 'blob/' + branch + '/deploy/'; 99 | if (/^\.\.\//.test(href)) { 100 | // Vagrantfile 101 | text = text.slice(3); 102 | } else if (href === '.') { 103 | // The deploy folder itself 104 | href = ''; 105 | baseHref = baseHref.slice(0, -1); 106 | } else if (text === href) { 107 | // Files within the deploy folder 108 | text = 'deploy/' + text; 109 | } 110 | return pre + baseHref + href + post + text; 111 | }], 112 | // Make headers "linkable" in the Github style 113 | [/(]+>)/g, function(src, all, pre, id, post) { 114 | return pre + id + post + ''; 115 | }], 116 | [/^[\s\S]+$/, function(src, all) { 117 | var headers = toc.anchorize(all, { 118 | tocMin: 2, 119 | tocMax: 3, 120 | }).headers; 121 | var nav = toc.toc(headers, { 122 | TOC: '<%= toc %>', 123 | openUL: '
    ', 124 | closeUL: '
', 125 | openLI: '
  • <%= text %>', 126 | closeLI: '
  • ', 127 | }); 128 | templateContext.nav = nav; 129 | return all; 130 | }], 131 | ]; 132 | 133 | var templateContext; 134 | grunt.initConfig({ 135 | clean: { 136 | all: 'public', 137 | }, 138 | copy: { 139 | wwwroot: { 140 | expand: true, 141 | cwd: 'build/wwwroot', 142 | src: '**/*', 143 | dest: 'public', 144 | } 145 | }, 146 | markdown: { 147 | options: { 148 | template: 'build/index.html', 149 | templateContext: function() { 150 | templateContext = { 151 | mode: mode, 152 | nav: '', 153 | githubUrl: githubUrl, 154 | date: (new Date).toDateString(), 155 | }; 156 | return templateContext; 157 | }, 158 | }, 159 | readme: { 160 | options: { 161 | preCompile: function(src, context) { 162 | return preReplacements.reduce(function(src, arr) { 163 | // return src.replace.apply(src, arr); 164 | return src.replace(arr[0], arr[1].bind(null, src)); 165 | }, src); 166 | }, 167 | postCompile: function(src, context) { 168 | return postReplacements.reduce(function(src, arr) { 169 | // return src.replace.apply(src, arr); 170 | return src.replace(arr[0], arr[1].bind(null, src)); 171 | }, src); 172 | }, 173 | }, 174 | src: 'deploy/README.md', 175 | dest: 'public/index.html', 176 | }, 177 | '404': { 178 | src: 'build/404.md', 179 | dest: 'public/404.html', 180 | } 181 | }, 182 | watch: { 183 | readme: { 184 | options: { 185 | livereload: true, 186 | }, 187 | files: [ 188 | 'deploy/README.md', 189 | 'build/**', 190 | ], 191 | tasks: ['build-dev'] 192 | }, 193 | }, 194 | }); 195 | 196 | grunt.registerTask('build', ['clean', 'copy', 'get_branch', 'markdown']); 197 | grunt.registerTask('build-dev', ['mode:dev', 'build']); 198 | grunt.registerTask('dev', ['build-dev', 'watch']); 199 | grunt.registerTask('default', ['dev']); 200 | }; 201 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Modern Web Deployment Workflow 2 | 3 |
    Brought to you by
    4 | 5 | This collection of Ansible playbooks have been designed to simplify deployment 6 | of a modern website or web app using Vagrant, Ubuntu, nginx and HTTP/HTTPS. Many 7 | tasks have been separated into separate roles, and as much configuration as 8 | possible has been abstracted into external variables. 9 | 10 | High-level benefits include: 11 | 12 | * A new server can be up and running with fully deployed code in just a few 13 | minutes. 14 | * An update to an existing project can be deployed and built in under a minute. 15 | * A project can be rolled back to a previously-deployed version in a matter of 16 | seconds. 17 | * Updates to server configuration can be made in a matter of seconds. 18 | * Most server configuration and code updates can be made with zero server 19 | downtime. 20 | * Code can be tested locally in Vagrant before being deployed to a production 21 | server. 22 | * Code can be tested on a staging server for QA or final testing before being 23 | deployed to a production server. 24 | * Server configuration and project deployment can be made to scale to any number 25 | of remote hosts. 26 | 27 | More specific benefits include: 28 | 29 | * Almost all server configuration and project deployment information is stored 30 | in the project, making it easy to destroy and re-create servers with 31 | confidence. 32 | * All project maintainer user account information is stored in the project, 33 | making it easy to add or remove project maintainers. 34 | * SSH agent forwarding allows the remote server to access private GitHub repos 35 | without requiring a private key to be copied to the server or for dedicated 36 | deployment keys to be configured. 37 | * While working locally, the Vagrant box can easily be toggled between 38 | development and deployment modes at any time. This allows local changes to be 39 | previewed instantly (development) or a specific commit to be built as it would 40 | be in production (deployment). 41 | * SSL certs can be auto-generated for testing HTTPS in development. 42 | * Because the entire deployment workflow is comprised of Ansible playbooks and a 43 | Vagrantfile, it can be easily modified to meet any project's needs. 44 | 45 | Here are links to the official, original project home page, documentation, Git 46 | repository and wiki: 47 | 48 | * [Canonical home page & documentation](https://deployment-workflow.bocoup.com/) 49 | * [Canonical Git repository](https://github.com/bocoup/deployment-workflow/) 50 | * [Canonical wiki](https://github.com/bocoup/deployment-workflow/wiki) 51 | 52 | Notes: 53 | 54 | * Even though Node.js and npm are used in this sample project, with minor 55 | edits this workflow can be made to work with basically any programming 56 | language, package manager, etc. 57 | * This workflow won't teach you how to create an AWS instance. Fortunately, 58 | there are already excellent guides for [creating a key 59 | pair](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html), 60 | [setting up a security 61 | group](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html) 62 | and [launching an 63 | instance](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-instance_linux.html). 64 | * This workflow has been thoroughly tested in [Ubuntu 14.04 LTS 65 | (trusty)](http://releases.ubuntu.com/14.04/). More specifically, with the 66 | [ubuntu/trusty64](https://vagrantcloud.com/ubuntu/boxes/trusty64) Vagrant 67 | image and with the AWS EC2 `Ubuntu Server 14.04 LTS` AMI. Minor adjustments 68 | might need to be made for other providers, while more substantial changes 69 | might need to be made for other Ubuntu versions or Linux distributions. 70 | * While this workflow has been designed to meet the needs of a typical use 71 | case, it might not meet your project's needs exactly. Consider this to be 72 | a starting point; you're encouraged to edit the included playbooks and roles! 73 | 74 | ## Overview 75 | 76 | Assuming you've already created (or are in the process of creating) a website or 77 | web app, you will typically perform these steps when using this workflow. 78 | 79 | 1. Ensure the [workflow dependencies](#dependencies) have been installed on your 80 | development machine. 81 | 1. Add and commit the [workflow files](#initial-setup) into your project. 82 | 1. Modify the [Ansible variables, playbooks and roles](#ansible) to meet your 83 | specific project needs. 84 | 1. Test your project on the [local Vagrant box](#vagrant) while authoring 85 | it. _(Optional, but recommended)_ 86 | 1. [Deploy](#deploying) your project to a staging server for QA and final 87 | testing. _(Optional, but recommended)_ 88 | 1. [Deploy](#deploying) your project to a production server. 89 | 90 | Step 1 is usually only done per development machine, steps 2-3 are usually only 91 | done per project, and steps 4-6 will be repeated throughout the life of your 92 | project as you make and test changes and deploy new versions of your website or 93 | web app. 94 | 95 | ## Dependencies 96 | 97 | The following will need to be installed on your local development machine before 98 | you can use this workflow. All versions should be the latest available unless 99 | otherwise specified. 100 | 101 | * **[Ansible (version 2.1.x)](http://docs.ansible.com/)** 102 | - Install `ansible` via apt (Ubuntu), yum (Fedora), [homebrew][homebrew] (OS 103 | X), etc. See the [Ansible installation 104 | instructions](http://docs.ansible.com/intro_installation.html) for detailed, 105 | platform-specific information. 106 | * **[VirtualBox](https://www.virtualbox.org/)** 107 | - [Download](https://www.virtualbox.org/wiki/Downloads) (All platforms) 108 | - Install `virtualbox` via [homebrew cask][cask] (OS X) 109 | * **[Vagrant](https://www.vagrantup.com/)** 110 | - [Download](http://docs.vagrantup.com/v2/installation/) (All platforms) 111 | - Install `vagrant` via [homebrew cask][cask] (OS X) 112 | * **[vagrant-hostsupdater](https://github.com/cogitatio/vagrant-hostsupdater)** 113 | - Install with `vagrant plugin install vagrant-hostsupdater` (All platforms) 114 | 115 | [homebrew]: http://brew.sh/ 116 | [cask]: http://caskroom.io/ 117 | 118 | Notes: 119 | 120 | * Ansible doesn't really work in Windows. But it works great in OS X and Linux, 121 | so be sure to use one of those operating systems for development. 122 | 123 | ## Initial Setup 124 | 125 | Copy this project's files so that the [deploy](.) directory is in the root of 126 | your project Git repository. Be sure to copy recursively and preserve file 127 | modes, eg. executable, so that the [bash helper script](#bash-helper-script) 128 | continues to work. The [Vagrantfile](#configuring-vagrant) and 129 | [ansible.cfg](#ansible-configuration) file should be placed in your project root 130 | directory, _not_ the deploy directory. 131 | 132 | Also, be sure to add `.vagrant` to your project's `.gitignore` file so that 133 | directory's contents, which are auto-generated by Vagrant, aren't committed with 134 | your project's source. 135 | 136 | ## Ansible 137 | 138 | At the core of this workflow is Ansible, an IT automation tool. Ansible aims to 139 | be simple to configure and easy to use, while being secure and reliable. In this 140 | workflow, Ansible is used to configure systems and deploy software. 141 | 142 | ### Ansible Configuration 143 | 144 | #### Ansible Variables 145 | 146 | Much of this workflow's behavior can be configured via Ansible variables. 147 | 148 | * [ansible/group_vars/all.yml](ansible/group_vars/all.yml) - variables global to all 149 | [playbooks](#ansible-playbooks) and [roles](#ansible-roles) 150 | 151 | Host-specific settings may be defined in host-named files in the 152 | [host_vars](ansible/host_vars) directory and will override global values. 153 | 154 | * [ansible/host_vars/vagrant](ansible/host_vars/vagrant) - variables specific to the 155 | `vagrant` [inventory](#ansible-inventory) host 156 | 157 | Variables may be overridden when a playbook is run via the `--extra-vars` 158 | command line option. These variables are noted in the preceding files as `EXTRA 159 | VARS`. 160 | 161 | See the [Ansible variables](https://docs.ansible.com/playbooks_variables.html) 162 | documentation for more information on variables, variable precedence, and how 163 | `{{ }}` templates and filters work. 164 | 165 | ### Ansible Inventory 166 | 167 | These files contain the addresses of any servers to which this project will be 168 | deployed. Usually, these addresses will be [fully qualified domain 169 | names](https://en.wikipedia.org/wiki/Fully_qualified_domain_name), but they may 170 | also be IPs. Each inventory file may contain a list of multiple server FQDNs or 171 | IPs, allowing a playbook to be deployed to multiple servers simultaneously, but 172 | for this workflow, each inventory file will list a single server. 173 | 174 | * [ansible/inventory/production](ansible/inventory/production) 175 | * [ansible/inventory/staging](ansible/inventory/staging) 176 | * [ansible/inventory/vagrant](ansible/inventory/vagrant) 177 | 178 | Like with [host variables](#ansible-variables), settings defined here will 179 | override those defined in the [global variables](#ansible-variables) and [group 180 | variables](#ansible-variables) files. For example, in the staging inventory, the 181 | `site_fqdn` variable can be set to the staging server's FQDN, allowing nginx to 182 | respond to requests made to _its_ FQDN instead of the production server's FQDN. 183 | 184 | Unless the variable is a server name-specific override like `site_fqdn` or 185 | `ansible_ssh_host`, it should probably be defined in [host 186 | variables](#ansible-variables). 187 | 188 | ### Ansible Playbooks 189 | 190 | Ansible playbooks are human-readable documents that describe and configure the 191 | tasks that Ansible will run on a remote server. They should be idempotent, 192 | allowing them to be run multiple times with the same result each time. 193 | 194 | The following playbooks are included in this workflow: 195 | 196 | * [provision playbook](#provision-playbook) 197 | * [configure playbook](#configure-playbook) 198 | * [deploy playbook](#deploy-playbook) 199 | * [vagrant-link playbook](#vagrant-link-playbook) 200 | * [init playbook](#init-playbook) 201 | 202 | For more detailed information on what each playbook actually does and how it 203 | will need to be configured, be sure to check out the description for each 204 | [role](#ansible-roles) that playbook includes. 205 | 206 | #### provision playbook 207 | 208 | Provision server. This playbook must be run when a server is first created 209 | and is typically only run once. It may be run again if you make server-level 210 | changes or need to update any installed apt modules to their latest versions. 211 | If you were creating a new AMI or base box, you'd do so after running only 212 | this playbook. 213 | 214 | * Playbook: [ansible/provision.yml](ansible/provision.yml) 215 | * Roles: [base](#base-role) 216 | 217 | #### configure playbook 218 | 219 | Configure server. This playbook is run after a server is provisioned but 220 | before a project is deployed, to configure the system, add user accounts, 221 | and setup long-running processes like nginx, postgres, etc. 222 | 223 | * Playbook: [ansible/configure.yml](ansible/configure.yml) 224 | * Roles: [configure](#configure-role), [users](#users-role), [nginx](#nginx-role) 225 | 226 | #### deploy playbook 227 | 228 | Clone, build, and deploy, restarting nginx if necessary. This playbook must 229 | be run after `provision` and `configure`, and is used to deploy and build the 230 | specified commit (overridable via extra vars) on the server. Running this 231 | playbook in Vagrant will override the `vagrant-link` playbook, and vice-versa. 232 | 233 | * Playbook: [ansible/deploy.yml](ansible/deploy.yml) 234 | * Roles: [deploy](#deploy-role) 235 | 236 | #### vagrant-link playbook 237 | 238 | Instead of cloning the Git repo and building like the `deploy` playbook, this 239 | playbook links your local working project directory into the Vagrant box so that 240 | you can instantly preview your local changes on the server, for convenience 241 | while developing. While in this mode, all building will have to be done 242 | manually, at the command line of your development machine. Running this playbook 243 | will override the `deploy` playbook, and vice-versa. 244 | 245 | * Playbook: [ansible/vagrant-link.yml](ansible/vagrant-link.yml) 246 | 247 | #### init playbook 248 | 249 | This playbook saves the trouble of running the `provision`, `configure` and 250 | `vagrant-link` playbooks individually, and is provided for convenience. After 251 | `vagrant up`, this playbook will be run on the new Vagrant box. 252 | 253 | * Playbook: [ansible/init.yml](ansible/init.yml) 254 | 255 | ### Ansible Roles 256 | 257 | There are multiple ways to organize playbooks, and while it's possible to put 258 | all your tasks into a single playbook, it's often beneficial to separate related 259 | tasks into "roles" that can be included in one or more playbooks, for easy reuse 260 | and organization. 261 | 262 | The following roles are used by this workflow's playbooks: 263 | 264 | * [base role](#base-role) 265 | * [configure role](#configure-role) 266 | * [nginx role](#nginx-role) 267 | * [users role](#users-role) 268 | * [deploy role](#deploy-role) 269 | 270 | #### base role 271 | 272 | Get the box up and running. These tasks run before the box is configured 273 | or the project is cloned or built. All system dependencies should be 274 | installed here. 275 | 276 | Apt keys, apt ppas, apt packages and global npm modules can be configured in the 277 | `PROVISIONING` section of the [global variables](#ansible-variables) file. If 278 | you need custom packages or modules to be installed, specify them there. 279 | 280 | Don't be afraid to modify these tasks. For example, if your project doesn't use 281 | npm, just remove the npm tasks. 282 | 283 | 284 | 285 | #### configure role 286 | 287 | Configure the box. This happens after the base initialization, but before 288 | the project is cloned or built. 289 | 290 | 291 | 292 | #### nginx role 293 | 294 | Generate nginx config files (and ssl configuration, if ssl was specified), 295 | rolling back changes if any part of the config is invalid. 296 | 297 | The public site path, ssl and ssl cert/key file locations can be configured in 298 | the `WEB SERVER` section of the [global variables](#ansible-variables) file. If 299 | you want to override any settings for just Vagrant, you can do so in [host 300 | variables](#ansible-variables). 301 | 302 | By default, nginx is configured to serve a website with a custom 404 page. 303 | However, if you want to redirect all requests to `index.html` (eg. for a web 304 | app), you should modify the [site.conf](ansible/roles/nginx/templates/site.conf) 305 | template per the inline comments. For more involved customization, read the 306 | [nginx documentation](http://nginx.org/en/docs/). 307 | 308 | If you enable SSL for `production` or `staging`, you will need to supply your 309 | own signed SSL cert/key files and put them on the remote server via [AWS 310 | CloudFormation](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/deploying.applications.html), 311 | [cloud-init](http://cloudinit.readthedocs.org/), or you can [copy them 312 | manually](https://github.com/bocoup/deployment-workflow/wiki/FAQ#how-do-i-manually-copy-ssl-certs-to-a-remote-server) 313 | _before_ provisioning, or this role will fail. 314 | 315 | If you choose to enable SSL for `vagrant`, self-signed SSL cert/key files will 316 | be generated for you automatically if they don't already exist. In this case, 317 | your website or web app will work, but you will have to click past a SSL 318 | certificate warning before viewing it. 319 | 320 | 321 | 322 | #### users role 323 | 324 | In development ([localuser.yml](ansible/roles/users/tasks/localuser.yml)), 325 | create an account for the currently logged-in user, and copy their public key to 326 | the server. This makes it possible to run other playbooks without specifying a 327 | user or private key on the command line. 328 | 329 | In production ([users.yml](ansible/roles/users/tasks/users.yml)), ensure all 330 | users have been added, along with any public keys. If any user's state is 331 | `absent`, they will be removed. If any keys are removed, they will be deleted. 332 | In a development environment, make sudo passwordless, for convenience. 333 | 334 | User accounts, passwords and public keys can be configured in the `USERS` 335 | section of the [global variables](#ansible-variables) file. 336 | 337 | 338 | 339 | #### deploy role 340 | 341 | Clone the repo and check out the specified commit unless it has already been 342 | checked out. When done, symlink the specified commit to make it go live, and 343 | remove old clones to free up disk space. 344 | 345 | The Git repo URL, deployment paths, number of recent deployments to retain and 346 | build info file path can be configured in the `DEPLOY` section of the 347 | [global variables](#ansible-variables) file. 348 | 349 | The following variables defined in the `DEPLOY EXTRA VARS` section of the same 350 | file may be overridden on the `ansible-playbook` command line in the format 351 | `--extra-vars="commit=mybranch force=true"`. 352 | 353 | var | default | description 354 | ---------|----------|------------ 355 | `remote` | `bocoup` | Specify any remote (typically a github user). 356 | `commit` | `master` | Specify any ref (eg. branch, tag, SHA) to be deployed. This ref must be pushed to the remote `git_repo` before it can be deployed. 357 | `force` | `false` | Clone and build the specified commit SHA, regardless of prior build status. 358 | `local` | `false` | Use the local project Git repo instead of the remote `git_repo`. This option only works with the `vagrant` inventory, and not with `staging` or `production`. 359 | 360 | The [build.yml](ansible/roles/deploy/tasks/build.yml) file contains all the 361 | build tasks that need to be run after your project is cloned, eg. `npm install`, 362 | `npm run build`. Don't be afraid to modify these tasks. Your project's build 363 | process might need to be different than what's here, so adjust accordingly! 364 | 365 | 366 | 367 | ## Vagrant 368 | 369 | Vagrant allows you to isolate project dependencies (like nginx or postgres) in a 370 | stable, disposable, consistent work environment. In conjunction with Ansible and 371 | VirtualBox, Vagrant ensures that anyone on your team has access to their own 372 | private, pre-configured development server whenever they need it. 373 | 374 | If you only want to deploy to remote production or staging servers, you can just 375 | install Ansible and skip VirtualBox and Vagrant, which are only used to create 376 | the local development server. 377 | 378 | ### Configuring Vagrant 379 | 380 | The [../Vagrantfile](../Vagrantfile) file at the root of the repo contains the 381 | project's Vagrant configuration. Be sure to specify an appropriate hostname 382 | alias for the Vagrant box here. 383 | 384 | ### Using Vagrant 385 | 386 | Once the [Vagrantfile](#configuring-vagrant) and [Ansible variables, playbooks 387 | and roles](#ansible-configuration) have been customized to meet your project's 388 | needs, you should be able to run `vagrant up` to create a local, 389 | fully-provisioned Vagrant server that is accessible in-browser via the hostname 390 | alias specified in the Vagrantfile. _If you're asked for your administrator 391 | password during this process, it's so that the hostsupdater plugin can modify 392 | your `/etc/hosts` file._ 393 | 394 | If you change the Ansible configuration, running `vagrant provision` will re-run 395 | the Ansible playbooks. If you make drastic changes to the Ansible configuration 396 | and need to recreate the Vagrant server (which is often the case), you can 397 | delete it with `vagrant destroy`. _If you do this, be sure to let collaborators 398 | know too!_ 399 | 400 | See the Vagrant [Ansible 401 | provisioner](http://docs.vagrantup.com/v2/provisioning/ansible.html) 402 | documentation for more information. 403 | 404 | ### Using SSH with Vagrant 405 | 406 | Vagrant provides the `vagrant ssh` command which allows you to connect to the 407 | Vagrant box via its built-in `vagrant` user. While this is convenient for some 408 | basic development tasks, once provisioned, you should connect to the Vagrant box 409 | using the user account created by the [users role](#users-role). This will 410 | ensure that the [ansible-playbook](#deploying) command, which uses `ssh` 411 | internally, will work, allowing you to deploy. 412 | 413 | To connect to Vagrant in this way, use the `ssh` command along with the 414 | hostname alias defined in the [Vagrantfile](#configuring-vagrant). Eg, for this 415 | example project, the command would be `ssh deployment-workflow.loc`. 416 | 417 | Also, adding a [section like 418 | this](https://github.com/cowboy/dotfiles/blob/8e4fa2a/link/.ssh/config#L9-L14) 419 | to your `~/.ssh/config` file will prevent SSH from storing Vagrant box keys in 420 | `~/.ssh/known_hosts` and complaining about them not matching when a Vagrant 421 | box is destroyed and recreated. _Do not do this for production servers. This is 422 | only safe for private, local virtual machines!_ 423 | 424 | ## Deploying 425 | 426 | Once you've customized [Ansible variables, playbooks and 427 | roles](#ansible-configuration) and committed your changes to the Git repository 428 | configured in [global variables](#ansible-variables), you may run the 429 | `ansible-playbook` command or the included [playbook helper 430 | script](#playbook-helper-script) to run any [playbook](#ansible-playbooks) on 431 | any [inventory](#ansible-inventory) host. 432 | 433 | 434 | ### Command Line Flags 435 | 436 | Note that the following flags apply to both `ansible-playbook` and the included 437 | [playbook helper script](#playbook-helper-script). 438 | 439 | * **`--help`** - Display usage information and all available options; the list 440 | here contains only the most relevant subset of all options. 441 | * **`--user`** - Connect to the server with the specified user. If a user isn't 442 | specified, the currently logged-in user's username will be used. 443 | * **`--ask-become-pass`** - If the remote user account requires a password to be 444 | entered, you will need to specify this option. 445 | * **`--private-key`** - If the remote user account requires a private key, you 446 | will need to specify this option. 447 | * **`--extra-vars`** - Values that override those stored in the [ansible 448 | configuration](#ansible-variables) in the format 449 | `--extra-vars="commit=mybranch force=true"`. 450 | * **`-vvvv`** - Display verbose connection debugging information. 451 | 452 | #### Production and Staging Notes 453 | 454 | Once the [users role](#users-role) has run successfully, assuming your user 455 | account has been correctly added to it, you should be able to omit the `--user` 456 | and `--private-key` command line flags. However, until the users role has run at 457 | least once: 458 | 459 | * the `--user` flag will need to be specified. For the default AWS EC2 Ubuntu 460 | AMI, use `--user=ubuntu`. 461 | * the `--private-key` flag will need to be specified. For AWS, specify 462 | `--private-key=/path/to/keyfile.pem` where `keyfile.pem` is the file 463 | downloaded when [creating a new key 464 | pair](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) 465 | in AWS. _Do not store this private key in your project Git repo!_ 466 | 467 | The default AWS `ubuntu` user doesn't require a password for `sudo`, but user 468 | accounts added via the users role do, so be sure to specify the 469 | `--ask-become-pass` flag when you omit the `--user` command line flag. 470 | 471 | #### Vagrant Notes 472 | 473 | Once the [users role](#users-role) has run successfully, assuming your user 474 | account has been correctly added to it, you should be able to omit the `--user` 475 | and `--private-key` command line flags. However, until the users role has run at 476 | least once: 477 | 478 | * specify `--user=vagrant`, which is the default user created for the Vagrant 479 | box. 480 | * specify `--private-key=.vagrant/machines/vagrant/virtualbox/private_key`, 481 | which is where the private key file for user `vagrant` is generated during 482 | `vagrant up`. 483 | 484 | Additionally, you should never need to use the `--ask-become-pass` flag in 485 | Vagrant, once passwordless sudo has been enabled via the [configure 486 | role](#configure-role). This is done for convenience. 487 | 488 | ### Playbook helper script 489 | 490 | While you may run the `ansible-playbook` command manually, the 491 | [run-playbook.sh](run-playbook.sh) bash script has been provided to facilitate 492 | running `ansible-playbook`. 493 | 494 | ``` 495 | Usage: run-playbook.sh playbook[.yml] inventory [--flag ...] [var=value ...] 496 | 497 | playbook playbook file in deploy/ansible/, the .yml extension is optional. 498 | inventory inventory host file in deploy/ansible/inventory/. 499 | --flag any valid ansible-playbook flags, Eg. --help for help, -vvvv for 500 | connection debugging, --user=REMOTE_USER to specify the remote 501 | user, --ask-become-pass to prompt for a remote password, etc. 502 | var=value any number of ansible extra vars in the format var=value. 503 | ``` 504 | 505 | #### Notes 506 | 507 | * Flags and vars must be specified after both `playbook` and `inventory`. 508 | * All arguments specified after `playbook` and `inventory` not beginning with 509 | `-` or `--` will be treated as extra vars. 510 | * If a non-`vagrant` inventory host is specified, unless the `ubuntu` user is 511 | specified, the `--ask-become-pass` flag will be automatically added to the 512 | command. 513 | * You may pass flags to this scripts as you would to `ansible-playbook`. Eg. 514 | `--help` for help, `-vvvv` for connection debugging, `--user=REMOTE_USER` to 515 | specify the remote user, `--ask-become-pass` to prompt for a remote account 516 | password, etc. 517 | * You may specify any number of extra variables at the end of the command in the 518 | format `foo=12 bar=34` instead of the more verbose default 519 | `--extra-vars="foo=12 bar=34"`. 520 | 521 | #### Examples 522 | 523 | The following command to run the `provision` playbook on the 524 | `production` inventory host with the `--user` and `--private-key` command line 525 | flags: 526 | 527 | * `ansible-playbook deploy/ansible/provision.yml 528 | --inventory=deploy/ansible/inventory/production --user=ubuntu 529 | --private-key=~/keyfile.pem` 530 | 531 | can be run like: 532 | 533 | * `./deploy/run-playbook.sh provision production --user=ubuntu 534 | --private-key=~/keyfile.pem` 535 | 536 | And the following command to run the `deploy` playbook on the `vagrant` 537 | inventory host with the `commit` and `local` extra variables: 538 | 539 | * `ansible-playbook deploy/ansible/deploy.yml 540 | --inventory=deploy/ansible/inventory/vagrant --extra-vars="commit=testing 541 | local=true"` 542 | 543 | can be run like: 544 | 545 | * `./deploy/run-playbook.sh deploy vagrant commit=testing local=true` 546 | 547 | #### More Examples 548 | 549 | * Assume these examples are run from the root directory of your project's Git 550 | repository. 551 | * Don't type in the `$`, that's just there to simulate your shell prompt. 552 | 553 | ```bash 554 | # Provision the production server using the ubuntu user and the ~/keyfile.pem 555 | # private key. Note that while this installs apt packages, it doesn't 556 | # configure the server or deploy the site. 557 | 558 | $ ./deploy/run-playbook.sh provision production --user=ubuntu --private-key=~/keyfile.pem 559 | ``` 560 | 561 | ```bash 562 | # Run just the tasks from the nginx role from the configure playbook on the 563 | # production server. Using tags can save time when only tasks from a certain 564 | # role need to be re-run. 565 | 566 | $ ./deploy/run-playbook.sh configure production --tags=nginx 567 | ``` 568 | 569 | ```bash 570 | # If the current commit at the HEAD of master was previously deployed, this 571 | # won't rebuild it. However, it will still be symlinked and made live, in case 572 | # a different commit was previously made live. If master has changed since it 573 | # was last deployed, and that commit hasn't yet been deployed, it will be 574 | # cloned and built before being symlinked and made live. 575 | 576 | $ ./deploy/run-playbook.sh deploy production 577 | ``` 578 | 579 | ```bash 580 | # Like above, but instead of the HEAD of master, deploy the specified 581 | # branch/tag/sha. 582 | 583 | $ ./deploy/run-playbook.sh deploy production commit=my-feature 584 | $ ./deploy/run-playbook.sh deploy production commit=v1.0.0 585 | $ ./deploy/run-playbook.sh deploy production commit=8f93601a6bc7efeb90b1961d7574b47f61018b6f 586 | ``` 587 | 588 | ```bash 589 | # Regardless of the prior deploy state of commit at the HEAD of the my-feature 590 | # branch, re-clone and rebuild it before symlinking it and making it live. 591 | 592 | $ ./deploy/run-playbook.sh deploy production commit=my-feature force=true 593 | ``` 594 | 595 | ```bash 596 | # Deploy the specified branch to the Vagrant box from the local project Git 597 | # repo instead of the remote Git URL. This way, the specified commit can be 598 | # tested before being pushed to the remote Git repository. 599 | 600 | $ ./deploy/run-playbook.sh deploy vagrant commit=my-feature local=true 601 | ``` 602 | 603 | ```bash 604 | # Link the local project directory into the Vagrant box, allowing local changes 605 | # to be previewed there immediately. This is run automatically at the end of 606 | # "vagrant up". 607 | 608 | $ ./deploy/run-playbook.sh vagrant-link vagrant 609 | ``` 610 | --------------------------------------------------------------------------------