Chapter 12: Git and Development Containers

Developing with VS Code and development containers

A playbook is only half of what runs. The other half is the tools: which ansible-core, which collections, which version of the linter. A development container puts those tools in an image, so everyone who opens the project, on any machine, gets the same ones.

Three places your tools can come from

So far you have run Ansible with the tools installed on workstation. That works, but it ties the project to one machine: a colleague with a different ansible-core version can get different results from the same playbook. Containers fix the tools as well as the code.

Three runtimes for one project: Same project, same hosts: the runtime decides which ansible-core and which tools run your playbook.

RuntimeWhere it comes fromWhat it is for
Installed toolsPackages and pip on the machineWhatever that machine happens to have
Development containerAn image, opened by VS Code or podman runWriting, linting and testing the project with a known toolset
Execution environmentAn image, started by ansible-navigatorRunning playbooks with known engine and collection versions

The Ansible community publishes a ready-made development container image, ghcr.io/ansible/community-ansible-dev-tools. It includes ansible-core, ansible-navigator, ansible-lint and the other development tools. It is about 1.4 GB, so the first pull takes a minute or two.

Run the development container yourself

VS Code starts the development container for you, but it is an ordinary container, and you can start it with Podman. Doing that once shows exactly what the editor does. Compare the engine on workstation with the one in the image:

[student@workstation ~]$ ansible --version | head -1
ansible [core 2.14.18]
[student@workstation ~]$ podman run --rm ghcr.io/ansible/community-ansible-dev-tools:latest ansible --version | head -1
ansible [core 2.21.4]

Same machine, two different engines. The image's versions change over time, so yours may be newer.

To work on a project, the container needs to see it. From the project directory:

podman run --rm --security-opt label=disable \
  -v "$PWD:/workdir" -w /workdir \
  ghcr.io/ansible/community-ansible-dev-tools:latest ansible-lint site.yml

Lint the project

ansible-lint checks a playbook against good practice and reports every rule it breaks, with the file and line. This is part of its report for the exercise's site.yml:

yaml[truthy]: Truthy value should be one of [false, true]
site.yml:4

fqcn[action-core]: Use FQCN for builtin module actions (file).
site.yml:7:7 Use `ansible.builtin.file` or `ansible.legacy.file` instead.

no-changed-when: Commands should not change things if nothing needs doing.
site.yml:12 Task/Handler: Kernel version is recorded

Failed: 3 failure(s), 0 warning(s) in 1 files processed of 1 encountered.

Each finding names a rule and points to a line. Fix the line and run the linter again until it reports Passed. These rules come up often:

RuleProblemFix
yaml[truthy]yes, no, on used as booleansWrite true or false
fqcn[action-core]A short module name such as fileUse the full name, ansible.builtin.file
no-changed-whencommand or shell reports changed on every runAdd creates/removes, or changed_when
yaml[octal-values]mode: 0644 without quotesQuote it: mode: "0644" (chapter 6)

Run a playbook from the container

To reach the managed hosts, the container also needs your SSH key. Mount your .ssh directory read-only. The container runs as root, so it goes to /root/.ssh:

podman run --rm --security-opt label=disable \
  -v "$HOME/.ssh:/root/.ssh:ro" \
  -v "$PWD:/workdir" -w /workdir \
  ghcr.io/ansible/community-ansible-dev-tools:latest ansible-playbook site.yml

The play runs with the container's ansible-core 2.21, against the same hosts and with the same ansible.cfg and inventory.

Open the same container in VS Code

VS Code reads the container's settings from .devcontainer/devcontainer.json in the project. Commit that file, and everyone who opens the project gets the same tools:

{
  "name": "Ansible development",
  "image": "ghcr.io/ansible/community-ansible-dev-tools:latest",
  "runArgs": ["--security-opt", "label=disable"],
  "customizations": {
    "vscode": {
      "extensions": ["redhat.ansible"]
    }
  }
}

To use it:

  1. Install VS Code and a container engine (Podman or Docker).
  2. In the Extensions view, install Dev Containers (Microsoft) and Ansible (Red Hat).
  3. Open the project folder, open the Command Palette (Ctrl+Shift+P) and choose Dev Containers: Reopen in Container. The bottom-left corner of the window then shows that you are inside the container.
  4. Open a terminal with Terminal > New Terminal. It runs inside the container, in the project directory: ansible --version reports the image's engine.
  5. Open a playbook. The Ansible extension underlines lint findings as you type. Right-click the playbook in the editor or the Explorer to run it with ansible-navigator run or ansible-playbook.

Configure ansible-navigator for the project

ansible-navigator looks for a settings file in the project directory before it reads ~/.ansible-navigator.yml. A project file travels with the project in Git, so everyone runs it the same way:

---
ansible-navigator:
  mode: stdout
  execution-environment:
    enabled: true
    image: ghcr.io/ansible-community/community-ee-minimal:latest
    pull:
      policy: missing
  playbook-artifact:
    enable: false

Now a plain ansible-navigator run site.yml runs in that execution environment, and ansible-navigator exec runs any command inside it:

[student@workstation workflow-container]$ ansible-navigator exec -- ansible --version | head -1
ansible [core 2.21.3]

The Ansible extension in VS Code has its own settings, in .vscode/settings.json or under File > Preferences > Settings > Extensions > Ansible. The ones you are most likely to change:

SettingDefaultMeaning
ansible.ansibleNavigator.pathansible-navigatorWhich ansible-navigator the extension runs
ansible.validation.lint.enabledtrueRun ansible-lint as you edit
ansible.executionEnvironment.enabledfalseUse an execution environment for the extension's own checks
ansible.executionEnvironment.imagethe dev tools imageWhich image those checks use

Inside the development container, leave the defaults: the container already provides ansible-navigator and ansible-lint, and runs started from the editor read the project's ansible-navigator.yml.