Loading

saltstack-rails

Alexei Volkov

This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.

SaltStack

Ruby Quick Start

Skype: volkov.gl

Live example at GitHub

What is SaltStack?

SaltStack is a provisioning tool.

It manages one or more nodes, applying and maintaining configuration.

Anything like this?

  • Chef
  • Puppet
  • Ansible
  • and much more

Several tools providing configuration orchestrating one way or another:

Also check Docker, Vagrant and Otto if you are looking for something special.

Why SaltStack?

  • It is easy to start with.
  • Less time to start with in comparison with Docker and other containers.
  • Great documentation and examples

How it works?

Or we may go masterless and keep configuration on the target server.

Of course the real power, nifties and whistles lays in the world of master-minions setup. One keystroke — and couple of servers configured. 

 

Single server masterless setup is still great for individual developers and small teams when projects only needs a single server.

 

In the next slides we would target the second option (most configuration is common though).

When to use SaltStack?

Any time. Really.

  • Your server gone and you need to setup it again? One keystroke — bam, it is ready!
  • You have a production server, and want to setup staging just like that one? Here we go.
  • You need to show something, and need to spawn another typical server? Salt at your service.
  • The project grew up and should be split into several nodes with different roles? Just specify them and you are set up!

Building Bricks

Commands

If the Salt Master is present it may execute commands across all the minions:

salt '*' cmd.run 'ls -l /etc'

salt -E 'storage.*' disk.usage

salt '*' pkg.install cowsay

salt -G 'os:Ubuntu' network.interfaces

States

States are the main configuration building material. They specifies the state of the target server.

And remember: not all states are equally good for your server. 

States: Declarations

Most states declares the desired final state of your target server: installed packages, managed files or granted DB permissions.

 

These states would only be executed if required.

install vim:
  pkg.installed:
    - name: vim

https://github.com/myapp/hello-world:
  git.latest:
    - rev: develop
    - target: /var/www/epic_app

States: Executables

Executable states would run every time configuration applied, except you explicitly specify run conditions.

 

These states has prefix salt.module.*

restart vsftpd:
  module.run:
    - name: service.restart
    - m_name: vsftpd  
    # m_name gets passed to the execution module as "name"

Pillar

Pillar is basically a set of vocabularies containing configuration variables. It provides recipes reusability.

 

While states are freely distributed to any authorized client, pillar data is secure and restricted to the target nodes only.

ftpusername: me
ftppassword: oxfm4@8t5gglu^h^&

Testing States Application

If you want to check which changes would be made prior to new configuration application, you should specify test=True flag.

sudo salt 'minion1' state.apply examples test=True

States

United States of SaltStack

Searching the Right One 

Executable (.modules) — ignore this

Configurable (.states) — YES!

Inside the Doc

Basic usage

DevOps mode: ON

State Composition

frank: # State ID, Unique, default for name if name is absent  
  mysql_user.present: # Namespace.State
    - host: localhost # Named Params 
    - password: "bob@cat"
    - connection_user: someuser
    - connection_pass: somepass
    - connection_charset: utf8
    - saltenv:
      - LC_ALL: "en_US.utf8"

States are YAML entities, described by

  • unique ID
  • Namespace.StateName
  • State parameters

States With Logic

States utilizes template engine, by default they use Jinja.

Be aware of spacing!

# Raw state file
moe:
  user.present
larry:
  user.present
curly:
  user.present


# Template driven state file
{% for usr in ['moe','larry','curly'] %}
{{ usr }}:
  user.present
{% endfor %}

Describing a Node

Let the show begin!

Organizing Files

To start with you need a SaltStack installed in Masterless mode.

By default Salt checks /srv/salt for a file top.sls . This is a root state file, something like an entry point for application.

# /srv/salt/top.sls
base:
  '*':
    - webserver # state files to apply

# /srv/salt/webserver.sls
apache:               # ID declaration
  pkg:                # state declaration
    - installed       # function declaration

# bash
$ salt-call --local state.highstate

Getting Real

When Hello World just isn't  enough.

Here is how structure may look like when you need to setup three Ruby app servers from scratch.

Composing Setup

Top.sls again

Our entry point, which defines state files to apply.

base:
  '*':
    - user
    - swap
    - webserver
    - misc
    - ruby_server
    - mysql_server
    - ssh
    - puma

Create deploy user

deploy:
  group.present: []
  user.present:
    - gid: deploy
    - home: /home/deploy
    - groups:
      - sudo
      - deploy

Creating Swap

Cmd is a module. Cmd.run would be executed each time states applied — unless we add a condition.

 

Conditions are important: even if a module would change nothing it would produce "changed" entry in the log.

/swapfile:
  cmd.run:
    - name: "fallocate -l 1024M /swapfile && chmod 600 /swapfile && mkswap /swapfile"
    - unless: ls -lh /swapfile # condition to check before execution
  mount.swap:
    - require: # require statements allow to control SaltStack execution flow
      - cmd: /swapfile

Nginx

nginx:
  pkg:
    - installed
  service.running:
    - watch:
      - pkg: nginx
      - file: /etc/nginx/sites-available/exampleone

/srv/www:
  file.directory:
    - user: deploy
    - group: deploy
    - mode: 755
    - makedirs: True


/srv/www/exampleone.com/shared/config/secrets.yml:
  file.managed: # setup managed file
    - source: salt://files/srv/exampleone.secrets.yml
    - user: deploy
    - group: deploy
    - mode: 644
    - makedirs: True
/etc/nginx/sites-available/exampleone:
  file.managed:
    - source: salt://files/nginx/exampleone
    - user: root
    - group: root
    - mode: 640

/etc/nginx/sites-enabled/exampleone:
  file.symlink:
    - target: /etc/nginx/sites-available/exampleone
    - require:
      - file: /etc/nginx/sites-available/exampleone


/etc/nginx/sites-available/exampletwo:
  file.managed:
    - source: salt://files/nginx/exampletwo
    - user: root
    - group: root
    - mode: 640

/etc/nginx/sites-enabled/exampletwo:
  file.symlink:
    - target: /etc/nginx/sites-available/exampletwo
    - require:
      - file: /etc/nginx/sites-available/exampletwo

/etc/nginx/sites-available/examplethree:
  file.managed:
    # ...

NodeJS

NodeJS repo:
  cmd.wait:
    - name: curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
    - watch:
      - pkg: nodejs

nodejs:
  pkg.installed

Cmd.wait is another useful executable with built-in condition check. It wouldn't run unless NodeJS package should be installed.

Ruby Essentials

fetch_keys:
  cmd.run:
    - name: > 
        gpg --keyserver hkp://keys.gnupg.net
         --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
    - user: deploy
    - unless: gpg --list-keys | grep 4096R/D39DC0E3

rvm-deps:
  pkg.installed:
    - pkgs:
      - bash
      - coreutils
      - gzip
      - bzip2
      - gawk
      - sed
      - curl
      - git-core

mri-deps:
  pkg.installed:
    - pkgs:
      - build-essential
      - openssl
      - libreadline6
      - libreadline6-dev
      - curl
      - git-core
      # ...
### exampleone ###
ruby-2.2.2:
  rvm.installed:
    - default: True
    - user: deploy
    - require:
      - pkg: rvm-deps
      - pkg: mri-deps
      - user: deploy

exampleone:
  rvm.gemset_present:
    - ruby: ruby-2.2.2
    - user: deploy
    - require:
      - rvm: ruby-2.2.2

### exampletwo ###
ruby-2.0.0-p247:
  rvm.installed:
    - user: deploy
    - require:
      - pkg: rvm-deps
      - pkg: mri-deps
      - user: deploy

exampletwo:
  rvm.gemset_present:
    - ruby: ruby-2.0.0-p247
    - user: deploy
    - require:
      - rvm: ruby-2.0.0-p247

### examplethree ###
# ...

bundler-system:
  gem.installed:
    - name: bundler
    - user: deploy

MySQL Setup

mysql-server:
  pkg.installed

MySQL user:
  mysql_user.present:
    - name: deploy
    - host: localhost
    - password: SomeCoolPassword
    - connection_charset: utf8
    - require:
      - pkg: mysql-server

ruby-mysql-deps:
  pkg.installed:
    - pkgs:
      - libmysqlclient-dev

db-exampleone:
  mysql_database.present:
    - name: exampleone
  mysql_grants.present:
    - grant: all privileges
    - database: exampleone.*
    - user: deploy

db-exampletwo:
  mysql_database.present:
    - name: exampletwo
  mysql_grants.present:
    - grant: all privileges
    - database: exampletwo.*
    - user: deploy

db-examplethree:
  mysql_database.present:
    - name: examplethree
  mysql_grants.present:
    - grant: all privileges
    - database: examplethree.*
    - user: deploy

SSH Access

ssh_key_access:
  ssh_auth.present:
    - user: deploy
    - source: salt://files/authorized_keys
    - config: /home/deploy/.ssh/authorized_keys

Run, Puma! Run!

/etc/init/puma.conf:
  file.managed:
    - source: salt://files/puma/etc/init/puma.conf
    - user: root
    - group: root
    - mode: 640

/etc/init/puma-manager.conf:
  file.managed:
    - source: salt://files/puma/etc/init/puma-manager.conf
    - user: root
    - group: root
    - mode: 640

/etc/puma.conf:
  file.managed:
    - source: salt://files/puma/etc/puma.conf
    - user: root
    - group: root
    - mode: 640

Puma-manager is the official puma upstart script. It requires ./config/puma.rb in your app folder and "just works"™.

Done!

Copy here, past there, change a few variables, and you have another server live

Done!

Copy here, past there, change a few variables, and you have another server live

"Few variables" they said… Change deploy user name they said…

# webserver.sls
/srv/www:
  file.directory:
    - user: deploy
    - group: deploy
    - mode: 755
    - makedirs: True
# user.sls
deploy:
  group.present: []
  user.present:
    - gid: deploy
    - home: /home/deploy
    - groups:
      - sudo
      - deploy
# puma.conf
setuid deploy
setgid deploy
# mysql_server.sls
MySQL user:
  mysql_user.present:
    - name: deploy

#...
db-exampleone:
  mysql_database.present:
    - name: exampleone
  mysql_grants.present:
    - grant: all privileges
    - database: exampleone.*
    - user: deploy
# ruby_server.sls
exampleone:
  rvm.gemset_present:
    - ruby: ruby-2.2.2
    - user: deploy
    - require:
      - rvm: ruby-2.2.2

bundler-system:
  gem.installed:
    - name: bundler
    - user: deploy

Pillar

Vars. Vars never always changing

Mixing Pillar in 

Pillar has the same structure as state files:
top.sls points to YAML vocabularies

# pillar/top.sls
base:
  '*':
    - rails_data

# pillar/rails_data.sls
rails_data:
  user: deploy
  mysql:
    password: SomeVeryStr0ngPa55worD
  ssh_public_key: >
    PastYourPubKeyHereAndRecieveYourSshAccess
    ToDeployUserWithoutSmsAndRegistration== webowner@example.com  
  servers:
    - exampleone:
      name: exampleone
      ruby: ruby-2.0.0-p247
      gemset: exampleone
      domain: exampleone.com
      db: exampleonedb
    - exampletwo:
      name: exampletwo
      ruby: ruby-2.0.0-p247
      gemset: exampletwo
      domain: exampletwo.com
      db: exampletwodb
    - examplethree:
      name: examplethree
      ruby: ruby-2.2.1
      gemset: examplethree
      domain: examplethree.com
      db: examplethreedb

Fat free. Typo free.

Cleaning up managed files

Okay, we may use Jinja in state files. But what about Nginx, Puma and other static configs? 

 

We may clean them up as well, just specify the template engine, and you are set up.

{% for server in salt['pillar.get']('rails_data:servers') %}
/etc/nginx/sites-available/{{ server.name }}:
  file.managed:
    - template: jinja
    - source: salt://files/nginx/vhost

Excerpts

# webserver.sls
/etc/nginx/sites-available/exampleone:
  file.managed:
    - source: salt://files/nginx/exampleone
    - user: root
    - group: root
    - mode: 640

/etc/nginx/sites-available/exampletwo:
  file.managed:
    - source: salt://files/nginx/exampletwo
    - user: root
    - group: root
    - mode: 640

/etc/nginx/sites-available/examplethree:
  file.managed:
    - source: salt://files/nginx/examplethree
    - user: root
    - group: root
    - mode: 640
# webserver.sls with Pillar
{% for server in 
 salt['pillar.get']('rails_data:servers') %}

/etc/nginx/sites-available/{{ server.name }}:
  file.managed:
    - template: jinja
    - source: salt://files/nginx/vhost
    - user: root
    - group: root
    - mode: 640
    - defaults: # provide template variables
      domain: {{ server.domain }}
      appname: {{ server.name }}_app

{% endfor %}
# puma.conf
# ...
setuid deploy
setgid deploy
# ...
# puma.conf with Pillar
# ...
{%- set user = 
 salt['pillar.get']('rails_data:user') %}
setuid {{ user }}
setgid {{ user }}
# ...

Questions?

No Updates?

Some pepper?

Made with Slides.com