├── 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: '',
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 |
--------------------------------------------------------------------------------