Server setup documentation is wrong.
Environment configuration documentation is wrong.
We have a million servers in each environment.
Wait, whose server is this?!?!
How to create the server from scratch including all installed programs, security settings, configuration, etc.
For the most part, we do this.
But then we ask, is every server documented?
How the server should be setup for each environment: development, test, user-acceptance-testing, production, etc.
But then we ask, is every environment matching this documentation?
Each environment has a lot of servers to setup and maintain.
But then we ask, how do we setup each of the servers?
Tribal knowledge is any unwritten knowledge and is passed on by word of mouth & experience.
This could be a problem if you lose your vital employees.
Because not everything is a container yet.
And even containers have to run on a server.
Automation. We know how to automate deployments
Quality. We can code review infrastructure changes.
Documentation as Code.
Source control all code in Git/TFS/Mercurial.
Great for audits and compliance restrictions.
DevOps is cool.
Chef
Puppet
Ansible
Vagrant
and many others!
For when we don't want 5000 line shell scripts.
Everything was tested with the following 3 VMs
PS C:\Users\Daniel> az network public-ip list --query '[].{IPAddress: ipAddress, FQDN: dnsSettings.fqdn, Name: name}' --output table
IPAddress FQDN Name
------------- ------------------------------------ ------------------
13.90.245.94 ansiblevm1.eastus.cloudapp.azure.com ansiblevm1PublicIP
52.168.111.32 ansiblevm2.eastus.cloudapp.azure.com ansiblevm2PublicIP
13.90.251.252 ansiblevm3.eastus.cloudapp.azure.com ansiblevm3PublicIP
PS C:\Users\Daniel>
Note: these VMs shouldn't be publicly accessible.
ansible all -m ping
$ ansible all -m ping
ansiblevm1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
ansiblevm2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
ansiblevm3 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Ansible provides wrappers to common functionality in the form of modules.
$ ansible all -m ping
Ansible configuration includes an "inventory" of the servers.
ansiblevm1 ansible_ssh_user=daniel ansible_host=ansiblevm1.eastus.cloudapp.azure.com
ansiblevm2 ansible_ssh_user=daniel ansible_host=ansiblevm2.eastus.cloudapp.azure.com
ansiblevm3 ansible_ssh_user=daniel ansible_host=ansiblevm3.eastus.cloudapp.azure.com
Inventories can be dynamic too.
$ ansible all -m ping
Ansible connects using SSH, SCP, and SFTP.
An example from the above looks like this.
ssh -i /mnt/c/Users/Daniel/.ssh/id_rsa -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no \
-o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no \
-o User=daniel -o ConnectTimeout=10 ansiblevm3.eastus.cloudapp.azure.com '/bin/sh -c '"'"'echo ~ && sleep 0'"'"''
Simplified
ssh -i /mnt/c/Users/Daniel/.ssh/id_rsa daniel@ansiblevm1.eastus.cloudapp.azure.com
$ ansible all -m ping
Ansible configuration is simple to start with.
[ssh_connection]
ssh_args = -i "/mnt/c/Users/Daniel/.ssh/id_rsa"
[defaults]
inventory = hosts
host_key_checking = False
Note: "host_key_checking = False" may not be a great idea.
$ ansible all -m ping
ansiblevm1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
When each command runs, it's going to tell you if it changed anything on the server.
Infrastructure as Code declares the state we want to achieve.
Idempotent actions allow us to run the code many times with no harm.
$ ansible all -s -m user -a "name=johndoe state=absent"
ansiblevm1 | SUCCESS => {
"changed": true,
"force": false,
"name": "johndoe",
"remove": false,
"state": "absent",
"stderr": "sent invalidate(passwd) request, exiting\nsent invalidate(group) request, exiting\nsent invalidate(passwd) request, exiting\nsent invalidate(group) request, exiting\n",
"stderr_lines": [
"sent invalidate(passwd) request, exiting",
"sent invalidate(group) request, exiting",
"sent invalidate(passwd) request, exiting",
"sent invalidate(group) request, exiting"
]
}
ansiblevm3 | SUCCESS => {
"changed": false,
"name": "johndoe",
"state": "absent"
}
...
Idempotent commands are as close to declaring the state of the server as we can get.
Servers are rather like databases, being transformed in-place.
CREATE OR ALTER PROCEDURE
Ansible can combine commands into a playbook.
---
- hosts: all # The servers from the inventory.
tasks:
# Idempotent Task.
- name: Make sure that apt-get dotnet-sdk-2.1.4 is present.
apt:
name: dotnet-sdk-2.1.4
state: present
# Non-Idempotent Task.
- name: Runs apt-get update, if last run more than one hour ago.
apt:
update_cache: yes
cache_valid_time: 3600
Notice that not all commands are Idempotent.
$ ansible-playbook -s dotnet-playbook.yml
PLAY [all] *************************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************
ok: [ansiblevm1]
TASK [Is Microsoft.gpg there?] *****************************************************************************************************************************************************
ok: [ansiblevm1]
TASK [Download Microsoft GPG] ******************************************************************************************************************************************************
skipping: [ansiblevm1]
TASK [Microsoft Packages is in source list.] ***************************************************************************************************************************************
ok: [ansiblevm1]
TASK [Runs apt-get update, if last run more than one hour ago.] ********************************************************************************************************************
ok: [ansiblevm1]
TASK [dotnet-sdk-2.1.4 is present.] ************************************************************************************************************************************************
ok: [ansiblevm1]
PLAY RECAP *************************************************************************************************************************************************************************
ansiblevm1 : ok=5 changed=0 unreachable=0 failed=0
---
- hosts: ansiblevm1
tasks:
- name: Is Microsoft.gpg there?
stat:
path: /etc/apt/trusted.gpg.d/microsoft.gpg
register: microsoft_gpg
- name: Download Microsoft GPG
command: bash -c "curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.gpg"
when: microsoft_gpg.stat.exists == False
- name: Microsoft Packages is in source list.
lineinfile:
path: /etc/apt/sources.list.d/dotnetdev.list
line: "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main"
state: present
create: yes
- name: Runs apt-get update, if last run more than one hour ago.
apt:
update_cache: yes
cache_valid_time: 3600
- name: dotnet-sdk-2.1.4 is present.
apt:
name: dotnet-sdk-2.1.4
state: present
Ansible code can be declarative, but it can also be imperative when needed.
And there is an Ansible Module for almost anything you want.
Server setup documentation is how to create the server from scratch including all installed programs, security settings, configuration, etc.
So far, I've shown how to install programs with Ansible.
Checking dotnet version with ad-hoc commands shows this:
$ ansible all -m shell -a "dotnet --version"
ansiblevm1 | SUCCESS | rc=0 >>
2.1.4
ansiblevm3 | SUCCESS | rc=0 >>
2.1.4
ansiblevm2 | SUCCESS | rc=0 >>
2.1.4
Ad-hoc commands are useful for gathering information on existing servers.
Environment configuration documentation is how the server should be setup for each environment:
development, test, user-acceptance-testing, production, etc.
Variables can come from:
System Variables are implicit. User-defined variables must be explicitly defined.
Putting a line in a file with a template
$ ansible all -m lineinfile -a "path=~/test.txt state=present create=true line=\"{{ inventory_hostname }} is the
name of this server in the inventory file\""
ansiblevm3 | SUCCESS => {
"backup": "",
"changed": true,
"msg": "line added"
}
...
Reading the line from that file
$ ansible all -m shell -a "cat ~/test.txt"
ansiblevm1 | SUCCESS | rc=0 >>
ansiblevm1 is the name of this server in the inventory file
ansiblevm2 | SUCCESS | rc=0 >>
ansiblevm2 is the name of this server in the inventory file
...
Variables such as a database connection string may be placed with these modules and more:
So far you've seen "all" or the "ansiblevm1" virtual machine name used to identify servers in ad-hoc commands and playbooks.
Inventory can also be grouped for conenvience.
[DBServer]
ansiblevm1 ansible_ssh_user=daniel ansible_host=ansiblevm1.eastus.cloudapp.azure.com
[WebServers]
ansiblevm2 ansible_ssh_user=daniel ansible_host=ansiblevm2.eastus.cloudapp.azure.com
ansiblevm3 ansible_ssh_user=daniel ansible_host=ansiblevm3.eastus.cloudapp.azure.com
Environments can be groups in inventory.
Or, make separate inventory files for each environment.
ansible WebServers -m ping
Configuration variables usually hold sensitive information: passwords, keys.
Ansible-Vault can help protect variables in source control.
Variables are the basis for configuration and can be grouped by many ways, and come from many sources.
How variables are organized can be flexible, and will depend upon your organization.
Let's make our lives easier.
Website: olivercoding.com
Twitter: @a_software_dev
Github: danieloliver
Slides: https://ansible.olivercoding.com/