Difference between revisions of "Ansible"

From Indie IT Wiki
Line 21: Line 21:
 
  source ~/.bashrc
 
  source ~/.bashrc
 
  ansible-galaxy collection install community.docker
 
  ansible-galaxy collection install community.docker
 +
 +
If you receive the error ...
 +
 +
/usr/bin/python3: No module named pip
 +
 +
... then install pip as follows ...
  
 
== Upgrade ==
 
== Upgrade ==

Revision as of 06:11, 19 August 2024

Introduction

Ansible is an open-source software provisioning, configuration management, and application-deployment tool enabling infrastructure as code.

It is used to deploy and maintain many servers at once, from the command line.

  • Control Node = Admin Computer
  • Host Node = Remote Server

How To Install and Configure Ansible on Ubuntu 20.04

Requirements

Python 2.6+ or Python 3.5+

Installation

sudo apt remove ansible
python3 -m pip install ansible
echo "export PATH=$PATH:$HOME/.local/bin" >> ~/.bashrc
source ~/.bashrc
ansible-galaxy collection install community.docker

If you receive the error ...

/usr/bin/python3: No module named pip

... then install pip as follows ...

Upgrade

python3 -m pip install --upgrade ansible ansible-core

Configuration

Add your hosts...

sudo nano /etc/ansible/hosts

[servers]
myserver ansible_host=123.456.789.0

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Check the config...

ansible-inventory --list -y

all:
  children:
    servers:
      hosts:
        myserver:
          ansible_host: 123.456.789.0
          ansible_python_interpreter: /usr/bin/python3
    ungrouped: {}

Check the config with hosts file specified...

ansible-inventory --list -y -i ./inventory/hosts

Using an Ansible playbook with an SSH bastion / jump host

Add the following configuration inside your ~/.ssh/config file:

Host bastion
  User username
  Hostname bastion.example.com

Host private-server-*.example.com
  ProxyJump bastion

https://www.jeffgeerling.com/blog/2022/using-ansible-playbook-ssh-bastion-jump-host

Testing

List Hosts

./inventory/hosts

[servers]
server1 ansible_connection=ssh ansible_user=username1
domain.uk ansible_connection=ssh ansible_user=username2
nas ansible_connection=ssh ansible_user=username3
ansible -i ./inventory/hosts servers --list-hosts

 hosts (3):
   server1
   domain.uk
   nas

Ping

ansible -i ./inventory/hosts servers -m ping
ansible -i ./inventory/hosts server1.mydomain.com -m ping

Echo

Get all hosts to output a word on the command line...

ansible all -a "/bin/echo hello" -i ./inventory/hosts

Uptime

Uptime for a single host...

ansible hostname -m command -a "uptime" -i ./inventory/hosts

Uptime for all hosts in a group...

ansible groupname -m command -a "uptime" -i ./inventory/hosts

Uptime for all hosts...

ansible all -m command -a "uptime" -i ./inventory/hosts

AWS SSH Key

ansible all -m ping -u ubuntu --private-key=~/.ssh/myserver.pem

myserver | SUCCESS => {
   "changed": false,
   "ping": "pong"
}

Commands

You can perform one off commands on a single host or multiple hosts.

Hostname

ansible all -u ubuntu --private-key=~/.ssh/myserver.pem -a "hostname -f"

Update Package Cache

ansible all -u ubuntu --private-key=~/.ssh/myserver.pem -a "sudo apt-get update"

Playbooks

Playbooks are YAML text files which contain commands and options in a text file, just like a docker compose file.

The file contains 'modules' which perform different tasks.

Examples

Add line to end of text file

- hosts: all
  tasks:
    - name: insert a line at the end of a file
      lineinfile:
        path: /to/the/file.txt
        line: last
    - name: check file
      debug: msg="{{ item }}"
      with_lines: tail -n1 /to/the/file.txt

See if hosts are alive by using only Gather Facts

99_gather.yml

- name: gather facts
  become: true
  hosts: all

... and run it on all but 1 host (by using the limit option with an exclamation mark (!host) (!host1:!host2)...

ansible-playbook -i ~/Bin/ansible-homelab/inventory/hosts ~/Bin/ansible-homelab/playbooks/ubuntu/99_gather.yml --limit '!server1'

Update Package List and Upgrade System

Create hosts file...

./inventory/hosts

[servers]
server1 ansible_connection=ssh ansible_user=user1
server2 ansible_connection=ssh ansible_user=user2
server3 ansible_connection=ssh ansible_user=user3

Check the list of hosts is readable..

ansible -i ./inventory/hosts all --list-hosts

Create the playbook file...

./playbooks/apt.yml

- hosts: "*"
  become: yes
  tasks:
    - name: apt
      apt:
        update_cache: yes
        upgrade: 'yes'

Run the playbook command...

ansible-playbook ./playbooks/apt.yml -i ./inventory/hosts

To run this playbook on a single host or multiple hosts, you would use the -l (limit) option at the end of the command line...

ansible-playbook ./playbooks/apt.yml -i ./inventory/hosts -l server1
ansible-playbook ./playbooks/apt.yml -i ./inventory/hosts -l server1,server2

Create single directory using the 'file' module

Create the playbook...

nano mkdir.yml

- hosts: all
  tasks:
  - name: Ansible file module create directory
    file:
      path: ~/backups
      state: directory

Run the playbook (dry run)...

ansible-playbook -C mkdir.yml

Run the playbook...

ansible-playbook mkdir.yml

Create multiple directories using the 'file' module

Create the playbook...

nano mkdirs.yml

- hosts: all
  tasks:
  - name: Ansible create multiple directories with_items    
    file:
      path: ~/backups/{{item}}
      state: directory
      with_items:
      - 'mysql'      
      - 'repository'
      - 'config'

Run the playbook...

ansible-playbook mkdirs.yml

https://linuxhint.com/create-directory-ansible/

Install Apache web server software and start it on a Red Hat based system

- name: Playbook
  hosts: webservers
  become: yes
  become_user: root
  tasks:
    - name: ensure apache is at the latest version
      yum:
        name: httpd
        state: latest
    - name: ensure apache is running
      service:
        name: httpd
        state: started

Touch a file based on the date of another file

- hosts: alpine
  become: yes
  tasks:
  - name: Get stats of a file
    ansible.builtin.stat:
      path: /usr
    register: usr
  - name: Print a debug message
    ansible.builtin.debug:
      msg: "Path exists and is a directory"
    when: usr.stat.isdir is defined and usr.stat.isdir
  - name: Print a debug message
    ansible.builtin.debug:
      msg: "ctime is {{ usr.stat.ctime }}"
  - name: touch file /root/misc/system_installed
    file:
      path: /root/misc/system_installed
      state: touch
      modification_time: '{{ "%Y%m%d%H%M.%S" | strftime(usr.stat.ctime) }}'

Copying SSH Keys

https://medium.com/@visualskyrim/ansible-playbook-deploy-the-public-key-to-remote-hosts-da3f3b4b5481

Advanced Usage

Blocks, Rescue and Always

The tasks in the block execute normally. If any tasks in the block return failed, the rescue section executes tasks to recover from the error. The always section runs regardless of the results of the block and rescue sections.

- name: Attempt and graceful roll back demo

  block:
    - name: Print a message
      ansible.builtin.debug:
        msg: 'I execute normally'

    - name: Force a failure
      ansible.builtin.command: /bin/false

    - name: Never print this
      ansible.builtin.debug:
        msg: 'I never execute, due to the above task failing, :-('
  rescue:
    - name: Print when errors
      ansible.builtin.debug:
        msg: 'I caught an error'

    - name: Force a failure in middle of recovery! >:-)
      ansible.builtin.command: /bin/false

    - name: Never print this
      ansible.builtin.debug:
        msg: 'I also never execute :-('
  always:
    - name: Always do this
      ansible.builtin.debug:
        msg: "This always executes"

https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html

Example - Set up a server Firewall

This is good when you want to set up a server's firewall using the UFW module. It prevents you from accidentally blocking yourself out of a server :-)

- name: Setup admin production UFW
 
  hosts: my_hosts
  gather_facts: no
 
  tasks:
 
    - block:
      - name: Reset UFW
        ufw: 
          state: reset
 
      - name: Allow outgoing
        ufw: 
          default: allow
          direction: outgoing
 
      - name: Disallow ingoing
        ufw: 
          default: deny
          direction: incoming
 
      - name: Establish regular admin rules
        ufw:
          rule: allow
          direction: in
          port: '5432'
          proto: tcp
          from_ip: '{{ item }}'
        loop:
          - 123.456.789/32
          - 987.654.321/32
 
      always:
        - name: Grant ssh access
          ufw:
            rule: allow
            direction: in
            port: '22'
            proto: tcp
            from_ip: 321.456.987/32
         
         - name: Enable Firewall
           ufw: 
            state: enabled

Example - Add Entry To Hosts File

This is useful for testing new web sites and for deploying docker containers in cinjunction with Traefik.

ansible-playbook --extra-vars "ip_address=192.168.0.189 host_name=example5 domain_name=domain.xyz db_name=example5domainxyz" 99_add_to_etc_hosts.yml

99_add_to_etc_hosts.yml

- name: add entry to computer hosts file
  hosts: localhost
  become: yes
  gather_facts: no

  tasks:

  - name: check variables
    debug:
      msg: "{{ ip_address }} {{ host_name}}.{{domain_name }}"

  - name: pausing to check domain_name variable
    pause:
      seconds: 10

  - name: insert a line at the end of the file
    lineinfile:
      path: /etc/hosts
      line: "{{ ip_address }} {{ host_name }}.{{ domain_name }}"

  - name: check file
    debug: msg="{{ item }}"
    with_lines: tail -n1 /etc/hosts

Do Release Upgrade

IT DOES WORK BUT USE WITH EXTREME CAUTION

https://www.jeffgeerling.com/blog/2018/ansible-playbook-upgrade-all-ubuntu-1204-lts-hosts-1404-or-1604-1804-etc

Variables

Standard variables

{{ ansible_facts["eth0"]["ipv4"]["address"] }}

https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html

Custom variables

Example 1 - run the WordPress command line tool to install or update the software...

ansible-playbook wp.yml --extra-vars "major=true installcli=true"

wp.yml

- name: Install WordPress Cli
  register: wpclidonwloaded
  become: yes
  when: installcli is defined
  get_url: 
    url: https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
    dest: "{{ wpclipath }}"
    mode: '0755'
    owner: "{{ wpcliuser }}"
    group: "{{ wpcligroup }}"

- name: Update WordPress Core (Major version)
  command: "{{ wpclipath }} core update"
  when: major is defined
  args:
    chdir: '{{ projects[inventory_hostname].blog_folder }}'

Example 2 - create a folder from a one-off command line custom variable...

ansible-playbook website.yml --extra-vars "domain=mybusiness.com"

website.yml

- name: create directory for web site
  file:
    path: "/var/www/{{ domain }}"
    state: directory

Advanced variables

Using variables for Docker container labels (for Traefik)

Here, you can pass the extra variables via the playbook command line and then that is used to build the docker container labels.

I won't lie, I spent 6 hours on this, and it was all solved by a jinja2 option 'items2dict' :)

Now, you too can create Docker containers which Traefik will dynamically add to the hosts because of the labels!

ansible-playbook --extra-vars "host_name=example4 domain_name=domain.xyz db_name=example4domainxyz" 99_dictionary_test_2.yml
- name: Dynamic dict
  hosts: localhost
  gather_facts: false

  vars:
    ID:
      - "{{ db_name }}"

    my_attributes:
      - key: "traefik.enable"
        value: "true"
      # - key: "traefik.http.routers.{{ ID[0] }}.entrypoints"
      - key: "traefik.http.routers.{{ db_name }}.entrypoints"
        value: "web"
      # - key: "traefik.http.routers.{{ ID[0] }}.rule"
      - key: "traefik.http.routers.{{ db_name }}.rule"
        value: "Host(`{{ host_name }}.{{ domain_name }}`)"

  tasks:
    - name: construct a dynamic dict and debug
      vars:
        manager_attributes: "{{ my_attributes | items2dict }}"
      debug:
        var: manager_attributes

    - name: pausing to check
      pause:
        seconds: 10

    - name: deploy nginx docker container with custom labels
      community.docker.docker_container:
        name: "{{ db_name }}"
        image: nginx
        labels: "{{ my_attributes | items2dict }}"
        networks:
          - name: traefik_default
        state: started

Troubleshooting

ERROR: [WARNING]: sftp transfer mechanism failed on

[WARNING]: sftp transfer mechanism failed on [130.130.0.232]. Use ANSIBLE_DEBUG=1 to see detailed information

https://fantashit.com/warning-sftp-transfer-mechanism-failed/