Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ansible-deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: Deploy Ansible configuration
on:
push:
branches: [main]
paths: [ansible/**]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ansible-deploy
cancel-in-progress: false
jobs:
ansible-playbook:
environment: ansible
name: Run the playbook against all hosts
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
cache-dependency-glob: uv.lock
activate-environment: true
- name: Install dependencies
run: uv sync --frozen --only-group ansible
- name: Configure SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |-
install -d -m 0700 "$HOME/.ssh"
printf '%s\n' "$SSH_PRIVATE_KEY" > "$HOME/.ssh/id_ed25519"
chmod 0600 "$HOME/.ssh/id_ed25519"
cat ansible/known_hosts >> "$HOME/.ssh/known_hosts"
- name: Run the playbook
working-directory: ansible
env:
ANSIBLE_REMOTE_USER: ci
ANSIBLE_BECOME_ASK_PASS: 'false'
run: uv run ansible-playbook --diff playbook.yaml
26 changes: 26 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,29 @@ jobs:
run: uv sync
- name: Run prek
run: uv run prek run --all-files
ansible-lint:
name: Ansible lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version-file: pyproject.toml
- name: Sync dependencies
run: uv sync --frozen --only-group ansible
- name: Run ansible-lint
working-directory: ansible
run: uv run ansible-lint
- name: Run playbook syntax check
working-directory: ansible
env:
ANSIBLE_BECOME_ASK_PASS: 'false'
run: uv run ansible-playbook --syntax-check playbook.yaml
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ansible/vault_password
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# infrastructure
Our infrastructure

## Documentation

- [Ansible](docs/ansible.md) — server configuration, CI deploys, bootstrapping
1 change: 0 additions & 1 deletion ansible/ansible.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[defaults]
inventory = inventory/hosts.yaml
vault_password_file = vault_password

[privilege_escalation]
become = yes
Expand Down
2 changes: 2 additions & 0 deletions ansible/known_hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# microwave.box.letsbuilda.dev:22 SSH-2.0-OpenSSH_10.2p1 Ubuntu-2ubuntu3.5
microwave.box.letsbuilda.dev ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA19eLgakX4Y69JrGaMetJlg4YmTBC7KoYvhnGiX83Iz
3 changes: 3 additions & 0 deletions ansible/playbook.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
---
- name: Manage users
hosts: all
roles: [users]
- name: Run common setup
hosts: all
roles: [common]
Expand Down
1 change: 1 addition & 0 deletions ansible/roles/users/files/ci-sudoers
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ci ALL=(ALL:ALL) NOPASSWD: ALL
1 change: 1 addition & 0 deletions ansible/roles/users/files/ci_ed25519.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL5Qm8eF6Sqdu+sOKMimDAWIJVlgmgyBQQhcOqapHmMI ci@letsbuilda/infrastructure
32 changes: 32 additions & 0 deletions ansible/roles/users/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
- name: Create the ci automation user
ansible.builtin.user:
name: ci
shell: /bin/bash
create_home: true
password_lock: true
- name: Authorize the CI deploy key for the ci user
ansible.posix.authorized_key:
user: ci
key: "{{ lookup('file', 'ci_ed25519.pub') }}"
exclusive: true
- name: Grant the ci user passwordless sudo
ansible.builtin.copy:
src: ci-sudoers
dest: /etc/sudoers.d/ci
owner: root
group: root
mode: '0440'
validate: /usr/sbin/visudo -cf %s
- name: Create the docker group
ansible.builtin.group:
name: docker
system: true
- name: Create the letsbuilda service user
ansible.builtin.user:
name: letsbuilda
system: true
shell: /usr/sbin/nologin
create_home: true
groups: [docker]
append: true
42 changes: 42 additions & 0 deletions docs/ansible.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Ansible

Server configuration lives in `ansible/`. All commands below run from that directory.

## Running locally

```
uv sync --group ansible
uv run ansible-playbook --diff playbook.yaml
```

You connect as your own user and are prompted for your sudo (BECOME) password.

## CI deploys

Pushes to `main` that touch `ansible/**` (and manual `workflow_dispatch` runs) run the
playbook from GitHub Actions via the `ansible` GitHub Environment. CI connects as the
dedicated `ci` user (created by the `users` role) using the `SSH_PRIVATE_KEY`
environment secret, with passwordless sudo granted by `/etc/sudoers.d/ci`. The host
key is pinned in `ansible/known_hosts`, keeping strict host key checking enabled.

## Bootstrapping / key rotation

The `ci` user only exists after the playbook has run once, so a new host needs one
local run (as yourself, with your sudo password) before CI can deploy:

1. Generate the CI keypair:
`ssh-keygen -t ed25519 -N '' -C 'ci@letsbuilda/infrastructure' -f ./ci_ed25519`
2. Put the public key in `ansible/roles/users/files/ci_ed25519.pub`.
3. Pin the host key: run `ssh-keyscan -t ed25519 microwave.box.letsbuilda.dev` from a
trusted network, verify the fingerprint out-of-band, and add the output to
`ansible/known_hosts`.
4. Add the private key as the `SSH_PRIVATE_KEY` secret in the `ansible` GitHub
Environment (restrict the environment's deployment branches to `main`).
5. Run the playbook locally once to create the `ci` user, then verify the CI auth
path:
`ssh -i ./ci_ed25519 -o UserKnownHostsFile=ansible/known_hosts ci@microwave.box.letsbuilda.dev 'sudo -n true && echo sudo-ok'`
6. Shred the local private key copy.

To rotate the CI key: repeat with a new keypair, replacing the committed public key
and the `SSH_PRIVATE_KEY` secret, then run the playbook once (locally or via the old
key). If the host is reinstalled, refresh `ansible/known_hosts` the same way as step 3.