Ansible Installation (Debian) #
Official Documentation: https://docs.ansible.com/ansible/latest/installation_guide/installation_distros.html#installing-ansible-on-debian
# Install requirements
sudo apt install gnupg -y
Ansible installation script:
# Export Ubuntu version:
UBUNTU_CODENAME=jammy # Ubuntu 22
UBUNTU_CODENAME=noble # Ubuntu 24
# Download and add the Ansible GPG key
wget -O- "https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=get&search=0x6125E2A8C77F2818FB7BD15B93C4A3FD7BB9C367" \
| sudo gpg --dearmour -o /usr/share/keyrings/ansible-archive-keyring.gpg
# Add Ansible repository
echo "deb [signed-by=/usr/share/keyrings/ansible-archive-keyring.gpg] http://ppa.launchpad.net/ansible/ansible/ubuntu $UBUNTU_CODENAME main" \
| sudo tee /etc/apt/sources.list.d/ansible.list
# Install Ansible
sudo apt update && sudo apt install ansible -y
# Verify installation / list version
ansible --version
Ansible Playground #
File & Folder Structure #
# The file and folder structure looks like this:
ansible-playground
├── ansible.cfg
├── inventory
├── playbooks
│ └── example_playbook.yml
└── roles
└── example_role
└── tasks
└── main.yml
# Create the folder structure
mkdir -p \
ansible-playground/playbooks \
ansible-playground/roles/example_role/tasks \
&& cd ansible-playground
Ansible Configuration #
- ansible.cfg
[defaults]
roles_path = ./roles
Inventory File #
- inventory
[example_hosts]
192.168.30.161 ansible_user=debian
Example Playbook #
- playbooks/example_playbook.yml
---
- name: Example Playbook
hosts: example_hosts
become: true
gather_facts: true
roles:
- example_role
Example Role #
-
roles/example_role/tasks/main.yml
-
Add the tasks to the
main.yml
file. -
All tasks were tested on Debian 12 server.
Task: Create User #
- This task creates a new user with home directory, adds the user to the sudo and docker group, allowes the user to uses sudo without password and adds a public SSH key to the “~/.ssh/authorized_keys” file of the user.
---
- name: Ensure user exists
ansible.builtin.user:
name: example-user
shell: /bin/bash
home: /home/example-user
groups: sudo, docker
append: yes # Add user to groups, keep user in any other groups the user might already be part of
expires: -1 # No expiration
state: present
create_home: yes
- name: Add public SSH key to authorized_keys
ansible.posix.authorized_key:
user: example-user
state: present
key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC56iSOcPyN6CGW4RAzcv7w1YxO3wJMY9mC1lEAT5W6SgnjiUjQFstQyNCJJcrPNHYF1sjMfbN8lThY/b007fwVh9XQX7tnVZ6rQ5CU1VrmgMzyMPkcLPoIaVTWSggDRu/PUtrBtcURje+HZQno9oRYyA768l6acG0PSHwftRyyKcJ4TE/4Iv6tmQPXi5HKt4aIRy1MYqtMR3FcKd+enCx0I9TM1+eSL/vxfYS+e2MZJwnz/Zl8HJxBg/XRfti7FIydy2CvPszQkIvFrMpP8BQhvNHtIWDzooabLnxuAv7//xlWwwC6GXBeQ+UxWmn6VUexpN6Hr0WUqI7ErmODhPLqzXtOPHhfq8MC2SAtZxx9+b+yjpjT0rlDnm1Fv9MWi0kEiYiYnJxsJZZiEKAxwUSb0pz/uneU1Cj66A6xoFNCwG3H6bW7gySGtKWtjrjM2EmwzRAvA7h4xZufLns0tPx4MhR9AaounWpdj1vaJhJiHAACG2scmJMjUYhMTuY9lIUUW9viak/z8VlBsKCgve+oSRZrXg84kleHR8eREeAtDwQdPEtxXgNID2B9v2XwDYVo3Lgl41tSiloA7TU/49GG6SvCsyVfW1r2AdlI/QBoncoP3OxixxfDNauVg7omJ0W8Fh1MSb7h+G5Hq8mV2l8L1s75yPTVnmOrMt3jfIjlEQ== debian@debansible01"
- name: Allow user to use sudo without a password
ansible.builtin.copy:
dest: /etc/sudoers.d/example-user
content: "example-user ALL=(ALL) NOPASSWD:ALL\n"
mode: '0440'
owner: root
group: root
Task: Delete User #
Delete the user but keep the users home directory:
---
- name: Remove user account and home directory
ansible.builtin.user:
name: example-user
state: absent
Delete the user and the users home directory:
---
- name: Remove user account and home directory
ansible.builtin.user:
name: example-user
state: absent
remove: yes # Delete home directory
Task: Create Group #
---
- name: Ensure group exists
ansible.builtin.group:
name: example-group
gid: 1010
state: present
Task: Create Directory #
---
- name: Ensure directory exists
ansible.builtin.file:
path: /opt/example-directory
state: directory
owner: debian
group: debian
recurse: yes
become: true
Task: Create File with Content #
---
- name: Ensure file exists
ansible.builtin.copy:
dest: /tmp/example-file
content: |
some text
more text
owner: debian
group: debian
Task: Create Symlink #
- name: Create symlink
ansible.builtin.file:
src: /tmp/some-script
dest: /usr/local/bin/some-script
state: link
become: true
Task: Copy Static File #
# The file and folder structure looks like this:
ansible-playground
├── ansible.cfg
├── inventory
├── playbooks
│ └── example_playbook.yml
└── roles
└── example_role
├── files
│ └── example-file.txt
└── tasks
└── main.yml
# Create the folder structure
mkdir -p roles/example_role/files
# Create static file that will be copied
touch roles/example_role/files/example-file.txt
- roles/example_role/tasks/main.yml
- name: configure gitlab docker dnd runner
copy:
src: example-file.txt
dest: /tmp/destination-file.txt
owner: debian
group: debian
mode: "600"
Task: Copy Several Static Files #
# The file and folder structure looks like this:
ansible-playground
├── ansible.cfg
├── inventory
├── playbooks
│ └── example_playbook.yml
└── roles
└── example_role
│ ├── files
│ │ ├── file1.txt
│ │ ├── file2.txt
│ │ └── file3.txt
└── tasks
└── main.yml
# Create the folder structure
mkdir -p roles/example_role/files
# Create static file that will be copied
touch roles/example_role/files/file1.txt
touch roles/example_role/files/file2.txt
touch roles/example_role/files/file3.txt
- roles/example_role/tasks/main.yml
---
- name: Ensure files exist
copy:
src: "{{ item }}"
dest: "/tmp/{{ item }}"
loop:
- file1.txt
- file2.txt
- file3.txt
Task: Copy File from Template #
# The file and folder structure looks like this:
ansible-playground
├── ansible.cfg
├── inventory
├── playbooks
│ └── example_playbook.yml
└── roles
└── example_role
├── tasks
│ └── main.yml
└── templates
└── config.toml.j2 # template file
# Create the folder structure
mkdir -p roles/example_role/templates
Create the template file:
- roles/example_role/templates/config.toml.j2
concurrent = 5
check_interval = 0
shutdown_timeout = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "example-runner"
url = {{ gitlab_runner_url }}
id = 1
token = {{ gitlab_runner_token }}
executor = "docker"
Define the varibales in the playbook:
- playbooks/example_playbook.yml
---
- name: Example Playbook
hosts: example_hosts
become: true
gather_facts: true
# Define the variables that are used by the template
vars:
gitlab_runner_url: "https://gitlab.example.com/"
gitlab_runner_token: "SOME-TOKEN"
roles:
- example_role
Create the task thak copies the file from the template:
- roles/example_role/tasks/main.yml
---
- name: Ensure GitLab Runner config directory exists
file:
path: /etc/gitlab-runner
state: directory
owner: root
group: root
mode: '0755'
- name: Deploy GitLab Runner config from template
template:
src: config.toml.j2
dest: /etc/gitlab-runner/config.toml
owner: root
group: root
mode: '0644'
Task: Clone Git Repository #
The following task clones a git repository from GitHub and pulls the latest version, every time the playbook triggers the task:
---
- name: Ensure git repository exists
ansible.builtin.git:
repo: https://github.com/octocat/Hello-World.git
dest: /tmp/github-repository # The content of the git repo gets cloned into this directory
version: master
update: yes
clone: yes
force: yes
become_user: debian
Task: Download File (if it does not exist) #
The following task checks if a files exists locally and if not downloads it:
---
## Verify if script exists locally
- name: Verify if script exists locally
stat:
path: /tmp/gitlab-runner.script.deb.sh
register: example_script
# Fetch script if it does not exist locally
- name: Fetch script if it does not exist locally
ansible.builtin.get_url:
url: https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh
dest: /tmp/gitlab-runner.script.deb.sh
mode: "0744"
when: not example_script.stat.exists
Note: In this example it would make more sense to reference the path, that is created via the script when it is later executed, like /etc/apt/sources.list.d/runner_gitlab-runner.list
.
Task: Install a Package & Packages #
Install a single package:
- name: Ensure package is installed
ansible.builtin.apt:
name: gitlab-runner
state: latest
update_cache: true
cache_valid_time: 0
Install several packages:
---
- name: Ensure packages are installed
ansible.builtin.apt:
name: "{{ item }}"
state: latest
update_cache: true
cache_valid_time: 0
loop:
- sudo
- git
- curl
- neovim
Task: Uninstall Package #
- The task is idempotent, if the package is already uninstalled, nothing happens.
Uninstall a package:
---
- name: Uninstall package
ansible.builtin.apt:
name: ufw
state: absent
purge: yes # Remove the package and its configuration files
Uninstall several packages:
- name: Uninstall packages
ansible.builtin.apt:
name: "{{ item }}"
state: absent
purge: yes # Remove the package and its configuration files
loop:
- curl
- ufw
- git
Task: Install & Configure UFW Firewall #
-
Using
state: reset
clears all existing firewall rules before applying new ones. This ensures that the firewall configuration matches exactly what is defined in the playbook. -
to remove a port, simply delete it from the list and rerun the playbook.
- name: Ensure UFW firewall is installed
ansible.builtin.apt:
name: ufw
state: latest # If already installed, update to the latest version
update_cache: yes # Update the APT cache
- name: Reset UFW firewall rules
community.general.ufw:
state: reset
- name: Add UFW firewall rules
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- 22
- 80
- 443
- name: Ensure UFW is enabled
community.general.ufw:
state: enabled
policy: deny # Everything is blocked by default
Task: Install Docker & Docker Compose #
---
# Install required dependencies for Docker
- name: Install dependencies
ansible.builtin.apt:
name: "{{ item }}"
state: latest
update_cache: true
cache_valid_time: 0
loop:
- ca-certificates
- curl
- git
- gnupg
- lsb-release
# Install Docker & Docker Compose
- name: Ensure /etc/apt/keyrings directory exists
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add Docker GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /etc/apt/keyrings/docker.asc
mode: '0644'
- name: Add Docker APT repository
ansible.builtin.apt_repository:
repo: "deb [arch={{ (ansible_architecture == 'aarch64') | ternary('arm64', (ansible_architecture == 'x86_64') | ternary('amd64', ansible_architecture)) }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable"
state: present
filename: docker
- name: Install docker
ansible.builtin.apt:
name: "{{ item }}"
state: present
update_cache: true
cache_valid_time: 0
loop:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
# Add user to Docker group
- name: Ensure user is in the Docker group
ansible.builtin.user:
name: debian
groups: docker
append: yes
# Reboot
- name: Reboot if needed (optional)
ansible.builtin.reboot:
msg: "Rebooting to apply docker group membership"
reboot_timeout: 120
when: reboot_required | default(true)
Task: Start Docker Container #
---
- name: Start Docker container
docker_container:
name: nginx
image: "nginx:latest"
restart_policy: unless-stopped
ports:
- "8080:80"
Task: Start Docker Container with Volume Mapping #
---
- name: Ensure directory exists
ansible.builtin.file:
path: /opt/nginx/data
state: directory
mode: '0755'
owner: debian
group: debian
recurse: yes
become: true
- name: Ensure file exists
ansible.builtin.copy:
dest: /opt/nginx/data/index.html
content: |
hi there
some text
owner: debian
group: debian
- name: Start a Docker container with volume mapping
docker_container:
name: nginx
image: "nginx:latest"
restart_policy: unless-stopped
ports:
- "8080:80"
volumes:
- "/opt/nginx/data:/usr/share/nginx/html"
Task: Stop Docker Container #
---
- name: Stop Docker container without removing it
docker_container:
name: nginx
state: stopped
Task: Stop & Remove Docker Container #
# Start container with the name "nginx"
docker run --name nginx -d -p 8080:80 nginx:latest
---
- name: Stop and remove Docker container
docker_container:
name: nginx
state: absent
Task: Remove Container Image #
- name: Remove Nginx image
docker_image:
name: nginx
tag: latest
state: absent
Task: Start Docker Compose Stack #
-
This task creates a “docker-compose.yml” file with the specified content. As an alternative, the file could be copied from a static file or template.
-
The stack is started using a command, which is not idempotent. The task runs even if the stack is already up, and Ansible will always report a “changed” status.
---
- name: Ensure directory exists
ansible.builtin.file:
path: /opt/nginx-stack
state: directory
owner: debian
group: debian
recurse: yes
become: true
- name: Ensure file exists
ansible.builtin.copy:
dest: /opt/nginx-stack/docker-compose.yml
content: |
services:
nginx:
image: nginx:latest
restart: unless-stopped
ports:
- "8080:80"
owner: debian
group: debian
become: true
- name: Start Docker Compose stack
ansible.builtin.command: docker compose up -d
args:
chdir: /opt/nginx-stack
become: true
become_user: debian
Task: Stop Docker Compose Stack #
---
- name: Stop Docker Compose stack
ansible.builtin.command: docker compose down
args:
chdir: /opt/nginx-stack
become: true
Task: Check Compose Status & Stop Stack #
- name: Check if Docker Compose steck is up
ansible.builtin.command: docker compose ps
register: compose_status
failed_when: false
changed_when: false
args:
chdir: /opt/nginx-stack
become: yes
- name: Stop Docker Compose stack
ansible.builtin.command: docker compose down --remove-orphans
args:
chdir: /opt/nginx-stack
when: "'Up' in compose_status.stdout"
become: yes
Task - Services: Start Service #
- name: Start service
ansible.builtin.systemd:
name: nginx
state: started
Task - Services: Restart Service #
- name: Restart service
ansible.builtin.systemd:
name: nginx
state: restarted
Task - Services: Stop Service #
- name: Stop service
ansible.builtin.systemd:
name: nginx
state: stopped
Task - Services: Enable Service #
- name: Enable service at boot
ansible.builtin.systemd:
name: nginx
enabled: yes
Task - Services: Disable Service #
- name: Disable service at boot
ansible.builtin.systemd:
name: nginx
enabled: no
Task - Shell Commands: Run Command (once) #
This task copies /tmp/file1
to /tmp/file2
only if /tmp/file2
does not yet exist. The if condition makes it idempotent, the playbook notes the task still
as changed though, because Ansible has no way to know that nothing was actually changed.
- name: Run command
ansible.builtin.shell: |
if ! ls /tmp/file2 >/dev/null 2>&1; then
cp /tmp/file1 /tmp/file2
fi
Syntax:
-
If
ls
finds the file, it exits with code0
, and the condition is true. -
!
inverts the condition, ifls /tmp/file2
fails (file does not exist), then it runs thecp
command.
Task - Shell Commands: Run Script (once) #
- name: Run a script
ansible.builtin.command: bash /tmp/gitlab-runner.script.deb.sh
args:
creates: /etc/apt/sources.list.d/runner_gitlab-runner.list
Note: The script creates the following file /etc/apt/sources.list.d/runner_gitlab-runner.list
and does not run again,
if it already exists.
Task - K8s (Shell): Create Namespace (once) #
- name: Create namespace
ansible.builtin.shell: |
if ! kubectl get namespace example-namespace >/dev/null 2>&1; then
kubectl create namespace example-namespace
fi
Task - K8s (Shell): Create Secret (once) #
- name: Create secret from yml
ansible.builtin.shell: |
kubectl get secret secret-name -n example-namespace || \
kubectl apply -f secret.yml
args:
chdir: /some/directory
Task - K8s (Shell): Kubectl Apply (with timeout) #
- name: Kubectl apply with timeout
ansible.builtin.shell: kubectl apply -f some.yml --timeout=120s
args:
chdir: /some/directory
Task - K8s (Shell): Deploy Helm Release (once) #
- name: Deploy Helm Chart
ansible.builtin.shell: |
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
if ! helm status release-name -n example-namespace >/dev/null 2>&1; then
helm install release-name repository/chart-name \
--namespace example-namespace \
-f values.yml
fi
args:
chdir: /some/directory
Task with Handler #
The following task triggers a handler that restarts a server.
# The file and folder structure looks like this:
ansible-playground
├── ansible.cfg
├── inventory
├── playbooks
│ └── example_playbook.yml
└── roles
└── example_role
├── handlers
│ └── main.yml
├── tasks
│ └── main.yml
└── templates
└── config.toml.j2
- roles/example_role/tasks/main.yml
# Copy NFS exports file from template
- name: Copy NFS exports file from template
template:
src: exports.j2
dest: /etc/exports
owner: root
group: root
mode: "600"
notify: Restart NFS server
- roles/example_role/handlers/main.yml
# Restart NFS server
- name: Restart NFS server
ansible.builtin.systemd:
name: nfs-server
state: restarted
enabled: true
Run Playbook #
Run Example Playbook #
# CD into Ansible project
cd ~/ansible-playground
# Run the playbook
ansible-playbook playbooks/example_playbook.yml -i inventory
Run Playbook with Tags #
- playbooks/example_playbook.yml
---
- name: Example Playbook
hosts: example_hosts
become: true
gather_facts: true
roles:
- example_role
- roles/example_role/tasks/main.yml
---
- name: Folder 1
ansible.builtin.file:
path: /tmp/folder1
state: directory
tags: [tag1]
- name: Folder 2
ansible.builtin.file:
path: /tmp/folder2
state: directory
tags: [tag2]
- name: Folder 3
ansible.builtin.file:
path: /tmp/folder3
state: directory
tags: [tag2, tag3]
Run the playbook:
# Run playbook: Default run (no tag filtering, run all tasks)
ansible-playbook playbooks/example_playbook.yml -i inventory
# Run playbook: Run specific tag only (only /tmp/folder3 is created)
ansible-playbook playbooks/example_playbook.yml --tags "tag3" -i inventory
# Run playbook: Skip specific tag (only /tmp/folder1 is created)
ansible-playbook playbooks/example_playbook.yml --skip-tags "tag2" -i inventory
# Define multiple tags (All folders are created)
ansible-playbook playbooks/example_playbook.yml --tags "tag1,tag2" -i inventory