Getting MEAN with Ansible

@RossKukulinski
SpeakIt.io Co-Founder

BayNode Talk Night
July 24, 2014

Warmup: Raise your hand if...

  • You've never heard of the MEAN stack
    (hint: MongoDB, ExpressJS, AngularJS, NodeJS)
  • You've never heard of Ansible
  • You've never heard of configuration management tools
    (i.e. Salt, Puppet, Chef, Ansible)

Deploying Node

Platform as a Service

$ git push heroku master
$ jitsu deploy

PaaS Benefits

  • "Cheap"
  • Simple deploy
  • Integrations with DB PaaS (MongoHQ, Redis To Go, etc.)
  • Nodejitsu / Heroku handle scaling your application

PaaS Downsides

  • performance limitations
  • npm downtime
  • vendor lock-in
  • limited custom architectures
  • loss of control

Custom Deployment

We need some servers

  • AWS
  • Rackspace
  • Joyent
  • Digital Ocean
  • Co-Lo

Servers need to be setup

  • Manually
  • Bash scripts
  • Configuration Management Tools

Configuration Management Tools

  • Puppet
  • Chef
  • Salt
  • Ansible

Ansible

is awesome

Ansible is an open-source software platform for configuring and managing computers


Orchestration Engine

“Ansible is infrastructure as data
(as opposed to infrastructure as code)”

Why we picked Ansible

  • Easy to learn
  • Tons of built-in modules
  • Python under the hood (no Ruby for us)
  • Uses existing SSH infrastructure
  • Idempotent

Continous Integration

With Jenkins

  • Pulls changes from GitHub
  • Runs tests
  • Builds Debian packages
  • Archives build artifacts
  • Runs Ansible deploy playbooks

What are we deploying?

In this example...

  • MongoDB Server
  • 2 (or more) NodeJS 'ToDo App' Servers
  • Nginx Server for load balancing

Quick Ansible Tutorial

Play to install NodeJS


- name: Add node apt repo
  apt_repository: repo='ppa:chris-lea/node.js' state=present update_cache=yes

- name: Install node
  apt: pkg=nodejs state=present

- name: Install global packages
  npm: global=yes name={{ item }} state=present
  with_items: nodejs_global_packages
				

Playbooks


- name: configure and deploy nginx load balancer
  hosts: nginx-prod
  user: root

  roles:
  - common
  - nginx

- name: configure and deploy nodejs app
  hosts: app-prod
  user: root

  roles:
  - common
  - nodejs-app
				

Inventory


[app-prod]
162.1.2.3
162.1.2.4

[nginx-prod]
162.2.1.3
		    

Ok, onto 'real' stuff

Let's create VMs on Digitial Ocean

provision_digital_ocean.yml

---
- hosts: localhost
  gather_facts: False

  vars_files:
    - vars/keys.yml
    - vars/droplets.yml

  tasks:
    - include: tasks/create_droplet.yml droplets={{droplets}}

droplets.yml


---
droplets:
  - name: production.nginx.1
  - name: production.app.1
  - name: production.mongodb.1

create_droplet.yml


---
- name: ensure droplet exists
  digital_ocean: >
    state=present
    command=droplet
    name={{ item.name }}
    ssh_key_ids={{ default_key }}
    size_id={{ item.size|default(default_size) }}
    region_id={{ item.region|default(default_region) }}
    image_id={{ item.image|default(default_image) }}
    private_networking=yes
    wait_timeout=500
    unique_name=true
  register: droplet
  with_items: droplets

Digital Ocean

Dynamic Inventory!



ansible-playbook -i hosts site.yml

This becomes our Inventory

Putting it all together

Lets look at the code...

References

https://github.com/garethr/ansible-provisioner
https://github.com/scotch-io/node-todo/tree/tut1-starter
https://github.com/rosskukulinski/getting-mean-with-ansible

Special thanks to @jareddlc

Thanks!

Ross Kukulinski
@rosskukulinski
ross _at_ SpeakIt.io

Reference slides w/ code copypasta

site.yml (1/3)


- include: provision_digital_ocean.yml
#  MongoDB Configuration
- hosts: production.mongodb.*
  user: root
  vars_files:
    - vars/database.yml
  tasks:
    - name: Add production.mongodb.* to inventory
      group_by: key="production.mongodb"
    - name: setfact
      set_fact: db_ip={{ ansible_eth1["ipv4"]["address"] }}
  roles:
    - common
    - mongodb

site.yml (2/3)


#  NodeJS Todo App Configuration
- hosts: production.app.*
  user: root
  vars_files:
    - vars/database.yml
  tasks:
    - name: Add production.app.* to inventory
      group_by: key="production.app"
    - name: setfact
      set_fact: http_ip={{ ansible_eth1["ipv4"]["address"] }}
  roles:
    - common
    - nodejs
    - app

NodeJS DB Connection


module.exports = {
  // the database url to connect
  url : 'mongodb://
    {%- for host in groups['production.mongodb'] -%}
      {{ hostvars[host]['db_ip'] }}
        {%- if loop.first -%}
          /{{db_name}}
        {% endif %}
        {%- if loop.length > 1 and not loop.last -%}
          ,
        {% endif %}
    {% endfor %}';
}
// url : 'mongodb://10.128.190.31/node-todo,10.128.190.11,10.112.232.11'

site.yml (3/3)


#  Nginx Configuration
- hosts: production.nginx.*
  user: root
  tasks:
    - name: Add production.nginx.* to inventory
      group_by: key="production.nginx"
  roles:
    - common
    - nginx

nginx site


# Simple Load Balancer
upstream static  {
  least_conn;
  {% for host in groups['production.app'] %}
    server {{ hostvars[host]['http_ip'] }}:8080;
  {% endfor %}
}
server {
  listen 80;
  location / {
    proxy_pass http://static/;
    proxy_http_version 1.1;
    proxy_next_upstream error timeout;
  }
}