DBA Hub

📋Steps in this guide1/13

Ansible : Playbooks - First Steps

This article presents some examples of basic Ansible Playbooks, to give you a feel for how Ansible Playbooks work.

oracle miscconfigurationintermediate
by OracleDba
14 views
1

Useful Resources

There is a vagrant build for the virtual machines used in these examples here . There is a GitHub repository of the scripts used in the examples here . There are videos covering these topics here. - Ansible Playbooks : Introduction - Ansible Playbooks : Lists and Loops - Ansible Playbooks : Host and Group Variables - Ansible Playbooks : Handlers - Ansible Playbooks : Files and Templates - Ansible Playbooks : Tags - Ansible Playbooks : Users and Groups - Ansible Playbooks : Roles - Ansible Playbooks : Vault
2

Creating a NGINX Playbook (Packages, Services, Firewall Rules)

We want to use Ansible to build a standard approach to configuring NGINX servers. Our first attempt at a playbook contains two tasks. We install the NGINX package, then enable and start the NGINX service. We create a file called "configure_nginx.yml" with the following contents. We've given the playbook a name of "Configure NGINX servers". It is to be applied to all hosts in the "appservers" group. We allow escalated privileges by setting "become" to true. We then have two named tasks. The first uses the "dnf" module to install the "nginx" package. The second uses the "service" module to enable and start the "nginx" service. We run the playbook using the command. If we needed a password for escalated privileges, we would add the flag to the command line, which would prompt us for the password. From the output we can see the "Install NGINX package" task resulted in changes on the two application servers. So did the "Enable and start NGINX service" task. Some time later we remember we need to make sure the firewall is enable and running on these servers, and we need to make sure SSH and HTTPS traffic can get through the firewall. To do this we add new tasks into the existing playbook. The "firewalld" module allows us to amend the firewall, and the "service" module allows us to enable and start the "firewalld" service. With the new tasks in place, we can run the entire playbook again. This time there are no changes for the "Install NGINX package" and "Enable and start NGINX service" tasks, as they were done previously. The "Allow SSH traffic through the firewall" task didn't require any changes, as port 22 is open by default on the firewall, but the "Allow HTTPS traffic through the firewall" task did require changes to the firewall configuration. So did the "Enable the firewall" task, as the firewall was disabled and stopped on the VM originally. The fact we can incrementally improve a playbook, and rerun it without affecting previously applied tasks is one of benefits of Ansible. Rather than applying one-off changes, we can build them into a playbook, which acts as a permanent and consistent record of how the servers were configured. Let's assume our company is going to start using Ubuntu for some application servers. For Ubuntu we would use the "apt" module to install packages, so the NGINX installation will be different on Oracle Linux and Ubuntu. We could write separate playbooks for each distribution, or we could combine them together, using "when" to determine which tasks apply to which distribution using Ansible "facts". Facts are just information about the remote system, which we can reference in our playbooks. We'll mention more about facts later. In the following example the "Install NGINX package (DNF)" task uses the "dnf" module, and is only run for hosts with a distribution called "OracleLinux", "Red Hat Enterprise Linux" or "CentOS". The "Install NGINX package (APT)" task uses the "apt" module, and is only run for hosts with a distribution called "Ubuntu" or "Debian". When we run the playbook, the "Install NGINX package (DNF)" task is run, but the "Install NGINX package (APT)" task is skipped, because we don't have any Ubuntu servers yet. The output tells us a task was skipped for each server. You can find more information, or facts, about the remote servers using the following Ansible commands. That these facts can be referred to in the playbook, like in your "when" criteria.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
---
- name: Configure NGINX servers
  hosts: appservers
  become: true
  tasks:
  
  - name: Install NGINX package
    dnf:
      name: nginx
      state: present
      update_cache: yes

  - name: Enable and start NGINX service
    service:
      name: nginx
      enabled: yes
      state: started

$ ansible-playbook configure_nginx.yml

PLAY [Configure NGINX servers] *******************************************************************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Install NGINX package] *********************************************************************************************************************************************************************
changed: [appserver1.localdomain]
changed: [appserver2.localdomain]

TASK [Enable and start NGINX service] ************************************************************************************************************************************************************
changed: [appserver1.localdomain]
changed: [appserver2.localdomain]

PLAY RECAP ***************************************************************************************************************************************************************************************
appserver1.localdomain     : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
appserver2.localdomain     : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

---
- name: Configure NGINX servers
  hosts: appservers
  become: true
  tasks:
  
  - name: Install NGINX package
    dnf:
      name: nginx
      state: present
      update_cache: yes

  - name: Enable and start NGINX service
    service:
      name: nginx
      enabled: yes
      state: started

  - name: Allow SSH traffic through the firewall
    firewalld:
      service: ssh
      permanent: yes
      state: enabled

  - name: Allow HTTPS traffic through the firewall
    firewalld:
      service: https
      permanent: yes
      state: enabled

  - name: Enable the firewall
    service:
      name: firewalld
      enabled: yes
      state: started

$ ansible-playbook configure_nginx.yml

PLAY [Configure NGINX servers] *****************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Install NGINX package] *******************************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Enable and start NGINX service] **********************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Allow SSH traffic through the firewall] **************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Allow HTTPS traffic through the firewall] ************************************************************************************************
changed: [appserver1.localdomain]
changed: [appserver2.localdomain]

TASK [Enable the firewall] *********************************************************************************************************************
changed: [appserver2.localdomain]
changed: [appserver1.localdomain]

PLAY RECAP *************************************************************************************************************************************
appserver1.localdomain     : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
appserver2.localdomain     : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

---
- name: Configure NGINX servers
  hosts: appservers
  become: true
  tasks:
  
  - name: Install NGINX package (DNF)
    dnf:
      name: nginx
      state: present
      update_cache: yes
    when: ansible_distribution in ["OracleLinux", "Red Hat Enterprise Linux", "CentOS"]

  - name: Install NGINX package (APT)
    apt:
      name: nginx
      state: present
      update_cache: yes
    when: ansible_distribution in ["Ubuntu", "Debian"]

  - name: Enable and start NGINX service
    service:
      name: nginx
      enabled: yes
      state: started

  - name: Allow SSH traffic through the firewall
    firewalld:
      service: ssh
      permanent: yes
      state: enabled

  - name: Allow HTTPS traffic through the firewall
    firewalld:
      service: https
      permanent: yes
      state: enabled

  - name: Enable the firewall
    service:
      name: firewalld
      enabled: yes
      state: started

$ ansible-playbook configure_nginx.yml

PLAY [Configure NGINX servers] *****************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Install NGINX package (DNF)] *************************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Install NGINX package (APT)] *************************************************************************************************************
skipping: [appserver1.localdomain]
skipping: [appserver2.localdomain]

TASK [Enable and start NGINX service] **********************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Allow SSH traffic through the firewall] **************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Allow HTTPS traffic through the firewall] ************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Enable the firewall] *********************************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

PLAY RECAP *************************************************************************************************************************************
appserver1.localdomain     : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
appserver2.localdomain     : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

$

$ ansible database1.localdomain -m setup
$ ansible database1.localdomain -m gather_facts
3

Creating a Patching Playbook (Packages, Reboots)

We want a playbook that will update all the packages on our database servers. We create a file called "update_database_packages.yml" with the following contents. We use the "dnf" module with a wildcard to update all installed packages to the latest version. We run the playbook and it works OK. There are no changes, as the packages are already up to date. To make sure we get all changes applied, including kernel changes, it might be sensible to reboot. We can do that by adding the a task based on the "reboot" module. We run the playbook and this time we see a change associated with the "Reboot server" task. Once again, we want to make the playbook work for multiple distributions, so we add a new task using the "apt" module, and limit the update tasks based on the underlying distribution. As expected, the "Update all packages (DNF)" task runs, but the "Update all packages (APT)" task is skipped. That works fine, but it has one annoying characteristic. Even if there are no package changes, we still get a reboot. We can fix that by registering the result of a task, and only performing an action if the task resulted in a change. In the following example we register the "dnf_update" and "apt_update" variables for their respective tasks, and limit the reboot task to only happen if one of these tasks has resulted in a change. When we run the playbook there are no package updates, so the reboot is skipped.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
---
- name: Patch servers
  hosts: databases
  become: true
  tasks:

  - name: Update all packages
    dnf:
      name: "*"
      update_cache: yes
      state: latest

$ ansible-playbook update_database_packages.yml

PLAY [Patch servers] ***************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Update all packages] *********************************************************************************************************************
ok: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

---
- name: Patch servers
  hosts: databases
  become: true
  tasks:

  - name: Update all packages
    dnf:
      name: "*"
      update_cache: yes
      state: latest

  - name: Reboot server
    reboot:

$ ansible-playbook update_database_packages.yml

PLAY [Patch servers] ***************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Update all packages] *********************************************************************************************************************
ok: [database1.localdomain]

TASK [Reboot server] ***************************************************************************************************************************
changed: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

---
- name: Patch servers
  hosts: databases
  become: true
  tasks:

  - name: Update all packages (DNF)
    dnf:
      name: "*"
      update_cache: yes
      state: latest
    when: ansible_distribution in ["OracleLinux", "Red Hat Enterprise Linux", "CentOS"]

  - name: Update all packages (APT)
    apt:
      name: "*"
      update_cache: yes
      state: latest
    when: ansible_distribution in ["Ubuntu", "Debian"]

  - name: Reboot server
    reboot:

$ ansible-playbook update_database_packages.yml

PLAY [Patch servers] ***************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Update all packages (DNF)] ***************************************************************************************************************
ok: [database1.localdomain]

TASK [Update all packages (APT)] ***************************************************************************************************************
skipping: [database1.localdomain]

TASK [Reboot server] ***************************************************************************************************************************
changed: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=3    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

$

---
- name: Patch servers
  hosts: databases
  become: true
  tasks:

  - name: Update all packages (DNF)
    dnf:
      name: "*"
      update_cache: yes
      state: latest
    when: ansible_distribution in ["OracleLinux", "Red Hat Enterprise Linux", "CentOS"]
    register: dnf_update

  - name: Update all packages (APT)
    apt:
      name: "*"
      update_cache: yes
      state: latest
    when: ansible_distribution in ["Ubuntu", "Debian"]
    register: apt_update

  - name: Reboot server
    reboot:
    when: dnf_update.changed or apt_update.changed

$ ansible-playbook update_database_packages.yml

PLAY [Patch servers] ***************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Update all packages (DNF)] ***************************************************************************************************************
ok: [database1.localdomain]

TASK [Update all packages (APT)] ***************************************************************************************************************
skipping: [database1.localdomain]

TASK [Reboot server] ***************************************************************************************************************************
skipping: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=2    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

$
4

Using Tags

While we are testing a playbook, we may want to limit which parts of it execute. We can control this with tags. We assign tags to the tasks, and then limit the play using those. We create a file called "tags.yml" with the following contents. This is similar to the NGINX installation we used previously, but we have added tags to each task. These tags can be any text, but using the tag "all" means the task will always run. We can list the tags using the flag. The playbook will run as normal, but now we have the option of running only those tasks that match a specific tag, or group of tags.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
---
- name: Using tags
  hosts: appservers
  become: true
  tasks:
  
  - name: Install NGINX package
    tags: nginx
    dnf:
      name: nginx
      state: present
      update_cache: yes

  - name: Enable and start NGINX service
    tags: nginx
    service:
      name: nginx
      enabled: yes
      state: started

  - name: Allow SSH traffic through the firewall
    tags: firewall,fwrule
    firewalld:
      service: ssh
      permanent: yes
      state: enabled

  - name: Allow HTTPS traffic through the firewall
    tags: firewall,fwrule
    firewalld:
      service: https
      permanent: yes
      state: enabled

  - name: Enable the firewall
    tags: firewall,fwservice
    service:
      name: firewalld
      enabled: yes
      state: started

$ ansible-playbook --list-tags tags.yml

playbook: tags.yml

  play #1 (appservers): Using tags      TAGS: []
      TASK TAGS: [firewall, nginx, rule, service]

$

$ ansible-playbook --tags "firewall" tags.yml
$ ansible-playbook --tags "nginx" tags.yml
$ ansible-playbook --tags "nginx,fwrule" tags.yml
5

Managing Files

Create a directory called "files" on the local server under your working directory, or under the role if the files are part of a role. This is where Ansible will look for files by default. Under that directory create a file called "defaut_page.html" will the following contents. We create a new playbook called "configure_nginx_2.yml" with the following contents. It's similar to one of the simpler NGINX playbooks, but we've added an extra task using the "copy" module to copy the default web page to the NGINX servers. The "src" is our source file, without the implied "files" directory. The "dest" is the location we want to copy it to on the servers. The ownership and permissions are also included. When we run the playbook, we see the file is copied as required. You may prefer to use templates if some customization of the files are necessary.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<html>
  <title>Default Page</title>
  <body>
    <p>This is the default page!</p>
  </body>
</html>

---
- name: Configure NGINX servers
  hosts: appservers
  become: true
  tasks:

  - name: Install NGINX package
    dnf:
      name: nginx
      state: present
      update_cache: yes

  - name: Enable and start NGINX service
    service:
      name: nginx
      enabled: yes
      state: started

  - name: Allow SSH traffic through the firewall
    firewalld:
      service: ssh
      permanent: yes
      state: enabled

  - name: Allow HTTPS traffic through the firewall
    firewalld:
      service: https
      permanent: yes
      state: enabled

  - name: Enable the firewall
    service:
      name: firewalld
      enabled: yes
      state: started

  - name: Copy default web page
    copy:
      src: default_page.html
      dest: /usr/share/nginx/html/index.html
      owner: root
      group: root
      mode: 0644

$ ansible-playbook configure_nginx_2.yml

PLAY [Configure NGINX servers] *****************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Install NGINX package] *******************************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Enable and start NGINX service] **********************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Allow SSH traffic through the firewall] **************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Allow HTTPS traffic through the firewall] ************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Enable the firewall] *********************************************************************************************************************
ok: [appserver1.localdomain]
ok: [appserver2.localdomain]

TASK [Copy default web page] *******************************************************************************************************************
changed: [appserver1.localdomain]
changed: [appserver2.localdomain]

PLAY RECAP *************************************************************************************************************************************
appserver1.localdomain     : ok=7    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
appserver2.localdomain     : ok=7    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$
6

Lists and Loops

Sometimes we want to perform a task multiple times with different parameters. We could list each task separately, but that would make the playbook large and clumsy. Some modules allow us to specify parameters as lists. Even if they don't, we can use loops to simplify our playbooks. In previous examples we've seen how to install packages using the "dnf" module. In this example we install multiple packages in a single task by specifying the package names using a list. In this example we set some firewall rules. The "port" parameter doesn't accept a list, but we can use "with_items" to supply a list of parameters. We could make that more complicated by specifying the state for each port in the list. We could use a loop to do the same thing. We may want to define the list as a variable at the top of the playbook, and refer to it later. This also works when using "with_items". The is a lot more to know about loops, which you can read in the documentation here .

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
---
- name: Add basic packages
  hosts: all
  become: true
  tasks:

  - name: Install packages
    dnf:
      name:
        - zip
        - unzip
        - wget
      state: present
      update_cache: yes

---
- name: Configure firewall
  hosts: appservers
  become: true
  tasks:

  - name: Allow web through the firewall
    firewalld:
      port: "{{ item }}"
      permanent: yes
      state: enabled
    with_items:
      - 80/tcp
      - 443/tcp
      - 8080/tcp
      - 8443/tcp

---
- name: Configure firewall
  hosts: appservers
  become: true
  tasks:

  - name: Allow web through the firewall
    firewalld:
      port: "{{ item.port }}"
      permanent: yes
      state: "{{ item.state}}"
    with_items:
      - { port: 80/tcp, state: disabled } 
      - { port: 443/tcp, state: enabled } 
      - { port: 8080/tcp, state: disabled } 
      - { port: 8443/tcp, state: disabled }

---
- name: Configure firewall
  hosts: appservers
  become: true
  tasks:

  - name: Allow web through the firewall
    firewalld:
      port: "{{ item.port }}"
      permanent: yes
      state: "{{ item.state}}"
    loop:
      - { port: 80/tcp, state: disabled } 
      - { port: 443/tcp, state: enabled } 
      - { port: 8080/tcp, state: disabled } 
      - { port: 8443/tcp, state: disabled }

---
- name: Configure firewall
  hosts: appservers
  become: true
  vars:
    fwrules:
      - { port: 80/tcp, state: disabled } 
      - { port: 443/tcp, state: enabled } 
      - { port: 8080/tcp, state: disabled } 
      - { port: 8443/tcp, state: disabled }
  tasks:

  - name: Allow web through the firewall
    firewalld:
      port: "{{ item.port }}"
      permanent: yes
      state: "{{ item.state}}"
    loop: "{{ fwrules }}"

---
- name: Configure firewall
  hosts: appservers
  become: true
  vars:
    fwrules:
      - { port: 80/tcp, state: disabled } 
      - { port: 443/tcp, state: enabled } 
      - { port: 8080/tcp, state: disabled } 
      - { port: 8443/tcp, state: disabled }
  tasks:

  - name: Allow web through the firewall
    firewalld:
      port: "{{ item.port }}"
      permanent: yes
      state: "{{ item.state}}"
    with_items: "{{ fwrules }}"
7

Manage Users and Groups

First we create a playboook called "groups_and_users.yml" with the following contents. We target all hosts in the "databases" group, and use the "group" module to create some common Linux groups. We run the playbook and can see the groups were created. We edit the file and add a task using the "user" module to create a user. We specify the user id and name, but we also need a password. We must hash the password for it to be accepted. It's important to set the "update_password" flag to "user_created", or the password will be reset every time the playbook is run. When we run the playbook the user is created. If we run the playbook again the user is unchanged. Using a plain text password in a playbook is a security risk, so it is better to hash to password separately, and include the hashed password in the playbook, or as a variable. The simplest way to generate the password hash is to use Ansible to do it. The output from the "debug" module displays the hashed password. We can now include the hashed password as a string, or as a variable.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
---
- name: Create groups and users
  hosts: databases
  become: true
  tasks:

  - name: Create groups
    group:
      gid: "{{ item.group_id}}"
      name: "{{ item.group_name}}"
      state: present
    with_items:
      - { group_name: oinstall, group_id: 54321} 
      - { group_name: dba, group_id: 54322} 
      - { group_name: oper, group_id: 54323 }

$ ansible-playbook groups_and_users.yml

PLAY [Create groups and users] *****************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Create groups] ***************************************************************************************************************************
changed: [database1.localdomain] => (item={'group_name': 'oinstall', 'group_id': 54321})
changed: [database1.localdomain] => (item={'group_name': 'dba', 'group_id': 54322})
changed: [database1.localdomain] => (item={'group_name': 'oper', 'group_id': 54323})

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

---
- name: Create groups and users
  hosts: databases
  become: true
  tasks:

  - name: Create groups
    group:
      gid: "{{ item.group_id}}"
      name: "{{ item.group_name}}"
      state: present
    with_items:
      - { group_name: oinstall, group_id: 54321} 
      - { group_name: dba, group_id: 54322} 
      - { group_name: oper, group_id: 54323 }

  - name: Create oracle user
    user:
      uid: 54321
      name: oracle
      password: "{{ 'DummyPassword123' | password_hash('sha512', 'mysecretsalt') }}"
      groups: oinstall,dba,oper
      append: yes
      state: present
      update_password: on_create

$ ansible-playbook groups_and_users.yml

PLAY [Create groups and users] *****************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Create groups] ***************************************************************************************************************************
ok: [database1.localdomain] => (item={'group_name': 'oinstall', 'group_id': 54321})
ok: [database1.localdomain] => (item={'group_name': 'dba', 'group_id': 54322})
ok: [database1.localdomain] => (item={'group_name': 'oper', 'group_id': 54323})

TASK [Create oracle user] **********************************************************************************************************************
changed: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

$ ansible-playbook groups_and_users.yml

PLAY [Create groups and users] *****************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Create groups] ***************************************************************************************************************************
ok: [database1.localdomain] => (item={'group_name': 'oinstall', 'group_id': 54321})
ok: [database1.localdomain] => (item={'group_name': 'dba', 'group_id': 54322})
ok: [database1.localdomain] => (item={'group_name': 'oper', 'group_id': 54323})

TASK [Create oracle user] **********************************************************************************************************************
ok: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

$ ansible all -i localhost, -m debug -a "msg={{ 'DummyPassword123' | password_hash('sha512', 'mysecretsalt') }}"
localhost | SUCCESS => {
    "msg": "$6$mysecretsalt$RP/rxvw0AG/pfo/SLr9LEkQuxGBsFlrfU01cWAcWMxOAA4leVi7j1Y2UzIIU1YyrqyWbpuiE/Ic7efvLJYzaE/"
}
$

---
- name: Create groups and users
  hosts: databases
  become: true
  tasks:

  - name: Create groups
    group:
      gid: "{{ item.group_id}}"
      name: "{{ item.group_name}}"
      state: present
    with_items:
      - { group_name: oinstall, group_id: 54321} 
      - { group_name: dba, group_id: 54322} 
      - { group_name: oper, group_id: 54323 }

  - name: Create oracle user
    user:
      uid: 54321
      name: oracle
      password: "$6$mysecretsalt$RP/rxvw0AG/pfo/SLr9LEkQuxGBsFlrfU01cWAcWMxOAA4leVi7j1Y2UzIIU1YyrqyWbpuiE/Ic7efvLJYzaE/"
      groups: oinstall,dba,oper
      append: yes
      state: present
      update_password: on_create
8

Host Variables

Host variables are variables that have host-specific values. These allow us to write more generic playbooks and roles, to aid in code reusability. We create a directory called "host_vars" in our working directory. We create files in this directory to hold host variables. There is a separate file for each host, with the file name matching the host entry in the inventory, with a file extension of ".yml". For example, in our inventory we have a host called "database1.localdomain", so we create a file called "database1.localdomain.yml" with the following contents. These are variables with values specific to the "database1.localdomain" host. In our working directory we create a playbook called "host_variables.yml" with the following content. The first task uses the "lineinfile" module to add an entry into the "/etc/hosts" file if it is not already present. The line is made up of the "ip_address", "hostname" and "short_hostname" variables from the "database1.localdomain.yml" file. The second task uses the "dnf" module to install the packages listed in the "packages" variable from the "database1.localdomain.yml" file. The third task uses the "firewalld" module to configure the firewall with the ports listed in the "fwrules" variable from the "database1.localdomain.yml" file. The important point is the same playbook can install different packages and configure different firewall rules based on the variables defined for that host. We run the playbook as normal. Most the tasks have already been completed previously, but we can see the "Add hosts entry" task resulted in a change. Ansible has lots of ways to supply variable values. You need to be aware of the Variable Precedence .

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
mkdir -p host_vars

hostname: database1.localdomain
short_hostname: database1
ip_address: 192.168.56.103
packages:
  - zip
  - unzip
  - tar
  - cpio
  - tmux
  - oracle-database-preinstall-19c
fwrules:
  - 22/tcp
  - 1521/tcp

---
- name: Use host variables
  hosts: databases
  become: true
  tasks:

  - name: Add hosts entry
    lineinfile:
      state: present
      dest: /etc/hosts
      line: "{{ ip_address }} {{ hostname }} {{ short_hostname }}"

  - name: Install packages
    dnf:
      name: "{{ packages }}"
      update_cache: yes
      state: latest

  - name: Configure firewall
    firewalld:
      port: "{{ item }}"
      permanent: yes
      state: enabled
    with_items: "{{ fwrules }}"

$ ansible-playbook host_variables.yml

PLAY [Use host variables] **********************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Add hosts entry] *************************************************************************************************************************
changed: [database1.localdomain]

TASK [Install packages] ************************************************************************************************************************
ok: [database1.localdomain]

TASK [Configure firewall] **********************************************************************************************************************
ok: [database1.localdomain] => (item=22/tcp)
ok: [database1.localdomain] => (item=1521/tcp)

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$
9

Group Variables

Group variables are similar to host variables, except they have group-specific values. These allow us to write more generic playbooks and roles, to aid in code reusability. We create a directory called "group_vars" in our working directory. We create files in this directory to hold group variables. There is a separate file for each group, with the file name matching the group entry in the inventory, with a file extension of ".yml". For example, in our inventory we have a group called "appservers", so we create a file called "appservers.yml" with the following contents. These are variables that apply to the whole "appservers" group. In our working directory we create a playbook called "group_variables.yml" with the following content. We just message out the variable value using the "debug" module. We run the playbook as normal, and see the same value appear for all hosts in the group. Ansible has lots of ways to supply variable values. You need to be aware of the Variable Precedence .

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
mkdir -p group_vars

ssh_users: "oracle tim"

---
- name: Use group variables
  hosts: appservers
  tasks:

  - name: Show variable value
    debug:
      var: ssh_users

$ ansible-playbook group_variables.yml

PLAY [Use group variables] *********************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [appserver2.localdomain]
ok: [appserver1.localdomain]

TASK [Show variable value] *********************************************************************************************************************
ok: [appserver1.localdomain] => {
    "ssh_users": "oracle tim"
}
ok: [appserver2.localdomain] => {
    "ssh_users": "oracle tim"
}

PLAY RECAP *************************************************************************************************************************************
appserver1.localdomain     : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
appserver2.localdomain     : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$
10

Handlers

In a previous example we showed how to make one task dependent on the state of another. We register a variable for the task and check for the status of "changed" in the dependant task. In this example we add a line to the "nginx.conf" file if it is not already present. If we make a change we restart the NGINX service. That works fine for one item, but what if there are several tasks that need to trigger the same task later? We can't share the same variable, as we run the risk of overwriting the changed state in a later task. We could register a different variable for each task and alter the "when" in the final task like this. An alternative is to define a handler and notify that handler when it needs to run. It doesn't matter if it is notified one or many times during the lifespan of the playbook, it will only result in a single action. In this example we make two changes to the "nginx.conf" file, and each time notify the "Restart nginx" handler. The handler looks like a regualr task, but it is defined under "handlers". A single task can notify multiple handlers. In this example the task notifies a list of handlers. Alternatively, handlers can listen to generic topics, and tasks can raise those topics to notify multiple handlers. Handlers can be defined separately to the playbook. Create a directory called "handlers" under the working directory, or under the role if the handler is part of a role. Create a file in that directory called "main.yml" with the following contents. Some sources suggest we can remove the handlers from the playbook and the they will be picked automatically from the handlers directory. In the version of Ansible we are using, the handlers file needs to be imported.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
---
- name: Configure appservers
  hosts: appservers
  become: true
  tasks:

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# One silly a line added."
    register: nginx1

  - name: Restart nginx
    service:
      name: nginx
      state: restarted
    when: nginx1.changed

---
- name: Configure appservers
  hosts: appservers
  become: true
  tasks:

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# One silly a line added."
    register: nginx1

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# Another silly a line added."
    register: nginx2

  - name: Restart nginx
    service:
      name: nginx
      state: restarted
    when: nginx1.changed or nginx2.changed

---
- name: Configure appservers
  hosts: appservers
  become: true
  tasks:

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# One silly a line added."
    notify: Restart nginx

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# Another silly a line added."
    notify: Restart nginx

  handlers:
  - name: Restart nginx
    service:
      name: nginx
      state: restarted

---
- name: Configure appservers
  hosts: appservers
  become: true
  tasks:

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# One silly a line added."
    notify:
      - Restart nginx
      - Restart firewall

  handlers:
  - name: Restart nginx
    service:
      name: nginx
      state: restarted

  - name: Restart firewall
    service:
      name: firewalld
      state: restarted

---
- name: Configure appservers
  hosts: appservers
  become: true
  tasks:

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# One silly a line added."
    notify: "restart stuff"

  handlers:
  - name: Restart nginx
    service:
      name: nginx
      state: restarted
    listen: "restart stuff"

  - name: Restart firewall
    service:
      name: firewalld
      state: restarted
    listen: "restart stuff"

- name: Restart nginx
    service:
      name: nginx
      state: restarted
    listen: "restart stuff"

  - name: Restart firewall
    service:
      name: firewalld
      state: restarted
    listen: "restart stuff"

---
- name: Configure appservers
  hosts: appservers
  become: true
  tasks:

  - name: Amend nginx.conf
    lineinfile:
      state: present
      dest: /etc/nginx/nginx.conf
      line: "# One silly a line added."
    notify: "restart stuff"

  handlers:
  - import_tasks: handlers/main.yml
11

Templates

Templates are files we can customise using the Jinja2 templating language to provide host-specific variations of a common file. This is really useful for customising configuration files that require host-specific values. Create a directory called "templates" in the working directory, or under the role if the template is part of a role. Create a file called "99-my-sshd-users.conf.j2" in the "templates" directory with the following contents. The ".j2" extension stands for Jinja2. Edit the "host_vars/database1.localdomain.yml" file, adding the new host variable "ssh_users" with a space separated list of users. We create a playbook called "templates.yml" with the following contents. We process the template and copy it to create a new config file. If the state has changed, we run a handler to restart the "sshd" service. We run the playbook and we can see the "Generate 99-my-sshd-users.conf file" task and "Restart sshd" handler ran. We can see the file is created on the host, and the host variable has been substituted into the file.

Code/Command (click line numbers to comment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
mkdir templates

AllowUsers {{ ssh_users }}

ssh_users: "oracle tim"

---
- name: Configure databases
  hosts: databases
  become: true
  tasks:

  - name: Generate 99-my-sshd-users.conf file
    template:
      src: 99-my-sshd-users.conf.j2
      dest: /etc/ssh/ssh_config.d/99-my-sshd-users.conf
      group: root
      owner: root
      mode: 0644
    notify: Restart sshd

  handlers:
  - name: Restart sshd
    service:
      name: sshd
      state: restarted

$ ansible-playbook templates.yml

PLAY [Configure databases] *********************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************
ok: [database1.localdomain]

TASK [Generate 99-my-sshd-users.conf file] *****************************************************************************************************
changed: [database1.localdomain]

RUNNING HANDLER [Restart sshd] *****************************************************************************************************************
changed: [database1.localdomain]

PLAY RECAP *************************************************************************************************************************************
database1.localdomain      : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$

[root@database1 ssh_config.d]# cat 99-my-sshd-users.conf
AllowUsers oracle tim
[root@database1 ssh_config.d]#
12

Roles

Roles allow us to split Ansible playbooks into more manageable chunks, rather than having one monolithic playbook. They also aid in code reusability. You can read more about roles here. - Ansible : Roles - First Steps
13

Vault

Ansible Vault provides a simple way to encrypt secrets, so you don't expose sensitive data in your playbooks. - Ansible : Vault For more information see: - Intro to playbooks - ansible-playbook - Index of all Modules - Loops - Ansible Playbooks : Introduction - Ansible Playbooks : Lists and Loops - Ansible Playbooks : Host and Group Variables - Ansible Playbooks : Handlers - Ansible Playbooks : Files and Templates - Ansible Playbooks : Tags - Ansible Playbooks : Users and Groups - Ansible Playbooks : Roles - Ansible Playbooks : Vault - Ansible : First Steps - Ansible : Roles - First Steps - Ansible : Vault - Ansible : All Articles Hope this helps. Regards Tim...

Comments (0)

Please to add comments

No comments yet. Be the first to comment!