Ansible Tutorial for Beginners: Automate Your First Server in 30 Minutes
Ansible tutorial for beginners: learn automation from scratch.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓Python 3.6+ on managed nodes. SSH access to target servers. A Linux/macOS control node (or WSL2 on Windows).
Ansible automates server setup and management using YAML playbooks. No agent needed — just SSH and Python on the target. Install it with pip install ansible, write a playbook, and run ansible-playbook playbook.yml.
Think of Ansible as a remote control for your servers. You write a list of instructions (a playbook) like 'install nginx, copy config file, restart service'. Ansible then connects to each server via SSH and follows those instructions exactly. No need to install anything on the servers — it's like using a universal remote that works with any TV.
You've SSH'd into a server at 2am to fix a config typo. Again. You've run the same 15 commands across 10 servers, praying you don't miss one. That's not DevOps — that's manual labor. Ansible kills that pain. It's the simplest automation tool I've used in 15 years, and it's the first thing I teach every junior. By the end of this, you'll write a playbook that installs a web server, deploys a static site, and ensures it stays running — all with one command. No agents, no daemons, just SSH and YAML.
What Problem Does Ansible Solve?
Before Ansible, you had two choices: SSH into each server and type commands by hand (error-prone, slow), or use tools like Puppet/Chef that required agents installed on every node (complex, heavy). Ansible came along and said: what if we just use SSH and a simple YAML file? No agents, no daemons, no PKI infrastructure. Just your SSH keys and a playbook. That's why it won. You write a playbook once, run it against 10 or 10,000 servers, and every one ends up in the same state. It's idempotent — run it twice, nothing changes. That's the superpower.
--check flag to dry-run a playbook: ansible-playbook playbook.yml --check. It shows what would change without actually making changes. I run this before every production deployment.Installing Ansible and Setting Up Your First Inventory
Install Ansible with pip: pip install ansible. That's it. No server setup, no database. Now create an inventory file — a list of servers Ansible will talk to. Inventory can be INI or YAML. I prefer INI for simplicity. Here's a basic one: [webservers] web1.example.com web2.example.com. You can also use IPs. Test connectivity with ansible all -i inventory.ini -m ping. That module just checks SSH works and Python is available. If you get a pong, you're ready.
ansible_ssh_private_key_file only for testing. In production, use an SSH agent or a secrets manager like HashiCorp Vault.Writing Your First Playbook: Install and Configure Nginx
A playbook is a YAML file with one or more plays. Each play targets a group of hosts and runs tasks in order. Let's write one that installs nginx, copies a custom index.html, and ensures the service is running. The become: yes tells Ansible to use sudo. Tasks are modules — apt, copy, service. Each module is idempotent: it checks the current state and only makes changes if needed. Run it with ansible-playbook -i inventory.ini nginx.yml.
Using Variables and Templates for Dynamic Configs
Hardcoding values in playbooks is a rookie move. Use variables. You can define them in the playbook (under vars), in separate files, or in the inventory. Templates use Jinja2 syntax — they're files with .j2 extension that get rendered with variables. For example, an nginx config template that uses {{ server_name }} and {{ http_port }}. This lets you reuse the same playbook for different environments: dev, staging, prod — just change the variables.
ansible-vault to encrypt sensitive variables like passwords or API keys. Create an encrypted file with ansible-vault create secrets.yml, then include it in your playbook with vars_files: secrets.yml. Run with --ask-vault-pass.Ansible Modules: The Tools in Your Belt
Modules are the building blocks. Each module does one thing: apt installs packages, copy transfers files, service manages daemons, command runs arbitrary commands. There are hundreds. The most common ones for beginners: apt/yum (package management), copy/template (file management), service/systemd (service management), user (user management), file (file/directory attributes). Always prefer a specific module over command or shell — they're idempotent and give better error messages. Use command only as a last resort.
shell or command when a module exists. Example: shell: apt install nginx -y instead of apt: name=nginx state=present. The module is idempotent; the shell command will reinstall every time. I've seen this cause downtime when a package update changed behavior unexpectedly.Playbook Structure: Plays, Tasks, and Handlers
A playbook is a list of plays. Each play targets a group of hosts and defines tasks. Tasks run in order. Handlers are special tasks that run only when notified by other tasks, and only once at the end of the play. Use handlers for actions like restarting a service after a config change. The notify keyword triggers a handler. Handlers are defined under handlers at the play level. They're a clean way to separate the 'do this' from 'if needed, do that'.
--force-handlers). To run handlers immediately, use meta: flush_handlers.Running Playbooks: Ad-Hoc Commands vs Playbooks
For one-off tasks, use ad-hoc commands: ansible all -i inventory.ini -m command -a 'uptime'. For repeatable automation, write playbooks. Ad-hoc is great for quick checks: ansible all -m ping, ansible all -m shell -a 'df -h'. But anything you'll run more than once belongs in a playbook. Playbooks are version-controlled, documented, and idempotent. Always use --check before running a playbook against production. Use --diff to see what changes will be made.
ansible-inventory --list -i inventory.ini to see how Ansible parses your inventory. Great for debugging group membership and variable inheritance.Idempotency: The Superpower You Must Understand
Idempotency means running the same playbook multiple times produces the same result. No unintended side effects. Ansible modules are designed to be idempotent: apt: name=nginx state=present checks if nginx is installed; if yes, it does nothing. This is why you can run a playbook daily as a cron job to enforce state. The opposite is a script that blindly runs apt install nginx — it would reinstall or error. Always write tasks that check state before acting. Use state: present or state: absent rather than state: latest unless you want upgrades.
command or shell breaks idempotency unless you add creates or when conditions. Example: command: /opt/install.sh creates=/opt/installed only runs if the file doesn't exist. Without that, it runs every time.The 4GB Container That Kept Dying
vm.overcommit_memory=1 on the host, allowing processes to allocate more memory than available. Combined with a misconfigured memory limit in Docker, the kernel killed the container.vm.overcommit_memory=0 in /etc/sysctl.conf and reload with sysctl -p. Also set memory.reservation equal to memory.limit_in_bytes in Docker.- Never tune kernel memory overcommit without understanding your workload.
- Ansible can apply dangerous configs silently — always test in staging.
ssh-add -l. 2. Test manual SSH: ssh user@host. 3. Check ansible_user and ansible_ssh_private_key_file in inventory. 4. Increase timeout: ansible_ssh_timeout=30.ansible all -m raw -a 'python3 --version'. 2. Set ansible_python_interpreter=/usr/bin/python3 in inventory. 3. Install missing Python packages via apt or pip.--diff to see what Ansible thinks the state is. 2. Check if module is idempotent (e.g., command never reports 'changed' unless you use creates). 3. Verify variables are interpolated correctly with --check.ssh-add -lssh-add ~/.ssh/id_rsa| File | Command / Code | Purpose |
|---|---|---|
| first_playbook.yml | - name: Ensure nginx is installed and running | What Problem Does Ansible Solve? |
| inventory.ini | [webservers] | Installing Ansible and Setting Up Your First Inventory |
| nginx.yml | - name: Configure nginx web server | Writing Your First Playbook |
| nginx.conf.j2 | server { | Using Variables and Templates for Dynamic Configs |
| modules_demo.yml | - name: Demonstrate common modules | Ansible Modules |
| handlers_example.yml | - name: Update app config and restart | Playbook Structure |
| ad_hoc_examples.sh | ansible all -i inventory.ini -m command -a 'uptime' | Running Playbooks |
| idempotent_example.yml | - name: Idempotent user management | Idempotency |
Key takeaways
--syntax-check and --check --diff before applying any playbook.Common mistakes to avoid
4 patternsUsing shell/command module for everything
Hardcoding variables in tasks
defaults/main.yml or group_vars/ depending on scope.Running playbooks without --syntax-check first
ansible-playbook playbook.yml --syntax-check before execution. Catches YAML indentation errors and undefined variables.Forgetting become for tasks that need root
become: yes at the play level for tasks that install packages, modify system files, or manage services.Interview Questions on This Topic
How does Ansible ensure idempotency when using the `command` module?
command module is not idempotent by default. You must use the creates or removes parameter to check for a file or directory that indicates the command has already run. Alternatively, use changed_when to control when the task reports a change.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Ansible. Mark it forged?
3 min read · try the examples if you haven't