bootstrap.yml prepares a host as an admin via -u; new-deploy-key.sh makes the key pair and refuses to write inside the repo. CI deploys refuse the bootstrap playbook, since the dispatch API ignores the dropdown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.8 KiB
YAML
125 lines
4.8 KiB
YAML
---
|
|
# Prepare a host for everything else in this repository: the deploy user, its
|
|
# key, its access to Docker, and the directory the stacks live in.
|
|
#
|
|
# The one playbook that cannot connect as the deploy user, because it is what
|
|
# creates it. Run it as an administrator who already has access:
|
|
#
|
|
# ansible-playbook playbooks/bootstrap.yml -u <admin-user> -k -K
|
|
#
|
|
# -u is what makes this work, and it works only because the deploy account is
|
|
# `remote_user` in ansible.cfg rather than ansible_user in the inventory. An
|
|
# inventory variable outranks -u; a config default does not. From the host
|
|
# itself, `-c local -K` skips SSH and the question with it.
|
|
#
|
|
# -k asks for the SSH password and -K for the sudo password; drop either if that
|
|
# account already uses a key or has passwordless sudo. SSH_KEY_PATH is not
|
|
# needed — an empty ansible_ssh_private_key_file is ignored by the ssh plugin,
|
|
# so group_vars/all.yml does not get in the way here either.
|
|
#
|
|
# Deliberately not imported by site.yml and not offered in the CI deploy
|
|
# dropdown: a converge run and the runner both authenticate as the deploy user,
|
|
# which is exactly what does not exist yet.
|
|
- name: Bootstrap a host for deployment
|
|
hosts: all
|
|
gather_facts: false
|
|
become: true
|
|
tags: [bootstrap]
|
|
|
|
vars:
|
|
deploy_user: deploy
|
|
deploy_pubkey_file: ~/.ssh/deploy_key.pub
|
|
# The compose_stack role escalates only when a stack_dirs entry sets owner
|
|
# or group, which is how a bind mount gets the uid its container runs as.
|
|
# metrics and openbao do that, so a host running either wants
|
|
# -e deploy_sudo=true. Still off by default: this grants NOPASSWD:ALL, and a
|
|
# host with no such stack has no reason to carry it.
|
|
deploy_sudo: false
|
|
|
|
tasks:
|
|
- name: Look for the public key on the control node
|
|
ansible.builtin.stat:
|
|
path: "{{ deploy_pubkey_file | expanduser }}"
|
|
register: bootstrap_pubkey
|
|
delegate_to: localhost
|
|
become: false
|
|
changed_when: false
|
|
|
|
# Before the user is half-created, rather than after.
|
|
- name: Verify the public key exists
|
|
ansible.builtin.assert:
|
|
that:
|
|
- bootstrap_pubkey.stat.exists | default(false)
|
|
fail_msg: >-
|
|
no public key at {{ deploy_pubkey_file }} —
|
|
run scripts/new-deploy-key.sh first
|
|
quiet: true
|
|
|
|
- name: Look for the docker group on the host
|
|
ansible.builtin.getent:
|
|
database: group
|
|
key: docker
|
|
fail_key: false
|
|
|
|
# Without this the user task fails with "Group docker does not exist", which
|
|
# is true and says nothing about the actual problem.
|
|
- name: Verify Docker is installed
|
|
ansible.builtin.assert:
|
|
that:
|
|
- ansible_facts.getent_group.docker is defined
|
|
fail_msg: >-
|
|
no docker group on {{ inventory_hostname }} — install Docker first
|
|
quiet: true
|
|
|
|
# The payload sync is ansible.posix.synchronize, which needs rsync at both
|
|
# ends. Installed rather than asserted, unlike Docker above: rsync is one
|
|
# package from the base repositories, where installing Docker means choosing
|
|
# repositories and versions and is a decision this playbook should not make
|
|
# on your behalf.
|
|
- name: Install rsync
|
|
ansible.builtin.package:
|
|
name: rsync
|
|
state: present
|
|
|
|
# In the docker group because the role runs compose unescalated: that is what
|
|
# keeps the bind mounts from being created root-owned.
|
|
- name: Create the deploy user
|
|
ansible.builtin.user:
|
|
name: "{{ deploy_user }}"
|
|
groups: docker
|
|
append: true
|
|
shell: /bin/bash
|
|
create_home: true
|
|
|
|
- name: Authorise the deploy key
|
|
ansible.posix.authorized_key:
|
|
user: "{{ deploy_user }}"
|
|
key: "{{ lookup('ansible.builtin.file', deploy_pubkey_file | expanduser) }}"
|
|
# Never exclusive. Rewriting the whole file would drop any other key that
|
|
# is currently somebody's only way onto this host.
|
|
exclusive: false
|
|
|
|
# The role creates {{ stack_root }}/<stack> unescalated, and /srv is
|
|
# root-owned, so without this the first deploy fails on a permission error
|
|
# that looks nothing like its cause.
|
|
- name: Create the stack root
|
|
ansible.builtin.file:
|
|
path: "{{ stack_root }}"
|
|
state: directory
|
|
owner: "{{ deploy_user }}"
|
|
group: "{{ deploy_user }}"
|
|
mode: "0755"
|
|
|
|
- name: Allow the deploy user to escalate
|
|
ansible.builtin.copy:
|
|
dest: "/etc/sudoers.d/{{ deploy_user }}"
|
|
content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
|
owner: root
|
|
group: root
|
|
mode: "0440"
|
|
# A malformed sudoers file can lock everyone out of a host permanently.
|
|
# validate runs visudo against the candidate and refuses to install it
|
|
# if it does not parse.
|
|
validate: visudo -cf %s
|
|
when: deploy_sudo | bool
|