├── .gitignore ├── CNAME ├── LICENSE-MIT ├── bocoup.png ├── deploy ├── README.md ├── ansible │ ├── configure.yml │ ├── deploy.yml │ ├── group_vars │ │ └── all.yml │ ├── host_vars │ │ └── vagrant │ ├── init.yml │ ├── inventory │ │ ├── production │ │ ├── staging │ │ └── vagrant │ ├── provision.yml │ ├── roles │ │ ├── base │ │ │ └── tasks │ │ │ │ └── main.yml │ │ ├── configure │ │ │ └── tasks │ │ │ │ ├── main.yml │ │ │ │ └── swap.yml │ │ ├── deploy │ │ │ ├── tasks │ │ │ │ ├── build.yml │ │ │ │ ├── checkout.yml │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ └── build_info.txt │ │ ├── nginx │ │ │ ├── tasks │ │ │ │ ├── certbot.yml │ │ │ │ ├── main.yml │ │ │ │ └── ssl.yml │ │ │ └── templates │ │ │ │ ├── gzip_params │ │ │ │ ├── site.conf │ │ │ │ └── ssl_params │ │ └── users │ │ │ └── tasks │ │ │ ├── localuser.yml │ │ │ ├── main.yml │ │ │ └── users.yml │ └── vagrant-link.yml └── run-playbook.sh ├── favicon.ico ├── index.html ├── rainbow-header.svg └── site.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Specific to the example project build process 2 | /node_modules 3 | /public 4 | 5 | # Used by the deployment workflow 6 | .vagrant 7 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | deployment-workflow.bocoup.com 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bocoup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bocoup/deployment-workflow/d3b20cd19faf2377f24c2625353d009a2051ccb3/bocoup.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | ######### 2 | # PROJECT 3 | ######### 4 | 5 | # Certain tasks may operate in a less secure (but more convenient) manner, eg. 6 | # enabling passwordless sudo or generating self-signed ssl certs, when testing 7 | # locally, in Vagrant. But not in production! 8 | env: production 9 | 10 | # This var is referenced by a few other vars, eg. git_repo, hostname, site_fqdn. 11 | project_name: deployment-workflow 12 | 13 | # This is what you'll see at the bash prompt if/when you ssh into your server. 14 | hostname: "{{project_name}}" 15 | 16 | # This is the fully qualified domain name of your production server. Because 17 | # nginx checks this value against the URL being requested, it must be the same 18 | # as the server's DNS name. This value is overridden for Vagrant and staging 19 | # servers. 20 | site_fqdn: "{{project_name}}.bocoup.com" 21 | 22 | ############## 23 | # PROVISIONING 24 | ############## 25 | 26 | # Keys to be added to apt. 27 | apt_keys: 28 | - "https://deb.nodesource.com/gpgkey/nodesource.gpg.key" 29 | 30 | # Ppas to be added to apt. Useful ppas (replace trusty with your Ubuntu 31 | # version codename, if necessary): 32 | # Git latest: ppa:git-core/ppa 33 | # Node.js 4.2.x (LTS): deb https://deb.nodesource.com/node_4.x trusty main 34 | # Node.js 5.x.x: deb https://deb.nodesource.com/node_5.x trusty main 35 | apt_ppas: 36 | - "deb https://deb.nodesource.com/node_4.x trusty main" 37 | - "ppa:git-core/ppa" 38 | 39 | # Any apt packages to install. Apt package versions may be specified like 40 | # - git=2.1.0 41 | apt_packages: 42 | - unattended-upgrades 43 | - nginx 44 | - git 45 | - nodejs 46 | 47 | ############ 48 | # WEB SERVER 49 | ############ 50 | 51 | # Should the nginx server use HTTPS instead of HTTP? 52 | ssl: true 53 | 54 | # If ssl is enabled, these cert/key files will be used by nginx. 55 | ssl_cert_path: /etc/ssl/cert.pem 56 | ssl_key_path: /etc/ssl/privkey.pem 57 | 58 | # If ssl is enabled, email address to receive notifications from letsencrypt. 59 | letsencrypt_email: infrastructure@bocoup.com 60 | 61 | # Use a custom parameter for stronger DHE key exchange. 62 | dhe_param_path: /etc/ssl/certs/dhparam.pem 63 | 64 | # The directory that nginx will serve as the production site. This is typically 65 | # where your index.html file exists (or will exist after the build process). 66 | public_path: "{{site_path}}/public" 67 | 68 | # Nginx dir and conf dir. 69 | nginx_dir: /etc/nginx 70 | conf_dir: "{{nginx_dir}}/conf.d" 71 | 72 | # Nginx templated configuration files to create. 73 | shared: 74 | - file: ssl_params 75 | directory: "{{nginx_dir}}" 76 | - file: gzip_params 77 | directory: "{{nginx_dir}}" 78 | confs: 79 | - file: site.conf 80 | directory: "{{conf_dir}}" 81 | 82 | ####### 83 | # USERS 84 | ####### 85 | 86 | # Specified users will be added to the remote server, along with all specified 87 | # public keys. Removing a user from this list does NOT remove them from the 88 | # remote server! You need to set their state to "absent", remove all the other 89 | # properties for that user, and re-run the "configure" playbook. Also, the 90 | # shadow password (that user's sudo password) should be a hash, and NOT plain 91 | # text! 92 | # 93 | # Generate a shadow password hash using the following command: 94 | # openssl passwd -1 -salt $(openssl rand -base64 6) yourpassword 95 | # 96 | users: [] 97 | 98 | # users: 99 | # - name: bob 100 | # state: present 101 | # real_name: Bob Bocoup 102 | # shadow_pass: $xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/ 103 | # public_keys: 104 | # - ssh-rsa xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx bob@bocoup.com 105 | 106 | ######## 107 | # DEPLOY 108 | ######## 109 | 110 | # Parent directory for cloned repository directories. The clone_path and 111 | # site_path should be children of this directory. 112 | base_path: /mnt 113 | 114 | # Temporary location where the Git repo will be cloned and the build scripts 115 | # will be run before going live. 116 | clone_path: "{{base_path}}/temp" 117 | 118 | # Where the production site symlink will exist. 119 | site_path: "{{base_path}}/site" 120 | 121 | # If defined, only this many of the most recent clone directories (including the 122 | # current specified commit) will be retained. Anything older will be removed, 123 | # once the current clone has been made live. 124 | keep_n_most_recent: 3 125 | 126 | # If this variable is defined, a file containing build information, including 127 | # the timestamp, commit, sha, and a few other useful values will be generated 128 | # after the build has completed. This file is relative to your project root. 129 | build_info_path: "public/build.txt" 130 | 131 | # If these variables are uncommented, add swap space to the machine when the 132 | # configure playbook is run. The swap configuration controlled by this is 133 | # meant to address installation problems on machines with minimal ram (e.g. 134 | # npm bails during install because it runs out of memory) 135 | #swap_file_path: /swap 136 | #swap_file_size: 2GB 137 | 138 | ################### 139 | # DEPLOY EXTRA VARS 140 | ################### 141 | 142 | # Specify any valid remote (typically a github user) 143 | remote: bocoup 144 | 145 | # Specify any ref (eg. branch, tag, SHA) to be deployed. This ref must be 146 | # pushed to the remote git_repo before it can be deployed. 147 | commit: master 148 | 149 | # Git repo address. 150 | # For private repositories: git@github.com:{{remote}}/{{project_name}}.git 151 | # For public: https://github.com/{{remote}}/{{project_name}} 152 | git_repo: https://github.com/{{remote}}/{{project_name}} 153 | 154 | # Uncomment this if if you are checking out a private repo 155 | # ansible_ssh_common_args: -o ForwardAgent=yes 156 | 157 | # Clone and build the specified commit SHA, regardless of prior build status. 158 | force: false 159 | 160 | # Use the local project Git repo instead of the remote git_repo. This option 161 | # only works with the vagrant inventory, and not with staging or production. 162 | local: false 163 | -------------------------------------------------------------------------------- /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/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/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-v2.bocoup.com 5 | -------------------------------------------------------------------------------- /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/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/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/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/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: 6 | name: '{{hostname}}' 7 | use: systemd 8 | 9 | - name: ensure unattended upgrades are running 10 | copy: 11 | content: | 12 | APT::Periodic::Update-Package-Lists "1"; 13 | APT::Periodic::Download-Upgradeable-Packages "1"; 14 | APT::Periodic::AutocleanInterval "7"; 15 | APT::Periodic::Unattended-Upgrade "1"; 16 | dest: /etc/apt/apt.conf.d/10periodic 17 | 18 | - name: add loopback references to our domain in /etc/hosts 19 | lineinfile: 20 | dest: /etc/hosts 21 | state: present 22 | line: "127.0.0.1 {{hostname}} {{site_fqdn}}" 23 | 24 | - name: disallow password authentication 25 | lineinfile: 26 | dest: /etc/ssh/sshd_config 27 | state: present 28 | regexp: "^PasswordAuthentication" 29 | line: "PasswordAuthentication no" 30 | notify: restart sshd 31 | 32 | - name: disallow challenge response authentication 33 | lineinfile: 34 | dest: /etc/ssh/sshd_config 35 | state: present 36 | regexp: "^ChallengeResponseAuthentication" 37 | line: "ChallengeResponseAuthentication no" 38 | notify: restart sshd 39 | 40 | - name: ensure github.com is a known host 41 | lineinfile: 42 | dest: /etc/ssh/ssh_known_hosts 43 | state: present 44 | create: yes 45 | regexp: "^github\\.com" 46 | line: "{{ lookup('pipe', 'ssh-keyscan -t rsa github.com') }}" 47 | 48 | - name: ensure ssh agent socket environment variable persists when sudoing 49 | lineinfile: 50 | dest: /etc/sudoers 51 | state: present 52 | insertafter: "^Defaults" 53 | line: "Defaults\tenv_keep += \"SSH_AUTH_SOCK\"" 54 | validate: "visudo -cf %s" 55 | 56 | - name: allow passwordless sudo - development only! 57 | lineinfile: 58 | dest: /etc/sudoers 59 | state: present 60 | regexp: "^%sudo" 61 | line: "%sudo\tALL=(ALL:ALL) NOPASSWD:ALL" 62 | validate: "visudo -cf %s" 63 | when: "{{ env == 'development' }}" 64 | 65 | - include: swap.yml 66 | when: swap_file_path is defined and swap_file_size is defined 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/deploy/templates/build_info.txt: -------------------------------------------------------------------------------- 1 | date: {{ansible_date_time.iso8601}} 2 | sha: {{sha.stdout}} 3 | env: {{env}} 4 | -------------------------------------------------------------------------------- /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/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 is 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 is succeeded 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 is failed 54 | 55 | - fail: msg="nginx config is invalid" 56 | when: nginx_test_valid is failed 57 | 58 | - include: certbot.yml 59 | when: ssl and inventory_hostname != 'vagrant' 60 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/run-playbook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bin=ansible-playbook 4 | 5 | function usage() { 6 | cat < 2 | 3 | 4 | 5 | 6 | Modern Web Deployment Workflow 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 24 | 25 |
26 |

Modern Web Deployment Workflow

27 |
A legacy technical operations tool built at Bocoup
28 | 29 |

This collection of Ansible playbooks have been designed to simplify deployment 30 | of a modern website or web app using Vagrant, Ubuntu, nginx and HTTP/HTTPS. Many 31 | tasks have been separated into separate roles, and as much configuration as 32 | possible has been abstracted into external variables.

33 |

Deprecation Warning This project is no longer 34 | maintained. Bocoup discourages new projects from adopting these patterns and 35 | offers the original documentation (below) strictly as a resource for legacy 36 | projects.

37 |

High-level benefits include:

38 |
    39 |
  • A new server can be up and running with fully deployed code in just a few 40 | minutes.
  • 41 |
  • An update to an existing project can be deployed and built in under a minute.
  • 42 |
  • A project can be rolled back to a previously-deployed version in a matter of 43 | seconds.
  • 44 |
  • Updates to server configuration can be made in a matter of seconds.
  • 45 |
  • Most server configuration and code updates can be made with zero server 46 | downtime.
  • 47 |
  • Code can be tested locally in Vagrant before being deployed to a production 48 | server.
  • 49 |
  • Code can be tested on a staging server for QA or final testing before being 50 | deployed to a production server.
  • 51 |
  • Server configuration and project deployment can be made to scale to any number 52 | of remote hosts.
  • 53 |
54 |

More specific benefits include:

55 |
    56 |
  • Almost all server configuration and project deployment information is stored 57 | in the project, making it easy to destroy and re-create servers with 58 | confidence.
  • 59 |
  • All project maintainer user account information is stored in the project, 60 | making it easy to add or remove project maintainers.
  • 61 |
  • SSH agent forwarding allows the remote server to access private GitHub repos 62 | without requiring a private key to be copied to the server or for dedicated 63 | deployment keys to be configured.
  • 64 |
  • While working locally, the Vagrant box can easily be toggled between 65 | development and deployment modes at any time. This allows local changes to be 66 | previewed instantly (development) or a specific commit to be built as it would 67 | be in production (deployment).
  • 68 |
  • SSL certs can be auto-generated for testing HTTPS in development.
  • 69 |
  • Because the entire deployment workflow is comprised of Ansible playbooks and a 70 | Vagrantfile, it can be easily modified to meet any project's needs.
  • 71 |
72 |

Here are links to the official, original project home page, documentation, Git 73 | repository and wiki:

74 | 79 |

Notes:

80 |
    81 |
  • Even though Node.js and npm are used in this sample project, with minor 82 | edits this workflow can be made to work with basically any programming 83 | language, package manager, etc.
  • 84 |
  • This workflow won't teach you how to create an AWS instance. Fortunately, 85 | there are already excellent guides for creating a key 86 | pair, 87 | setting up a security 88 | group 89 | and launching an 90 | instance.
  • 91 |
  • This workflow has been thoroughly tested in Ubuntu 14.04 LTS 92 | (trusty). More specifically, with the 93 | ubuntu/trusty64 Vagrant 94 | image and with the AWS EC2 Ubuntu Server 14.04 LTS AMI. Minor adjustments 95 | might need to be made for other providers, while more substantial changes 96 | might need to be made for other Ubuntu versions or Linux distributions.
  • 97 |
  • While this workflow has been designed to meet the needs of a typical use 98 | case, it might not meet your project's needs exactly. Consider this to be 99 | a starting point; you're encouraged to edit the included playbooks and roles!
  • 100 |
101 |

Overview

102 |

Assuming you've already created (or are in the process of creating) a website or 103 | web app, you will typically perform these steps when using this workflow.

104 |
    105 |
  1. Ensure the workflow dependencies have been installed on your 106 | development machine.
  2. 107 |
  3. Add and commit the workflow files into your project.
  4. 108 |
  5. Modify the Ansible variables, playbooks and roles to meet your 109 | specific project needs.
  6. 110 |
  7. Test your project on the local Vagrant box while authoring 111 | it. (Optional, but recommended)
  8. 112 |
  9. Deploy your project to a staging server for QA and final 113 | testing. (Optional, but recommended)
  10. 114 |
  11. Deploy your project to a production server.
  12. 115 |
116 |

Step 1 is usually only done per development machine, steps 2-3 are usually only 117 | done per project, and steps 4-6 will be repeated throughout the life of your 118 | project as you make and test changes and deploy new versions of your website or 119 | web app.

120 |

Dependencies

121 |

The following will need to be installed on your local development machine before 122 | you can use this workflow. All versions should be the latest available unless 123 | otherwise specified.

124 | 147 |

Notes:

148 |
    149 |
  • Ansible doesn't really work in Windows. But it works great in OS X and Linux, 150 | so be sure to use one of those operating systems for development.
  • 151 |
152 |

Initial Setup

153 |

Copy this project's files so that the deploy directory is in the root of 154 | your project Git repository. Be sure to copy recursively and preserve file 155 | modes, eg. executable, so that the bash helper script 156 | continues to work. The Vagrantfile and 157 | ansible.cfg file should be placed in your project root 158 | directory, not the deploy directory.

159 |

Also, be sure to add .vagrant to your project's .gitignore file so that 160 | directory's contents, which are auto-generated by Vagrant, aren't committed with 161 | your project's source.

162 |

Ansible

163 |

At the core of this workflow is Ansible, an IT automation tool. Ansible aims to 164 | be simple to configure and easy to use, while being secure and reliable. In this 165 | workflow, Ansible is used to configure systems and deploy software.

166 |

Ansible Configuration

167 |

Ansible Variables

168 |

Much of this workflow's behavior can be configured via Ansible variables.

169 | 173 |

Host-specific settings may be defined in host-named files in the 174 | host_vars directory and will override global values.

175 | 179 |

Variables may be overridden when a playbook is run via the --extra-vars 180 | command line option. These variables are noted in the preceding files as EXTRA 181 | VARS.

182 |

See the Ansible variables 183 | documentation for more information on variables, variable precedence, and how 184 | {{ }} templates and filters work.

185 |

Ansible Inventory

186 |

These files contain the addresses of any servers to which this project will be 187 | deployed. Usually, these addresses will be fully qualified domain 188 | names, but they may 189 | also be IPs. Each inventory file may contain a list of multiple server FQDNs or 190 | IPs, allowing a playbook to be deployed to multiple servers simultaneously, but 191 | for this workflow, each inventory file will list a single server.

192 | 197 |

Like with host variables, settings defined here will 198 | override those defined in the global variables and group 199 | variables files. For example, in the staging inventory, the 200 | site_fqdn variable can be set to the staging server's FQDN, allowing nginx to 201 | respond to requests made to its FQDN instead of the production server's FQDN.

202 |

Unless the variable is a server name-specific override like site_fqdn or 203 | ansible_ssh_host, it should probably be defined in host 204 | variables.

205 |

Ansible Playbooks

206 |

Ansible playbooks are human-readable documents that describe and configure the 207 | tasks that Ansible will run on a remote server. They should be idempotent, 208 | allowing them to be run multiple times with the same result each time.

209 |

The following playbooks are included in this workflow:

210 | 217 |

For more detailed information on what each playbook actually does and how it 218 | will need to be configured, be sure to check out the description for each 219 | role that playbook includes.

220 |

provision playbook

221 |

Provision server. This playbook must be run when a server is first created 222 | and is typically only run once. It may be run again if you make server-level 223 | changes or need to update any installed apt modules to their latest versions. 224 | If you were creating a new AMI or base box, you'd do so after running only 225 | this playbook.

226 | 230 |

configure playbook

231 |

Configure server. This playbook is run after a server is provisioned but 232 | before a project is deployed, to configure the system, add user accounts, 233 | and setup long-running processes like nginx, postgres, etc.

234 | 238 |

deploy playbook

239 |

Clone, build, and deploy, restarting nginx if necessary. This playbook must 240 | be run after provision and configure, and is used to deploy and build the 241 | specified commit (overridable via extra vars) on the server. Running this 242 | playbook in Vagrant will override the vagrant-link playbook, and vice-versa.

243 | 247 | 248 |

Instead of cloning the Git repo and building like the deploy playbook, this 249 | playbook links your local working project directory into the Vagrant box so that 250 | you can instantly preview your local changes on the server, for convenience 251 | while developing. While in this mode, all building will have to be done 252 | manually, at the command line of your development machine. Running this playbook 253 | will override the deploy playbook, and vice-versa.

254 | 257 |

init playbook

258 |

This playbook saves the trouble of running the provision, configure and 259 | vagrant-link playbooks individually, and is provided for convenience. After 260 | vagrant up, this playbook will be run on the new Vagrant box.

261 | 264 |

Ansible Roles

265 |

There are multiple ways to organize playbooks, and while it's possible to put 266 | all your tasks into a single playbook, it's often beneficial to separate related 267 | tasks into "roles" that can be included in one or more playbooks, for easy reuse 268 | and organization.

269 |

The following roles are used by this workflow's playbooks:

270 | 277 |

base role

278 |

Get the box up and running. These tasks run before the box is configured 279 | or the project is cloned or built. All system dependencies should be 280 | installed here.

281 |

Apt keys, apt ppas, apt packages and global npm modules can be configured in the 282 | PROVISIONING section of the global variables file. If 283 | you need custom packages or modules to be installed, specify them there.

284 |

Don't be afraid to modify these tasks. For example, if your project doesn't use 285 | npm, just remove the npm tasks.

286 |

This role contains the following files and tasks:

287 | 298 |

(Browse the deploy/ansible/roles/base directory for more information)

299 |

configure role

300 |

Configure the box. This happens after the base initialization, but before 301 | the project is cloned or built.

302 |

This role contains the following files and tasks:

303 | 326 |

(Browse the deploy/ansible/roles/configure directory for more information)

327 |

nginx role

328 |

Generate nginx config files (and ssl configuration, if ssl was specified), 329 | rolling back changes if any part of the config is invalid.

330 |

The public site path, ssl and ssl cert/key file locations can be configured in 331 | the WEB SERVER section of the global variables file. If 332 | you want to override any settings for just Vagrant, you can do so in host 333 | variables.

334 |

By default, nginx is configured to serve a website with a custom 404 page. 335 | However, if you want to redirect all requests to index.html (eg. for a web 336 | app), you should modify the site.conf 337 | template per the inline comments. For more involved customization, read the 338 | nginx documentation.

339 |

If you enable SSL for production or staging, you will need to supply your 340 | own signed SSL cert/key files and put them on the remote server via AWS 341 | CloudFormation, 342 | cloud-init, or you can copy them 343 | manually 344 | before provisioning, or this role will fail.

345 |

If you choose to enable SSL for vagrant, self-signed SSL cert/key files will 346 | be generated for you automatically if they don't already exist. In this case, 347 | your website or web app will work, but you will have to click past a SSL 348 | certificate warning before viewing it.

349 |

This role contains the following files and tasks:

350 | 379 |

And the following templates:

380 | 385 |

(Browse the deploy/ansible/roles/nginx directory for more information)

386 |

users role

387 |

In development (localuser.yml), 388 | create an account for the currently logged-in user, and copy their public key to 389 | the server. This makes it possible to run other playbooks without specifying a 390 | user or private key on the command line.

391 |

In production (users.yml), ensure all 392 | users have been added, along with any public keys. If any user's state is 393 | absent, they will be removed. If any keys are removed, they will be deleted. 394 | In a development environment, make sudo passwordless, for convenience.

395 |

User accounts, passwords and public keys can be configured in the USERS 396 | section of the global variables file.

397 |

This role contains the following files and tasks:

398 | 411 |

(Browse the deploy/ansible/roles/users directory for more information)

412 |

deploy role

413 |

Clone the repo and check out the specified commit unless it has already been 414 | checked out. When done, symlink the specified commit to make it go live, and 415 | remove old clones to free up disk space.

416 |

The Git repo URL, deployment paths, number of recent deployments to retain and 417 | build info file path can be configured in the DEPLOY section of the 418 | global variables file.

419 |

The following variables defined in the DEPLOY EXTRA VARS section of the same 420 | file may be overridden on the ansible-playbook command line in the format 421 | --extra-vars="commit=mybranch force=true".

422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 |
vardefaultdescription
remotebocoupSpecify any remote (typically a github user).
commitmasterSpecify any ref (eg. branch, tag, SHA) to be deployed. This ref must be pushed to the remote git_repo before it can be deployed.
forcefalseClone and build the specified commit SHA, regardless of prior build status.
localfalseUse 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.
453 |

The build.yml file contains all the 454 | build tasks that need to be run after your project is cloned, eg. npm install, 455 | npm run build. Don't be afraid to modify these tasks. Your project's build 456 | process might need to be different than what's here, so adjust accordingly!

457 |

This role contains the following files and tasks:

458 | 485 |

And the following templates:

486 | 489 |

(Browse the deploy/ansible/roles/deploy directory for more information)

490 |

Vagrant

491 |

Vagrant allows you to isolate project dependencies (like nginx or postgres) in a 492 | stable, disposable, consistent work environment. In conjunction with Ansible and 493 | VirtualBox, Vagrant ensures that anyone on your team has access to their own 494 | private, pre-configured development server whenever they need it.

495 |

If you only want to deploy to remote production or staging servers, you can just 496 | install Ansible and skip VirtualBox and Vagrant, which are only used to create 497 | the local development server.

498 |

Configuring Vagrant

499 |

The Vagrantfile file at the root of the repo contains the 500 | project's Vagrant configuration. Be sure to specify an appropriate hostname 501 | alias for the Vagrant box here.

502 |

Using Vagrant

503 |

Once the Vagrantfile and Ansible variables, playbooks 504 | and roles have been customized to meet your project's 505 | needs, you should be able to run vagrant up to create a local, 506 | fully-provisioned Vagrant server that is accessible in-browser via the hostname 507 | alias specified in the Vagrantfile. If you're asked for your administrator 508 | password during this process, it's so that the hostsupdater plugin can modify 509 | your /etc/hosts file.

510 |

If you change the Ansible configuration, running vagrant provision will re-run 511 | the Ansible playbooks. If you make drastic changes to the Ansible configuration 512 | and need to recreate the Vagrant server (which is often the case), you can 513 | delete it with vagrant destroy. If you do this, be sure to let collaborators 514 | know too!

515 |

See the Vagrant Ansible 516 | provisioner 517 | documentation for more information.

518 |

Using SSH with Vagrant

519 |

Vagrant provides the vagrant ssh command which allows you to connect to the 520 | Vagrant box via its built-in vagrant user. While this is convenient for some 521 | basic development tasks, once provisioned, you should connect to the Vagrant box 522 | using the user account created by the users role. This will 523 | ensure that the ansible-playbook command, which uses ssh 524 | internally, will work, allowing you to deploy.

525 |

To connect to Vagrant in this way, use the ssh command along with the 526 | hostname alias defined in the Vagrantfile. Eg, for this 527 | example project, the command would be ssh deployment-workflow.loc.

528 |

Also, adding a section like 529 | this 530 | to your ~/.ssh/config file will prevent SSH from storing Vagrant box keys in 531 | ~/.ssh/known_hosts and complaining about them not matching when a Vagrant 532 | box is destroyed and recreated. Do not do this for production servers. This is 533 | only safe for private, local virtual machines!

534 |

Deploying

535 |

Once you've customized Ansible variables, playbooks and 536 | roles and committed your changes to the Git repository 537 | configured in global variables, you may run the 538 | ansible-playbook command or the included playbook helper 539 | script to run any playbook on 540 | any inventory host.

541 |

Command Line Flags

542 |

Note that the following flags apply to both ansible-playbook and the included 543 | playbook helper script.

544 |
    545 |
  • --help - Display usage information and all available options; the list 546 | here contains only the most relevant subset of all options.
  • 547 |
  • --user - Connect to the server with the specified user. If a user isn't 548 | specified, the currently logged-in user's username will be used.
  • 549 |
  • --ask-become-pass - If the remote user account requires a password to be 550 | entered, you will need to specify this option.
  • 551 |
  • --private-key - If the remote user account requires a private key, you 552 | will need to specify this option.
  • 553 |
  • --extra-vars - Values that override those stored in the ansible 554 | configuration in the format 555 | --extra-vars="commit=mybranch force=true".
  • 556 |
  • -vvvv - Display verbose connection debugging information.
  • 557 |
558 |

Production and Staging Notes

559 |

Once the users role has run successfully, assuming your user 560 | account has been correctly added to it, you should be able to omit the --user 561 | and --private-key command line flags. However, until the users role has run at 562 | least once:

563 |
    564 |
  • the --user flag will need to be specified. For the default AWS EC2 Ubuntu 565 | AMI, use --user=ubuntu.
  • 566 |
  • the --private-key flag will need to be specified. For AWS, specify 567 | --private-key=/path/to/keyfile.pem where keyfile.pem is the file 568 | downloaded when creating a new key 569 | pair 570 | in AWS. Do not store this private key in your project Git repo!
  • 571 |
572 |

The default AWS ubuntu user doesn't require a password for sudo, but user 573 | accounts added via the users role do, so be sure to specify the 574 | --ask-become-pass flag when you omit the --user command line flag.

575 |

Vagrant Notes

576 |

Once the users role has run successfully, assuming your user 577 | account has been correctly added to it, you should be able to omit the --user 578 | and --private-key command line flags. However, until the users role has run at 579 | least once:

580 |
    581 |
  • specify --user=vagrant, which is the default user created for the Vagrant 582 | box.
  • 583 |
  • specify --private-key=.vagrant/machines/vagrant/virtualbox/private_key, 584 | which is where the private key file for user vagrant is generated during 585 | vagrant up.
  • 586 |
587 |

Additionally, you should never need to use the --ask-become-pass flag in 588 | Vagrant, once passwordless sudo has been enabled via the configure 589 | role. This is done for convenience.

590 |

Playbook helper script

591 |

While you may run the ansible-playbook command manually, the 592 | deploy/run-playbook.sh bash script has been provided to facilitate 593 | running ansible-playbook.

594 |
Usage: run-playbook.sh playbook[.yml] inventory [--flag ...] [var=value ...]
595 | 
596 |    playbook  playbook file in deploy/ansible/, the .yml extension is optional.
597 |   inventory  inventory host file in deploy/ansible/inventory/.
598 |      --flag  any valid ansible-playbook flags, Eg. --help for help, -vvvv for
599 |              connection debugging, --user=REMOTE_USER to specify the remote
600 |              user, --ask-become-pass to prompt for a remote password, etc.
601 |   var=value  any number of ansible extra vars in the format var=value.
602 | 

Notes

603 |
    604 |
  • Flags and vars must be specified after both playbook and inventory.
  • 605 |
  • All arguments specified after playbook and inventory not beginning with 606 | - or -- will be treated as extra vars.
  • 607 |
  • If a non-vagrant inventory host is specified, unless the ubuntu user is 608 | specified, the --ask-become-pass flag will be automatically added to the 609 | command.
  • 610 |
  • You may pass flags to this scripts as you would to ansible-playbook. Eg. 611 | --help for help, -vvvv for connection debugging, --user=REMOTE_USER to 612 | specify the remote user, --ask-become-pass to prompt for a remote account 613 | password, etc.
  • 614 |
  • You may specify any number of extra variables at the end of the command in the 615 | format foo=12 bar=34 instead of the more verbose default 616 | --extra-vars="foo=12 bar=34".
  • 617 |
618 |

Examples

619 |

The following command to run the provision playbook on the 620 | production inventory host with the --user and --private-key command line 621 | flags:

622 |
    623 |
  • ansible-playbook deploy/ansible/provision.yml 624 | --inventory=deploy/ansible/inventory/production --user=ubuntu 625 | --private-key=~/keyfile.pem
  • 626 |
627 |

can be run like:

628 |
    629 |
  • ./deploy/run-playbook.sh provision production --user=ubuntu 630 | --private-key=~/keyfile.pem
  • 631 |
632 |

And the following command to run the deploy playbook on the vagrant 633 | inventory host with the commit and local extra variables:

634 |
    635 |
  • ansible-playbook deploy/ansible/deploy.yml 636 | --inventory=deploy/ansible/inventory/vagrant --extra-vars="commit=testing 637 | local=true"
  • 638 |
639 |

can be run like:

640 |
    641 |
  • ./deploy/run-playbook.sh deploy vagrant commit=testing local=true
  • 642 |
643 |

More Examples

644 |
    645 |
  • Assume these examples are run from the root directory of your project's Git 646 | repository.
  • 647 |
  • Don't type in the $, that's just there to simulate your shell prompt.
  • 648 |
649 |
# Provision the production server using the ubuntu user and the ~/keyfile.pem
650 | # private key. Note that while this installs apt packages, it doesn't
651 | # configure the server or deploy the site.
652 | 
653 | $ ./deploy/run-playbook.sh provision production --user=ubuntu --private-key=~/keyfile.pem
654 | 
655 |
# Run just the tasks from the nginx role from the configure playbook on the
656 | # production server. Using tags can save time when only tasks from a certain
657 | # role need to be re-run.
658 | 
659 | $ ./deploy/run-playbook.sh configure production --tags=nginx
660 | 
661 |
# If the current commit at the HEAD of master was previously deployed, this
662 | # won't rebuild it. However, it will still be symlinked and made live, in case
663 | # a different commit was previously made live. If master has changed since it
664 | # was last deployed, and that commit hasn't yet been deployed, it will be
665 | # cloned and built before being symlinked and made live.
666 | 
667 | $ ./deploy/run-playbook.sh deploy production
668 | 
669 |
# Like above, but instead of the HEAD of master, deploy the specified
670 | # branch/tag/sha.
671 | 
672 | $ ./deploy/run-playbook.sh deploy production commit=my-feature
673 | $ ./deploy/run-playbook.sh deploy production commit=v1.0.0
674 | $ ./deploy/run-playbook.sh deploy production commit=8f93601a6bc7efeb90b1961d7574b47f61018b6f
675 | 
676 |
# Regardless of the prior deploy state of commit at the HEAD of the my-feature
677 | # branch, re-clone and rebuild it before symlinking it and making it live.
678 | 
679 | $ ./deploy/run-playbook.sh deploy production commit=my-feature force=true
680 | 
681 |
# Deploy the specified branch to the Vagrant box from the local project Git
682 | # repo instead of the remote Git URL. This way, the specified commit can be
683 | # tested before being pushed to the remote Git repository.
684 | 
685 | $ ./deploy/run-playbook.sh deploy vagrant commit=my-feature local=true
686 | 
687 |
# Link the local project directory into the Vagrant box, allowing local changes
688 | # to be previewed there immediately. This is run automatically at the end of
689 | # "vagrant up".
690 | 
691 | $ ./deploy/run-playbook.sh vagrant-link vagrant
692 | 
693 | 694 |
695 |
696 | 697 |
698 | 707 | 708 | 709 | 710 | -------------------------------------------------------------------------------- /rainbow-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /site.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | ::selection { 7 | text-shadow: none; 8 | background: rgba(123,194,67,.25); 9 | } 10 | 11 | html, body { 12 | margin: 0; 13 | padding: 0; 14 | background: #f1f1f1; 15 | } 16 | 17 | html { 18 | color: #3b3b3b; 19 | font-size: 1em; 20 | line-height: 1.4; 21 | } 22 | 23 | body { 24 | margin: 0; 25 | border-top: solid transparent; 26 | border-width: 10px 0 10px 0; 27 | border-style: solid; 28 | -webkit-border-image: url(rainbow-header.svg) 4 1 4 1 stretch; 29 | -moz-border-image: url(rainbow-header.svg) 4 1 4 1 stretch; 30 | -o-border-image: url(rainbow-header.svg) 4 1 4 1 stretch; 31 | border-image: url(rainbow-header.svg) 4 1 4 1 stretch; 32 | min-height: 100vh; 33 | } 34 | 35 | body:after { 36 | content: " "; 37 | display: table; 38 | clear: both; 39 | } 40 | 41 | .markdown-body { 42 | background: #fff; 43 | min-height: 100%; 44 | margin: 0 auto; 45 | } 46 | 47 | .markdown-body > h1:first-child { 48 | margin-top: 0.8em !important; 49 | } 50 | 51 | .col-fullwidth { 52 | max-width: 1200px; 53 | margin: 0 auto; 54 | } 55 | 56 | a { 57 | color: #d71818; 58 | } 59 | 60 | a:hover { 61 | color: #ea4142; 62 | text-decoration: none; 63 | } 64 | 65 | a:active { 66 | position: relative; 67 | bottom: -1px; 68 | } 69 | 70 | h1, h2, h3, h4, .nav ul a, .by-bocoup { 71 | font-family: "Open Sans"; 72 | } 73 | 74 | h2 { 75 | font-size: 1.5em; 76 | line-height: 1.2; 77 | margin: .25em 0 0; 78 | border-bottom: 1px solid #D6D6D6; 79 | padding-bottom: .5em; 80 | } 81 | 82 | #mc_embed_signup input.btn:hover { 83 | color: #ea4142; 84 | } 85 | 86 | .by-bocoup { 87 | margin-bottom: 2em; 88 | font-size: 1.1em; 89 | } 90 | 91 | .nav ul { 92 | list-style: none; 93 | padding: 0; 94 | margin: 0; 95 | } 96 | 97 | .nav ul li .nav1 { 98 | border-bottom: 1px solid #D6D6D6; 99 | } 100 | 101 | .nav ul a.nav1 { 102 | text-decoration: none; 103 | display: block; 104 | padding: 1em .7em; 105 | color: #3b3b3b; 106 | font-size: 1.1em; 107 | font-weight: 800; 108 | } 109 | 110 | .nav ul ul { 111 | display: none 112 | } 113 | 114 | .nav ul a:hover { 115 | color: #d71818; 116 | } 117 | 118 | .nav ul .nav1 span { 119 | color: #D6D6D6; 120 | } 121 | 122 | .nav ul .nav1:hover span { 123 | color: #d71818; 124 | } 125 | 126 | .nav ul .nav1 span::before { 127 | border-style: solid; 128 | border-width: 0.20em 0.20em 0 0; 129 | content: ''; 130 | height: 0.35em; 131 | position: relative; 132 | top: 0.45em; 133 | transform: rotate(-45deg); 134 | width: 0.35em; 135 | float: right; 136 | } 137 | 138 | .nav ul .nav1 span:before { 139 | transform: rotate(45deg); 140 | } 141 | 142 | .nav p { 143 | font-size: .8em; 144 | margin: 1.5em 1em; 145 | line-height: 1.5; 146 | } 147 | 148 | .by-bocoup img { 149 | vertical-align: bottom; 150 | margin-left: .2em; 151 | } 152 | 153 | /* General Post Content */ 154 | 155 | .post-body, .nav p { 156 | font-family: Georgia,serif; 157 | } 158 | 159 | .post .caption, 160 | .post small { 161 | font-style: italic; 162 | color: #969696; 163 | } 164 | 165 | .post-body p { 166 | font-size: 1.05em; 167 | margin: 1.1em 0; 168 | line-height: 1.5; 169 | width: 100%; 170 | } 171 | 172 | .post-body ul { 173 | list-style: none; 174 | padding: .4em 2.4em; 175 | } 176 | 177 | .post-body li { 178 | padding: 0 0 0.8em; 179 | line-height: 1.4em; 180 | position: relative; 181 | } 182 | 183 | .post-body li:before { 184 | color: #ea4142; 185 | font-size: 1em; 186 | font-style: normal; 187 | display: inline-block; 188 | margin-right: .5em; 189 | position: absolute; 190 | left: -1.25em; 191 | width: .75em; 192 | } 193 | 194 | .post-body ul, .post-body ol { 195 | padding: 0 0 0 2.4em; 196 | margin: 0.8em 0 0 0; 197 | } 198 | 199 | .post-body li li:last-child { 200 | padding-bottom: 0; 201 | } 202 | 203 | .post-body li p:first-of-type { 204 | margin-top: 0; 205 | } 206 | 207 | .post-body ul li:before, 208 | .post-body ol ul li:before { 209 | content: "•"; 210 | display: inline-block; 211 | font-size: 1.2em; 212 | } 213 | 214 | .post-body ol { 215 | counter-reset: ol; 216 | list-style: none; 217 | } 218 | 219 | .post-body ol li:before, 220 | .post-body ul ol li:before { 221 | counter-increment: ol; 222 | content: counter(ol) ". "; 223 | font-style: italic; 224 | font-size: 14px; 225 | } 226 | 227 | .post-body ul.checklist li:before, 228 | .post-body ol.checklist li:before { 229 | content: "\2713"; 230 | } 231 | 232 | .post-body pre { 233 | background: #f1f1f1; 234 | line-height: 1.6; 235 | position: relative; 236 | overflow: auto; 237 | padding: 1em 4.1em 1em 1.5em; 238 | } 239 | 240 | .post-body pre code { 241 | padding: 0; 242 | } 243 | 244 | code { 245 | background: #f1f1f1; 246 | font-size: 1.1em; 247 | border-radius: 3px; 248 | padding: 0 .35em; 249 | } 250 | 251 | @media( min-width: 50em ) { 252 | .nav { 253 | float: left; 254 | width: 25%; 255 | } 256 | .nav + .post-body { 257 | float: right; 258 | width: 70%; 259 | } 260 | .nav ul ul { 261 | display: block; 262 | background: #F7F7F7; 263 | padding: .3em 0; 264 | } 265 | 266 | .nav ul ul a { 267 | text-decoration: none; 268 | display: block; 269 | padding: .5em; 270 | color: #3b3b3b; 271 | font-size: 1em; 272 | padding-left: .9em; 273 | } 274 | } 275 | 276 | 277 | /* Column Styles */ 278 | 279 | .col-condensed { 280 | clear: both; 281 | padding: 7.4074074%; 282 | } 283 | 284 | .col-condensed:after { 285 | content: ""; 286 | clear: both; 287 | display: table; 288 | } 289 | 290 | .col-inset { padding: 1em 3.7037037%; } 291 | 292 | @media( min-width: 35em ) { 293 | .col-condensed { padding: 6.55555555% 5% } 294 | } 295 | 296 | /* Page Footer */ 297 | 298 | #footer { 299 | text-align: center; 300 | margin-top: 3em; 301 | max-width: 1200px; 302 | padding-top: 7%; 303 | padding-bottom: 0; 304 | border-top: 7px solid #f1f1f1; 305 | } 306 | 307 | /* Anchor */ 308 | @font-face { 309 | font-family: octicons-anchor; 310 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==) format('woff'); 311 | } 312 | 313 | .markdown-body .octicon { 314 | font: normal normal normal 16px/1 octicons-anchor; 315 | display: inline-block; 316 | text-decoration: none; 317 | text-rendering: auto; 318 | -webkit-font-smoothing: antialiased; 319 | -moz-osx-font-smoothing: grayscale; 320 | -webkit-user-select: none; 321 | -moz-user-select: none; 322 | -ms-user-select: none; 323 | user-select: none; 324 | } 325 | 326 | .markdown-body .octicon-link:before { 327 | content: '\f05c'; 328 | } 329 | 330 | .markdown-body>*:first-child { 331 | margin-top: 0 !important; 332 | } 333 | 334 | .markdown-body>*:last-child { 335 | margin-bottom: 0 !important; 336 | } 337 | 338 | .markdown-body a:not([href]) { 339 | color: inherit; 340 | text-decoration: none; 341 | } 342 | 343 | .markdown-body .anchor { 344 | position: absolute; 345 | top: 0; 346 | left: 0; 347 | display: block; 348 | padding-right: 6px; 349 | padding-left: 30px; 350 | margin-left: -30px; 351 | } 352 | 353 | .markdown-body .anchor:focus { 354 | outline: none; 355 | } 356 | 357 | .markdown-body h1, 358 | .markdown-body h2, 359 | .markdown-body h3, 360 | .markdown-body h4 { 361 | position: relative; 362 | margin-top: 2em; 363 | margin-bottom: 16px; 364 | font-weight: bold; 365 | line-height: 1.4; 366 | } 367 | 368 | .markdown-body h1 { 369 | margin-top: 0; 370 | font-size: 2.2em; 371 | } 372 | 373 | .markdown-body h1 .octicon-link, 374 | .markdown-body h2 .octicon-link, 375 | .markdown-body h3 .octicon-link, 376 | .markdown-body h4 .octicon-link { 377 | display: none; 378 | color: #000; 379 | vertical-align: middle; 380 | } 381 | 382 | .markdown-body h1 .anchor, 383 | .markdown-body h2 .anchor, 384 | .markdown-body h3 .anchor, 385 | .markdown-body h4 .anchor { 386 | line-height: 1.2em; 387 | } 388 | 389 | .markdown-body h1:hover .anchor, 390 | .markdown-body h2:hover .anchor, 391 | .markdown-body h3:hover .anchor, 392 | .markdown-body h4:hover .anchor { 393 | padding-left: 8px; 394 | margin-left: -30px; 395 | text-decoration: none; 396 | } 397 | 398 | .markdown-body h1:hover .anchor .octicon-link, 399 | .markdown-body h2:hover .anchor .octicon-link, 400 | .markdown-body h3:hover .anchor .octicon-link, 401 | .markdown-body h4:hover .anchor .octicon-link { 402 | display: inline-block; 403 | } 404 | 405 | @media( max-width: 50em ) { 406 | .nav { 407 | margin-bottom: 2em; 408 | } 409 | } 410 | 411 | .warning { 412 | border-width: 1px; 413 | padding: 1em; 414 | 415 | background-color: #ffdddd; 416 | border-color: #ff0000; 417 | border-style: solid; 418 | } 419 | --------------------------------------------------------------------------------