Skip to content

Instantly share code, notes, and snippets.

@douglasmiranda
Last active September 12, 2021 15:13
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save douglasmiranda/f21a4481d372ae54fcf4a6ff32249949 to your computer and use it in GitHub Desktop.
Save douglasmiranda/f21a4481d372ae54fcf4a6ff32249949 to your computer and use it in GitHub Desktop.
Ansible: Notes, Errors and Solutions

Ansible for Configuration Management

I'm using Ansible only for Configuration Management, the server is up and I want to configure users, install packages and configure them.

For infrastructure provisioning terraform.io is nice!

Currently, my deployment flow includes Drone.io/GitlabCI for CI/CD and Docker Swarm for orchestrating containers.

Tasks

How to get host information inside a task

Ansible will discover some information about the hosts when running, and you can access like a variable, they are defined by default.

If you run this simple task:

- debug: var=hostvars

# OR

- debug: var=ansible_facts

I won't paste the output here, but you can see here.

Check how you can access the variables.

How to get the current hostname as in hosts/hosts.yml file

The variable you're looking for is: {{ inventory_hostname }}.

"Accessing information about other hosts with magic variables"

How to apply tags to all imported tasks from file

Let's say you want to import tasks/users.yml in your tasks/main.yml, you can simply do:

- name: Import user management tasks
  include_tasks: users.yml
  tags:
    - "initial-setup"

But if you run:

ansible-playbook site.yml -i hosts.yml --tags=initial-setup

You'll see that you still need to add the tag initial-setup to all tasks in tasks/users.yml in order to run only tasks with our chosen tag.

But if you want to import/include a file and apply a tag to all tasks without manually adding tags to several tasks, just use args: apply: tags:

- name: Import user management tasks
  include_tasks: users.yml
  tags:
    - initial-setup
  # This let's me apply tags to all tasks imported with `include_tasks`
  args:
    apply:
      tags:
        - initial-setup

Now when you run playbooks with --tags=initial-setup it will execute the task "Import user management tasks" and every task you're including from tasks/users.yml.

Using tags:

More use cases of args:

Users, Ansible Vault and Password Stuff

Use the secrets you stored in Ansible Vault in your Ansible Playbooks

ansible-playbook site.yml -i hosts.yml --ask-vault-pass

If using encrypted files you can encrypt one of those vars/main.yml, host_vars/hostname.yml in order to have variables automatically available in your tasks.

Single encrypted variables are nice but refer to Error & Solutions below because there's a catch.

My user needs to be able to use sudo

Add the user to sudo group.

- name: "Create user"
  user:
    name: "myuser"
    groups:
      - "sudo"
    # ...

Note: you'll find online ways to add user to sudoers file, if you want to do that way.

My ansible user needs a sudo password in order to execute priviledged tasks

Check out priviledge escalation: https://docs.ansible.com/ansible/latest/user_guide/become.html

  • You could run ansible-playbook with --ask-become-pass flag.
  • Or define ansible_become_pass in your hosts configuration.

https://docs.ansible.com/ansible/latest/user_guide/become.html#passwords-for-enable-mode

hosts.yml

all:
  hosts:
    host_a:
      ansible_connection: ssh
      ansible_user: host_a
      ansible_become_pass: "{{ host_a_password }}"
      ansible_host: 111.11.111.111
      ansible_port: 22

host_vars/host_a.yml

host_a_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653236336462626566653063336164663966303231363934653561363964363833313662
          6431626536303530376336343832656537303632313433360a626438346336353331386135323734
          62656361653630373231613662633962316233633936396165386439616533353965373339616234
          3430613539666330390a313736323265656432366236633330313963326365653937323833366536
          34623731376664623134383463316265643436343438623266623965636363326136

Note: Don't forget to check ansible-playground --help there is a lot more you can do with just flags:

  Connection Options:
    control as whom and how to connect to hosts

    -k, --ask-pass      ask for connection password
    --private-key=PRIVATE_KEY_FILE, --key-file=PRIVATE_KEY_FILE
                        use this file to authenticate the connection
    -u REMOTE_USER, --user=REMOTE_USER
                        connect as this user (default=None)
    -c CONNECTION, --connection=CONNECTION
                        connection type to use (default=smart)
    -T TIMEOUT, --timeout=TIMEOUT
                        override the connection timeout in seconds
                        (default=10)
    --ssh-common-args=SSH_COMMON_ARGS
                        specify common arguments to pass to sftp/scp/ssh (e.g.
                        ProxyCommand)
    --sftp-extra-args=SFTP_EXTRA_ARGS
                        specify extra arguments to pass to sftp only (e.g. -f,
                        -l)
    --scp-extra-args=SCP_EXTRA_ARGS
                        specify extra arguments to pass to scp only (e.g. -l)
    --ssh-extra-args=SSH_EXTRA_ARGS
                        specify extra arguments to pass to ssh only (e.g. -R)

Setting password for a unix user

When creating users you have to provide a hashed password, not the plaintext version. (don't forget to use Ansible Vault to store secrets)

You could use things like mkpasswd if available, or even Python snnipets you find online like:

python -c "from passlib.hash import sha512_crypt; import getpass; print(sha512_crypt.hash(getpass.getpass()))"

But if you're templating, you can use the jinja filter password_hash:

{{ password | password_hash('sha512') }}

Create a user and copy the authorized_keys to the new user

Let's say user root is my current user.

NOTE: You can do a lot with the authorized key module, so it's worth to check it out:

- name: "Create main user"
  user:
    # define user attrs here

- name: "Ensure .ssh dir for our new user is present"
  file:
    state: directory
    path: /home/my_new_user/.ssh/
    owner: my_new_user
    group: my_new_user
    mode: 0700

- name: "Copy authorized_keys from root to new user"
  copy:
    remote_src: "yes"
    # root authorized_keys file
    src: "/root/.ssh/authorized_keys"
    dest: "/home/my_new_user/.ssh/authorized_keys"
    owner: my_new_user
    group: my_new_user
    mode: 0600
  tags:
    - initial-setup

Disallow root SSH access and disallow password authentication

For more secure access only use SSH Key-Based authentication.

- name: "Disallow password authentication"
  lineinfile:
    dest: "/etc/ssh/sshd_config"
    regexp: "^PasswordAuthentication"
    line: "PasswordAuthentication no"
    state: "present"
  notify: "Restart ssh"

- name: "Disallow root SSH access"
  lineinfile:
    dest: "/etc/ssh/sshd_config"
    regexp: "^PermitRootLogin"
    line: "PermitRootLogin no"
    state: "present"
  notify: "Restart ssh"

UFW Basics

UFW (Uncomplicated Firewall) helps you to not going crazy with iptables stuff.

Some basic tasks you may find useful:

# Denying all incoming and allowing all outgoing connections.
# So we can specify later what incoming to allow.
- name: "Configure ufw defaults"
  ufw:
    direction: "{{ item.direction }}"
    policy: "{{ item.policy }}"
  with_items:
    - { direction: 'incoming', policy: 'deny' }
    - { direction: 'outgoing', policy: 'allow' }
  notify:
    - "Restart ufw"

# That's a pretty common task for a developer:
# - allow ssh conections
# - allow incoming traffic on 80 (http)
# - allow incoming traffic on 443 (https)
- name: "Configure access for common ports ssh/http/https"
  ufw:
    rule: "{{ item.rule }}"
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
  with_items:
    - { rule: 'limit', port: '{{ ssh_port | default("22") }}', proto: 'tcp' }
    - { rule: 'allow', port: '80', proto: 'tcp' }
    - { rule: 'allow', port: '443', proto: 'tcp' }
  notify:
    - "Restart ufw"

- name: "Enable ufw logging"
  ufw:
    logging: on
  notify:
    - "Restart ufw"

- name: "Start and enable ufw service"
  ufw:
    state: "enabled"

Define a simple handler for restarting ufw every time you make changes:

- name: "Restart ufw"
  service:
    name: "ufw"
    state: "restarted"

UFW and Docker

If you need to configure ufw rules for your Swarm Manager:

- name: "Configure access for Swarm Manager host"
  ufw:
    rule: "{{ item.rule }}"
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
  with_items:
    - { rule: 'allow', port: '2376', proto: 'tcp' }
    - { rule: 'allow', port: '2377', proto: 'tcp' }
    - { rule: 'allow', port: '7946', proto: 'tcp' }
    - { rule: 'allow', port: '7946', proto: 'udp' }
    - { rule: 'allow', port: '4789', proto: 'udp' }
  notify:
    - "Restart ufw"

If you need to configure ufw rules to allow your Swarm Worker:

- name: "Configure access for Swarm Worker host"
  ufw:
    rule: "{{ item.rule }}"
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
  with_items:
    - { rule: 'allow', port: '2376', proto: 'tcp' }
    - { rule: 'allow', port: '7946', proto: 'tcp' }
    - { rule: 'allow', port: '7946', proto: 'udp' }
    - { rule: 'allow', port: '4789', proto: 'udp' }
  notify:
    - "Restart ufw"

IMPORTANT NOTE:

If you don't know already Docker doesn't respect UFW all the time, since Docker can manage iptables just like UFW does, it can interfere with rules previously applied by ufw.

If you search online for "ufw docker" you'll discover a lot of discussions.

You may want to try this one, the README is very informative:

APT

Ansible hangs when using apt module

Most of the times this happened when upgrading with apt-get upgrade:

- name: "Upgrade packages to the latest version available"
  apt:
    upgrade: "safe"

On Google you'll see that there's no definitive answer, so if you have to cancel the task and you get stuck even when doing manually now remember to kill the apt/aptitude/dpkg that are still running:

Check if they are running:

ps aux

# OR

ps aux | grep "aptitude"

Kill them!

pkill aptitude
pkill dpkg

Install multiple packages in one task

# Define your variable
common_packages:
  - htop
  - unattended-upgrades
  - fail2ban
  - ufw

# and then in your tasks:
- name: "Install common packages"
  apt:
    state: "present"
    pkg: "{{ common_packages }}"

You don't need to use loops anymore.

Keep your packages installed with apt always up-to-date

There are some Linus distributions with automatic updates built-in, like CoreOS. If we're using Debian, we can make this happen too, with Unattended Upgrades:

- name: "Install unattended-upgrades package"
  apt:
    state: "present"
    pkg: "unattended-upgrades"

- name: Adjust APT update intervals
  copy:
    content: |
      APT::Periodic::Update-Package-Lists "1";
      APT::Periodic::Download-Upgradeable-Packages "1";
      APT::Periodic::AutocleanInterval "7";
      APT::Periodic::Unattended-Upgrade "1";
    dest: /etc/apt/apt.conf.d/10periodic

Styleguide

  • Two spaces identation
  • Snake case for variables (variable_name)
  • Always quote strings
  • Name your hosts with _ instead of - (myhost_web), so you don't face issues when using in variables.
  • roles/x/vars/main.yml are a nice place for variables, unless you have a lot of global/shared vars then group_vars and host_vars are the way to go
  • Multiline strings
  • Stick with true and false for booleans
  • DON'T do that one line nonsense: file: 'path=x state=file mode=0755 owner=me group=me', vertical files are better for reading
  • Don't go crazy on many variables override, cause you can go insane with variable precedence
  • You may want to add to your .gitignore: site.retry

Errors & Solutions

Trying to apply some jinja2 pipeline functions/filters to encrypted inline var (ansible-vault)

You have an inline encrypted variable, not an encrypted file vault, and you want to transform that value with using some jinja2 filters using the pipe |.

In my case, I was trying to use the jinja2 filter password_hash in my_password variable.

vars/main.yml

mysecret: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653236336462626566653063336164663966303231363934653561363964363833313662
          6431626536303530376336343832656537303632313433360a626438346336353331386135323734
          62656361653630373231613662633962316233633936396165386439616533353965373339616234
          3430613539666330390a313736323265656432366236633330313963326365653937323833366536
          34623731376664623134383463316265643436343438623266623965636363326136

When using mysecret I was simply doing something like {{ devtools_password | password_hash('sha512') }}.

And the error was something like:

fatal: [remote]: FAILED! => {"msg": "Unexpected templating type error occurred on ({{ mysecret | password_hash('sha512') }}): secret must be unicode or bytes, not ansible.parsing.yaml.objects.AnsibleVaultEncryptedUnicode"}

The solution was to "force" Ansible to decrypt the variable so I can apply filters.

Check my tasks/users.yml:

- name: "Create main user"
  user:
    name: "myuser"
    password: "{{ '%s' | format(mysecret) | password_hash('sha512') }}"
    # ...

That probably is some problem with lazy evaluation or stuff like that.

More:

Copy link

ghost commented Nov 25, 2019

Thank you for the Errors & Solutions section, I was doing the same exact thing with the password_hash and vault.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment