Added basic provisioning

This commit is contained in:
Jan Beilicke 2020-03-18 22:51:57 +01:00
parent 75b8617af0
commit 30a3c8563f
35 changed files with 779 additions and 4 deletions

View file

@ -1,4 +1,4 @@
MIT License Copyright (c) <year> <copyright holders>
MIT License Copyright (c) 2020 Jan Beilicke <dev@jotbe.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,3 +1,15 @@
# devops-vm-ansible
# Ansible DevOps VM
A versatile VM for DevOps purposes.
A versatile VM for DevOps purposes, provisioned by Ansible.
Will bootstrap a Ubuntu VM using Vagrant and Virtualbox and install a stack of useful DevOps tools.
To provide a lightweight experience, the VM uses [XFCE](https://xfce.org/) as a desktop environment.
The default user is `vagrant` with the password `vagrant`, with auto-login by default.
Provided tools:
* Docker 19.03.5
* Docker Compose 1.25.0
* Ansible 2.9.4

3
Vagrantfile vendored
View file

@ -7,7 +7,7 @@ Vagrant.configure(2) do |config|
config.vm.box = "hashicorp/bionic64"
config.vm.provider :virtualbox do |vbox, override|
vbox.customize ["modifyvm", :id, "--name", "jotbe Dev VM Ansible Playground"]
vbox.customize ["modifyvm", :id, "--name", "jotbe DevOps VM Ansible Playground"]
vbox.customize ["modifyvm", :id, "--memory", 8192]
vbox.customize ["modifyvm", :id, "--cpus", Etc.nprocessors/2]
vbox.customize ["modifyvm", :id, "--vram", 128]
@ -16,6 +16,7 @@ Vagrant.configure(2) do |config|
vbox.customize ["modifyvm", :id, "--clipboard-mode", "bidirectional"]
vbox.customize ["modifyvm", :id, "--uartmode1", "disconnected"]
vbox.customize ["modifyvm", :id, "--natnet1", "10.0.5/24"]
vbox.customize ["storageattach", :id, "--storagectl", "IDE Controller", "--port", "0", "--device", "0", "--type", "dvddrive", "--medium", "emptydrive"]
#vbox.customize ["storagectl", :id, "--name", "SATA Controller", "--hostiocache", "on"]
# Default: The VM is hosted on an SSD.
# Set to "off" if it is hosted on an HDD to enable performance optimizations for rotational drives (e.g. defragmentation):

14
provisioning/ansible.cfg Normal file
View file

@ -0,0 +1,14 @@
[defaults]
gathering = smart
fact_caching_connection = ./facts_cache
fact_caching = jsonfile
ansible_roles_path = ./roles
# The timeout is defined in seconds
# This is 2 hours
fact_caching_timeout = 7200
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=18000 -o PreferredAuthentications=publickey
control_path = %(directory)s/ansible-ssh-%%h-%%p-%%r
pipelining = True

1
provisioning/inventory Normal file
View file

@ -0,0 +1 @@
jbdev ansible_ssh_host=192.168.6.65 ansible_ssh_port=22

101
provisioning/playbook.yml Normal file
View file

@ -0,0 +1,101 @@
- hosts: jbdev
gather_facts: yes
become: yes
vars:
locales_gen:
- en_US.UTF-8
- de_DE.UTF-8
locales_default: de_DE.UTF-8
ansible_version: "2.9.4"
pip_install_packages:
- name: docker
- name: docker-compose
docker_edition: "ce"
docker_version: "19.03.5"
docker_package: "docker-{{ docker_edition }}"
docker_package_state: present
docker_compose_version: "1.25.0"
docker_users:
- vagrant
pre_tasks:
- name: Generate locals
locale_gen:
name: "{{ item }}"
state: present
loop: "{{ locales_gen }}"
- name: Get current locale
command: localectl status
register: locale_status
changed_when: false
- name: Extract LANG from current locale configuration
set_fact:
locale_lang: "{{ locale_status.stdout | regex_search('LANG=([^\n]+)', '\\1') | first }}"
- debug:
msg: "{{ locale_lang }}"
- name: Set default locale
command: localectl set-locale LANG={{ locales_default }}
when: locale_lang != locales_default
- name: Replace US repo with DE mirror
replace:
path: /etc/apt/sources.list
regexp: 'us(\.archive\.ubuntu\.com)'
replace: 'de\1'
- name: Install Python3
raw: test -e /usr/bin/python3 || (apt update && apt install -y --force-yes python3)
changed_when: false
- name: Install pip3
raw: test -e /usr/bin/pip3 || (apt update && apt install -y --force-yes python3-pip)
changed_when: false
tasks:
- name: Install Ansible
raw: test -e /usr/local/bin/ansible || pip3 install ansible={{ ansible_version }}
changed_when: false
- name: Disable motd-news
lineinfile:
path: /etc/default/motd-news
regexp: '^\s*ENABLED=\s*1'
line: ENABLED=0
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 7200
- name: Install XFCE4 and lightdm
apt:
pkg:
- lightdm
- xfce4
register: desktop_installed
- name: lightdm config
copy:
dest: /etc/lightdm/lightdm.conf
owner: root
group: root
mode: 0644
content: |
[SeatDefaults]
allow-guest=false
autologin-user=vagrant
autologin-user-timeout=0
user-session=xfce
- name: Reboot system
reboot:
when: desktop_installed.changed
roles:
- geerlingguy.pip
- geerlingguy.docker

View file

@ -0,0 +1,4 @@
# These are supported funding model platforms
---
github: geerlingguy
patreon: geerlingguy

View file

@ -0,0 +1,3 @@
*.retry
*/__pycache__
*.pyc

View file

@ -0,0 +1,32 @@
---
language: python
services: docker
env:
global:
- ROLE_NAME: docker
matrix:
- MOLECULE_DISTRO: centos8
- MOLECULE_DISTRO: centos7
- MOLECULE_DISTRO: ubuntu1804
- MOLECULE_DISTRO: ubuntu1604
- MOLECULE_DISTRO: debian10
- MOLECULE_DISTRO: debian9
- MOLECULE_DISTRO: fedora30
install:
# Install test dependencies.
- pip install molecule docker
before_script:
# Use actual Ansible Galaxy role name for the project directory.
- cd ../
- mv ansible-role-$ROLE_NAME geerlingguy.$ROLE_NAME
- cd geerlingguy.$ROLE_NAME
script:
# Run tests.
- molecule test
notifications:
webhooks: https://galaxy.ansible.com/api/v1/notifications/

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Jeff Geerling
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,89 @@
# Ansible Role: Docker
[![Build Status](https://travis-ci.org/geerlingguy/ansible-role-docker.svg?branch=master)](https://travis-ci.org/geerlingguy/ansible-role-docker)
An Ansible Role that installs [Docker](https://www.docker.com) on Linux.
## Requirements
None.
## Role Variables
Available variables are listed below, along with default values (see `defaults/main.yml`):
# Edition can be one of: 'ce' (Community Edition) or 'ee' (Enterprise Edition).
docker_edition: 'ce'
docker_package: "docker-{{ docker_edition }}"
docker_package_state: present
The `docker_edition` should be either `ce` (Community Edition) or `ee` (Enterprise Edition). You can also specify a specific version of Docker to install using the distribution-specific format: Red Hat/CentOS: `docker-{{ docker_edition }}-<VERSION>`; Debian/Ubuntu: `docker-{{ docker_edition }}=<VERSION>`.
You can control whether the package is installed, uninstalled, or at the latest version by setting `docker_package_state` to `present`, `absent`, or `latest`, respectively. Note that the Docker daemon will be automatically restarted if the Docker package is updated. This is a side effect of flushing all handlers (running any of the handlers that have been notified by this and any other role up to this point in the play).
docker_service_state: started
docker_service_enabled: true
docker_restart_handler_state: restarted
Variables to control the state of the `docker` service, and whether it should start on boot. If you're installing Docker inside a Docker container without systemd or sysvinit, you should set these to `stopped` and set the enabled variable to `no`.
docker_install_compose: true
docker_compose_version: "1.22.0"
docker_compose_path: /usr/local/bin/docker-compose
Docker Compose installation options.
docker_apt_release_channel: stable
docker_apt_arch: amd64
docker_apt_repository: "deb [arch={{ docker_apt_arch }}] https://download.docker.com/linux/{{ ansible_distribution|lower }} {{ ansible_distribution_release }} {{ docker_apt_release_channel }}"
docker_apt_ignore_key_error: True
(Used only for Debian/Ubuntu.) You can switch the channel to `edge` if you want to use the Edge release.
docker_yum_repo_url: https://download.docker.com/linux/centos/docker-{{ docker_edition }}.repo
docker_yum_repo_enable_edge: '0'
docker_yum_repo_enable_test: '0'
(Used only for RedHat/CentOS.) You can enable the Edge or Test repo by setting the respective vars to `1`.
docker_users:
- user1
- user2
A list of system users to be added to the `docker` group (so they can use Docker on the server).
## Use with Ansible (and `docker` Python library)
Many users of this role wish to also use Ansible to then _build_ Docker images and manage Docker containers on the server where Docker is installed. In this case, you can easily add in the `docker` Python library using the `geerlingguy.pip` role:
```yaml
- hosts: all
vars:
pip_install_packages:
- name: docker
roles:
- geerlingguy.pip
- geerlingguy.docker
```
## Dependencies
None.
## Example Playbook
```yaml
- hosts: all
roles:
- geerlingguy.docker
```
## License
MIT / BSD
## Author Information
This role was created in 2017 by [Jeff Geerling](https://www.jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/).

View file

@ -0,0 +1,29 @@
---
# Edition can be one of: 'ce' (Community Edition) or 'ee' (Enterprise Edition).
docker_edition: 'ce'
docker_package: "docker-{{ docker_edition }}"
docker_package_state: present
# Service options.
docker_service_state: started
docker_service_enabled: true
docker_restart_handler_state: restarted
# Docker Compose options.
docker_install_compose: true
docker_compose_version: "1.24.1"
docker_compose_path: /usr/local/bin/docker-compose
# Used only for Debian/Ubuntu. Switch 'stable' to 'edge' if needed.
docker_apt_release_channel: stable
docker_apt_arch: amd64
docker_apt_repository: "deb [arch={{ docker_apt_arch }}] https://download.docker.com/linux/{{ ansible_distribution|lower }} {{ ansible_distribution_release }} {{ docker_apt_release_channel }}"
docker_apt_ignore_key_error: true
# Used only for RedHat/CentOS/Fedora.
docker_yum_repo_url: https://download.docker.com/linux/{{ (ansible_distribution == "Fedora") | ternary("fedora","centos") }}/docker-{{ docker_edition }}.repo
docker_yum_repo_enable_edge: '0'
docker_yum_repo_enable_test: '0'
# A list of users who will be added to the docker group.
docker_users: []

View file

@ -0,0 +1,3 @@
---
- name: restart docker
service: "name=docker state={{ docker_restart_handler_state }}"

View file

@ -0,0 +1,2 @@
install_date: Sun Feb 9 00:16:54 2020
version: 2.6.0

View file

@ -0,0 +1,33 @@
---
dependencies: []
galaxy_info:
author: geerlingguy
description: Docker for Linux.
company: "Midwestern Mac, LLC"
license: "license (BSD, MIT)"
min_ansible_version: 2.4
platforms:
- name: EL
versions:
- 7
- 8
- name: Fedora
versions:
- all
- name: Debian
versions:
- stretch
- buster
- name: Ubuntu
versions:
- xenial
- bionic
galaxy_tags:
- web
- system
- containers
- docker
- orchestration
- compose
- server

View file

@ -0,0 +1,29 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-file: molecule/default/yaml-lint.yml
platforms:
- name: instance
image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest"
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
pre_build_image: true
provisioner:
name: ansible
lint:
name: ansible-lint
playbooks:
converge: ${MOLECULE_PLAYBOOK:-playbook.yml}
scenario:
name: default
verifier:
name: testinfra
lint:
name: flake8

View file

@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
pre_tasks:
- name: Update apt cache.
apt: update_cache=yes cache_valid_time=600
when: ansible_os_family == 'Debian'
roles:
- role: geerlingguy.docker

View file

@ -0,0 +1,6 @@
---
extends: default
rules:
line-length:
max: 200
level: warning

View file

@ -0,0 +1,20 @@
---
- name: Check current docker-compose version.
command: docker-compose --version
register: docker_compose_current_version
changed_when: false
failed_when: false
- name: Delete existing docker-compose version if it's different.
file:
path: "{{ docker_compose_path }}"
state: absent
when: >
docker_compose_current_version.stdout is defined
and docker_compose_version not in docker_compose_current_version.stdout
- name: Install Docker Compose (if configured).
get_url:
url: https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64
dest: "{{ docker_compose_path }}"
mode: 0755

View file

@ -0,0 +1,7 @@
---
- name: Ensure docker users are added to the docker group.
user:
name: "{{ item }}"
groups: docker
append: true
with_items: "{{ docker_users }}"

View file

@ -0,0 +1,27 @@
---
- include_tasks: setup-RedHat.yml
when: ansible_os_family == 'RedHat'
- include_tasks: setup-Debian.yml
when: ansible_os_family == 'Debian'
- name: Install Docker.
package:
name: "{{ docker_package }}"
state: "{{ docker_package_state }}"
notify: restart docker
- name: Ensure Docker is started and enabled at boot.
service:
name: docker
state: "{{ docker_service_state }}"
enabled: "{{ docker_service_enabled }}"
- name: Ensure handlers are notified now to avoid firewall conflicts.
meta: flush_handlers
- include_tasks: docker-compose.yml
when: docker_install_compose | bool
- include_tasks: docker-users.yml
when: docker_users | length > 0

View file

@ -0,0 +1,40 @@
---
- name: Ensure old versions of Docker are not installed.
package:
name:
- docker
- docker-engine
state: absent
- name: Ensure dependencies are installed.
apt:
name:
- apt-transport-https
- ca-certificates
state: present
- name: Add Docker apt key.
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
id: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
state: present
register: add_repository_key
ignore_errors: "{{ docker_apt_ignore_key_error }}"
- name: Ensure curl is present (on older systems without SNI).
package: name=curl state=present
when: add_repository_key is failed
- name: Add Docker apt key (alternative for older systems without SNI).
shell: |
set -o pipefail
curl -sSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
args:
warn: false
when: add_repository_key is failed
- name: Add Docker repository.
apt_repository:
repo: "{{ docker_apt_repository }}"
state: present
update_cache: true

View file

@ -0,0 +1,41 @@
---
- name: Ensure old versions of Docker are not installed.
package:
name:
- docker
- docker-common
- docker-engine
state: absent
- name: Add Docker GPG key.
rpm_key:
key: https://download.docker.com/linux/centos/gpg
state: present
- name: Add Docker repository.
get_url:
url: "{{ docker_yum_repo_url }}"
dest: '/etc/yum.repos.d/docker-{{ docker_edition }}.repo'
owner: root
group: root
mode: 0644
- name: Configure Docker Edge repo.
ini_file:
dest: '/etc/yum.repos.d/docker-{{ docker_edition }}.repo'
section: 'docker-{{ docker_edition }}-edge'
option: enabled
value: '{{ docker_yum_repo_enable_edge }}'
- name: Configure Docker Test repo.
ini_file:
dest: '/etc/yum.repos.d/docker-{{ docker_edition }}.repo'
section: 'docker-{{ docker_edition }}-test'
option: enabled
value: '{{ docker_yum_repo_enable_test }}'
- name: Install containerd separately (CentOS 8).
package:
name: https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm
state: present
when: ansible_distribution_major_version | int == 8

View file

@ -0,0 +1,3 @@
*.retry
*/__pycache__
*.pyc

View file

@ -0,0 +1,29 @@
---
language: python
services: docker
env:
global:
- ROLE_NAME: pip
matrix:
- MOLECULE_DISTRO: centos7
- MOLECULE_DISTRO: fedora29
- MOLECULE_DISTRO: ubuntu1804
- MOLECULE_DISTRO: debian9
install:
# Install test dependencies.
- pip install molecule docker
before_script:
# Use actual Ansible Galaxy role name for the project directory.
- cd ../
- mv ansible-role-$ROLE_NAME geerlingguy.$ROLE_NAME
- cd geerlingguy.$ROLE_NAME
script:
# Run tests.
- molecule test
notifications:
webhooks: https://galaxy.ansible.com/api/v1/notifications/

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Jeff Geerling
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,76 @@
# Ansible Role: Pip (for Python)
[![Build Status](https://travis-ci.org/geerlingguy/ansible-role-pip.svg?branch=master)](https://travis-ci.org/geerlingguy/ansible-role-pip)
An Ansible Role that installs [Pip](https://pip.pypa.io) on Linux.
## Requirements
On RedHat/CentOS, you may need to have EPEL installed before running this role. You can use the `geerlingguy.repo-epel` role if you need a simple way to ensure it's installed.
## Role Variables
Available variables are listed below, along with default values (see `defaults/main.yml`):
pip_package: python-pip
The name of the packge to install to get `pip` on the system. You can set to `python3-pip`, for example, when using Python 3 on Ubuntu.
pip_executable: pip
The role will try to autodetect the pip executable based on the `pip_package` (e.g. `pip` for Python 2 and `pip3` for Python 3). You can also override this explicitly, e.g. `pip_executable: pip3.6`.
pip_install_packages: []
A list of packages to install with pip. Examples below:
pip_install_packages:
# Specify names and versions.
- name: docker
version: "1.2.3"
- name: awscli
version: "1.11.91"
# Or specify bare packages to get the latest release.
- docker
- awscli
# Or uninstall a package.
- name: docker
state: absent
# Or update a package ot the latest version.
- name: docker
state: latest
# Or force a reinstall.
- name: docker
state: forcereinstall
# Or install a package in a particular virtualenv.
- name: docker
virtualenv: /my_app/venv
## Dependencies
None.
## Example Playbook
- hosts: all
vars:
pip_install_packages:
- name: docker
- name: awscli
roles:
- geerlingguy.pip
## License
MIT / BSD
## Author Information
This role was created in 2017 by [Jeff Geerling](https://www.jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/).

View file

@ -0,0 +1,6 @@
---
# For Python 3, use python3-pip.
pip_package: python-pip
pip_executable: "{{ 'pip3' if pip_package.startswith('python3') else 'pip' }}"
pip_install_packages: []

View file

@ -0,0 +1,2 @@
install_date: Sun Feb 9 00:16:42 2020
version: 1.3.0

View file

@ -0,0 +1,30 @@
---
dependencies: []
galaxy_info:
author: geerlingguy
description: Pip (Python package manager) for Linux.
issue_tracker_url: https://github.com/geerlingguy/ansible-role-pip/issues
company: "Midwestern Mac, LLC"
license: "license (BSD, MIT)"
min_ansible_version: 2.0
platforms:
- name: EL
versions:
- all
- name: Fedora
versions:
- all
- name: Debian
versions:
- all
- name: Ubuntu
versions:
- all
galaxy_tags:
- system
- server
- packaging
- python
- pip
- tools

View file

@ -0,0 +1,29 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-file: molecule/default/yaml-lint.yml
platforms:
- name: instance
image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest"
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
pre_build_image: true
provisioner:
name: ansible
lint:
name: ansible-lint
playbooks:
converge: ${MOLECULE_PLAYBOOK:-playbook.yml}
scenario:
name: default
verifier:
name: testinfra
lint:
name: flake8

View file

@ -0,0 +1,20 @@
---
- name: Converge
hosts: all
become: true
vars:
pip_install_packages:
# Test installing a specific version of a package.
- name: ipaddress
version: "1.0.18"
# Test installing a package by name.
- colorama
pre_tasks:
- name: Update apt cache.
apt: update_cache=true cache_valid_time=600
when: ansible_os_family == 'Debian'
roles:
- role: geerlingguy.pip

View file

@ -0,0 +1,14 @@
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
def test_hosts_file(host):
f = host.file('/etc/hosts')
assert f.exists
assert f.user == 'root'
assert f.group == 'root'

View file

@ -0,0 +1,6 @@
---
extends: default
rules:
line-length:
max: 120
level: warning

View file

@ -0,0 +1,14 @@
---
- name: Ensure Pip is installed.
package:
name: "{{ pip_package }}"
state: present
- name: Ensure pip_install_packages are installed.
pip:
name: "{{ item.name | default(item) }}"
version: "{{ item.version | default(omit) }}"
virtualenv: "{{ item.virtualenv | default(omit) }}"
state: "{{ item.state | default(omit) }}"
executable: "{{ pip_executable }}"
with_items: "{{ pip_install_packages }}"