diff --git a/devserver/ansible/deploy-svc-bot.yaml b/devserver/ansible/deploy-svc-bot.yaml new file mode 100644 index 0000000..3b6b009 --- /dev/null +++ b/devserver/ansible/deploy-svc-bot.yaml @@ -0,0 +1,47 @@ +--- +# Lightweight deploy: pull the latest bot image and restart the +# svc-bot systemd service — without re-applying the whole playbook. +# +# Pull goes through run.sh so it runs in the same infisical context as +# the service: the bot compose.yaml interpolates ${...} secrets that +# only infisical can provide. A bare `docker compose pull` would fail on +# those missing vars. We pull explicitly first so a registry/network +# failure surfaces *before* the running container is taken down. +# +# Run from devserver/ansible/: +# ansible-playbook deploy-svc-bot.yaml + +- name: Pull latest bot image and restart service + hosts: devserver + become: true + vars_files: + - roles/svc-bot/defaults/main.yaml + + tasks: + - name: Pull latest bot image + ansible.builtin.command: + cmd: "{{ svc_bot_deploy_dir }}/run.sh pull" + chdir: "{{ svc_bot_deploy_dir }}" + changed_when: true + register: _svc_bot_pull + + - name: Show pull output + ansible.builtin.debug: + var: _svc_bot_pull.stdout_lines + + - name: Restart svc-bot.service + ansible.builtin.systemd: + name: svc-bot.service + state: restarted + daemon_reload: true + + - name: Show svc-bot service status + ansible.builtin.command: + cmd: systemctl --no-pager status svc-bot.service + changed_when: false + failed_when: false + register: _svc_bot_status + + - name: Print service status + ansible.builtin.debug: + var: _svc_bot_status.stdout_lines diff --git a/devserver/ansible/roles/infisical/defaults/main.yaml b/devserver/ansible/roles/infisical/defaults/main.yaml index e8a7269..39145e4 100644 --- a/devserver/ansible/roles/infisical/defaults/main.yaml +++ b/devserver/ansible/roles/infisical/defaults/main.yaml @@ -1,5 +1,15 @@ --- # infisical CLI role — configurable defaults. + infisical_cli_version: "latest" infisical_setup_script_url: "https://artifacts-cli.infisical.com/setup.deb.sh" infisical_apt_source_path: "/etc/apt/sources.list.d/infisical.list" + +# Shared devserver infisical machine identity. Used by every service +# role that needs infisical-injected secrets (svc-events, svc-bot, ...). +# Pass DEVSERVER_INFISICAL_CLIENT_ID / _CLIENT_SECRET / _PROJECT_ID via +# the environment when running ansible. +infisical_env_file: /etc/infisical/devserver.env +infisical_client_id: "{{ lookup('env', 'DEVSERVER_INFISICAL_CLIENT_ID') }}" +infisical_client_secret: "{{ lookup('env', 'DEVSERVER_INFISICAL_CLIENT_SECRET') }}" +infisical_project_id: "{{ lookup('env', 'DEVSERVER_INFISICAL_PROJECT_ID') }}" diff --git a/devserver/ansible/roles/infisical/tasks/main.yaml b/devserver/ansible/roles/infisical/tasks/main.yaml index 7a362b2..75b9171 100644 --- a/devserver/ansible/roles/infisical/tasks/main.yaml +++ b/devserver/ansible/roles/infisical/tasks/main.yaml @@ -1,7 +1,7 @@ --- -# ------------------------------------------------------------------- -# Installs and makes sure the infisical cli is on the latest version -# ------------------------------------------------------------------- +# infisical role — install the infisical CLI and write the shared +# devserver machine-identity env file used by every service that needs +# infisical-injected secrets. # Register the apt repository - name: Check if infisical apt source is already registered @@ -33,3 +33,25 @@ - name: Show installed infisical version ansible.builtin.debug: msg: "infisical CLI installed: {{ infisical_installed_version.stdout | trim }}" + +# Shared machine-identity credentials. Written 0600; sourced by every +# service role's run.sh at runtime. Notifying service handlers from here +# would couple this role to service names, so credential rotation should +# be followed by a `just deploy-` to restart consumers. +- name: Ensure infisical env file directory exists + become: true + ansible.builtin.file: + path: "{{ infisical_env_file | dirname }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Render infisical EnvironmentFile (client id + secret, 0600) + become: true + ansible.builtin.template: + src: infisical.env.j2 + dest: "{{ infisical_env_file }}" + owner: root + group: root + mode: "0600" diff --git a/devserver/ansible/roles/infisical/templates/infisical.env.j2 b/devserver/ansible/roles/infisical/templates/infisical.env.j2 new file mode 100644 index 0000000..43690f8 --- /dev/null +++ b/devserver/ansible/roles/infisical/templates/infisical.env.j2 @@ -0,0 +1,2 @@ +INFISICAL_CLIENT_ID={{ infisical_client_id }} +INFISICAL_CLIENT_SECRET={{ infisical_client_secret }} diff --git a/devserver/ansible/roles/svc-bot/defaults/main.yaml b/devserver/ansible/roles/svc-bot/defaults/main.yaml new file mode 100644 index 0000000..af54d75 --- /dev/null +++ b/devserver/ansible/roles/svc-bot/defaults/main.yaml @@ -0,0 +1,17 @@ +--- +# svc-bot: docker compose stack for the devICT slack bot, secrets +# injected at runtime by the infisical CLI (no .env on disk). +# +# Infisical credentials live in the infisical role (shared machine +# identity for the whole devserver). This role only owns its deploy dir +# and image/port config. + +# Where the compose.yaml + run.sh live on the host. +svc_bot_deploy_dir: /opt/devict/bot + +# Container image. Placeholder until the bot repo publishes +# (buoy.cr/devict.org/bot:latest). +svc_bot_image: "buoy.cr/devict.org/bot:latest" + +# Host port the bot container publishes on (events owns 3001). +svc_bot_host_port: "3002" diff --git a/devserver/ansible/roles/svc-bot/handlers/main.yaml b/devserver/ansible/roles/svc-bot/handlers/main.yaml new file mode 100644 index 0000000..a3eae1a --- /dev/null +++ b/devserver/ansible/roles/svc-bot/handlers/main.yaml @@ -0,0 +1,7 @@ +--- +- name: restart svc-bot + become: true + ansible.builtin.systemd: + name: svc-bot.service + state: restarted + daemon_reload: true diff --git a/devserver/ansible/roles/svc-bot/tasks/main.yaml b/devserver/ansible/roles/svc-bot/tasks/main.yaml new file mode 100644 index 0000000..f9a308a --- /dev/null +++ b/devserver/ansible/roles/svc-bot/tasks/main.yaml @@ -0,0 +1,56 @@ +--- +# svc-bot — deploy + run the devICT slack bot docker compose stack, with +# secrets injected at runtime by the infisical CLI. +# +# Prerequisites (satisfied by playbook role order): +# - docker role: docker + compose plugin installed +# - infisical role: infisical CLI installed +# - svc-events role: writes the infisical env file this role's run.sh +# sources (the bot reuses the events machine identity — see +# defaults/main.yaml). + +- name: Ensure deploy directory exists + become: true + ansible.builtin.file: + path: "{{ svc_bot_deploy_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Render compose.yaml + become: true + ansible.builtin.template: + src: compose.yaml.j2 + dest: "{{ svc_bot_deploy_dir }}/compose.yaml" + owner: root + group: root + mode: "0644" + notify: restart svc-bot + +- name: Render run.sh + become: true + ansible.builtin.template: + src: run.sh.j2 + dest: "{{ svc_bot_deploy_dir }}/run.sh" + owner: root + group: root + mode: "0755" + notify: restart svc-bot + +- name: Install systemd unit + become: true + ansible.builtin.template: + src: svc-bot.service.j2 + dest: /etc/systemd/system/svc-bot.service + owner: root + group: root + mode: "0644" + notify: restart svc-bot + +- name: Enable svc-bot.service + become: true + ansible.builtin.systemd: + name: svc-bot.service + enabled: true + daemon_reload: true diff --git a/devserver/ansible/roles/svc-bot/templates/compose.yaml.j2 b/devserver/ansible/roles/svc-bot/templates/compose.yaml.j2 new file mode 100644 index 0000000..607b2f4 --- /dev/null +++ b/devserver/ansible/roles/svc-bot/templates/compose.yaml.j2 @@ -0,0 +1,10 @@ +services: + bot: + image: {{ svc_bot_image }} + restart: unless-stopped + ports: + - "{{ svc_bot_host_port }}:3000" + environment: + SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET} + SLACK_TOKEN: ${SLACK_TOKEN} + SLACK_ADMIN_TOKEN: ${SLACK_ADMIN_TOKEN} diff --git a/devserver/ansible/roles/svc-bot/templates/run.sh.j2 b/devserver/ansible/roles/svc-bot/templates/run.sh.j2 new file mode 100644 index 0000000..ac8fe4e --- /dev/null +++ b/devserver/ansible/roles/svc-bot/templates/run.sh.j2 @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Runs the bot compose stack with secrets injected at runtime by the +# infisical CLI. Invoked by svc-bot.service (and the deploy playbook). +# +# run.sh -> docker compose up +# run.sh pull -> docker compose pull +# run.sh -> docker compose +set -euo pipefail + +# Shared devserver infisical machine identity — written by the infisical +# role. Holds the client id + secret used by every service on this host. +# shellcheck source=/dev/null +source {{ infisical_env_file }} + +# Exchange the machine-identity credentials for a short-lived access +# token. Exported (not passed via --token=) so it never appears on a +# command line (visible via ps). +export INFISICAL_TOKEN="$(infisical login \ + --silent --plain \ + --method=universal-auth \ + --client-id="$INFISICAL_CLIENT_ID" \ + --client-secret="$INFISICAL_CLIENT_SECRET")" + +# infisical run injects the project's prod secrets into the environment +# of the spawned `docker compose` process, which (a) resolves ${VAR} +# interpolation in compose.yaml and (b) lets `environment:` copy values +# into the container. `docker compose up` runs in the foreground so +# systemd (Type=simple) supervises it directly. +exec infisical run \ + --env=prod \ + --projectId={{ infisical_project_id }} \ + -- docker compose "${@:-up}" diff --git a/devserver/ansible/roles/svc-bot/templates/svc-bot.service.j2 b/devserver/ansible/roles/svc-bot/templates/svc-bot.service.j2 new file mode 100644 index 0000000..7e527dc --- /dev/null +++ b/devserver/ansible/roles/svc-bot/templates/svc-bot.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=devICT slack bot (docker compose via infisical) +Requires=docker.service +After=docker.service network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory={{ svc_bot_deploy_dir }} +ExecStart={{ svc_bot_deploy_dir }}/run.sh +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/devserver/ansible/roles/svc-caddy/defaults/main.yaml b/devserver/ansible/roles/svc-caddy/defaults/main.yaml index 62aa557..3ab556d 100644 --- a/devserver/ansible/roles/svc-caddy/defaults/main.yaml +++ b/devserver/ansible/roles/svc-caddy/defaults/main.yaml @@ -2,7 +2,8 @@ # svc-caddy defaults. caddy_email: "{{ lookup('env', 'CADDY_EMAIL') }}" -# Hostname Caddy serves (and obtains a TLS cert for via ACME). -caddy_hostname: events.devict.org - +caddy_events_hostname: events.devict.org caddy_events_upstream: "localhost:3001" + +caddy_bot_hostname: bot.devict.org +caddy_bot_upstream: "localhost:3002" diff --git a/devserver/ansible/roles/svc-caddy/templates/Caddyfile.j2 b/devserver/ansible/roles/svc-caddy/templates/Caddyfile.j2 index 29c8f00..1668137 100644 --- a/devserver/ansible/roles/svc-caddy/templates/Caddyfile.j2 +++ b/devserver/ansible/roles/svc-caddy/templates/Caddyfile.j2 @@ -2,6 +2,10 @@ email {{ caddy_email }} } -{{ caddy_hostname }} { +{{ caddy_events_hostname }} { reverse_proxy {{ caddy_events_upstream }} } + +{{ caddy_bot_hostname }} { + reverse_proxy {{ caddy_bot_upstream }} +} diff --git a/devserver/ansible/roles/svc-events/defaults/main.yaml b/devserver/ansible/roles/svc-events/defaults/main.yaml index ca2bed2..f19ef59 100644 --- a/devserver/ansible/roles/svc-events/defaults/main.yaml +++ b/devserver/ansible/roles/svc-events/defaults/main.yaml @@ -1,13 +1,9 @@ --- -# svc-events: docker compose stack for the events service, secrets injected -# at runtime by the infisical CLI (no .env on disk). +# svc-events: docker compose stack for the events service, secrets +# injected at runtime by the infisical CLI (no .env on disk). +# +# Infisical credentials live in the infisical role (shared machine +# identity for the whole devserver). This role only owns its deploy dir. # Where the compose.yaml + run.sh live on the host. svc_events_deploy_dir: /opt/devict/events - -# Where the protected EnvironmentFile (client id + secret) is written. -svc_events_env_file: /etc/infisical/svc-events.env - -svc_events_infisical_client_id: "{{ lookup('env', 'SVC_EVENTS_INFISICAL_CLIENT_ID') }}" -svc_events_infisical_client_secret: "{{ lookup('env', 'SVC_EVENTS_INFISICAL_CLIENT_SECRET') }}" -svc_events_infisical_project_id: "{{ lookup('env', 'DEVSERVER_INFISICAL_PROJECT_ID') }}" diff --git a/devserver/ansible/roles/svc-events/tasks/main.yaml b/devserver/ansible/roles/svc-events/tasks/main.yaml index 9fd35ba..77b622f 100644 --- a/devserver/ansible/roles/svc-events/tasks/main.yaml +++ b/devserver/ansible/roles/svc-events/tasks/main.yaml @@ -3,8 +3,9 @@ # injected at runtime by the infisical CLI. # # Prerequisites (satisfied by playbook role order): -# - docker role: docker + compose plugin installed -# - infisical role: infisical CLI installed +# - docker role: docker + compose plugin installed +# - infisical role: infisical CLI installed + shared machine-identity +# env file written (sourced by run.sh) - name: Ensure deploy directory exists become: true @@ -15,15 +16,6 @@ group: root mode: "0755" -- name: Ensure infisical env file directory exists - become: true - ansible.builtin.file: - path: "{{ svc_events_env_file | dirname }}" - state: directory - owner: root - group: root - mode: "0755" - - name: Render compose.yaml become: true ansible.builtin.template: @@ -44,16 +36,6 @@ mode: "0755" notify: restart svc-events -- name: Render infisical EnvironmentFile (client id + secret, 0600) - become: true - ansible.builtin.template: - src: infisical.env.j2 - dest: "{{ svc_events_env_file }}" - owner: root - group: root - mode: "0600" - notify: restart svc-events - - name: Install systemd unit become: true ansible.builtin.template: diff --git a/devserver/ansible/roles/svc-events/templates/infisical.env.j2 b/devserver/ansible/roles/svc-events/templates/infisical.env.j2 deleted file mode 100644 index 800c40e..0000000 --- a/devserver/ansible/roles/svc-events/templates/infisical.env.j2 +++ /dev/null @@ -1,2 +0,0 @@ -SVC_EVENTS_INFISICAL_CLIENT_ID={{ svc_events_infisical_client_id }} -SVC_EVENTS_INFISICAL_CLIENT_SECRET={{ svc_events_infisical_client_secret }} diff --git a/devserver/ansible/roles/svc-events/templates/run.sh.j2 b/devserver/ansible/roles/svc-events/templates/run.sh.j2 index bd01bbd..e5cd98f 100644 --- a/devserver/ansible/roles/svc-events/templates/run.sh.j2 +++ b/devserver/ansible/roles/svc-events/templates/run.sh.j2 @@ -7,29 +7,26 @@ # run.sh -> docker compose set -euo pipefail -# Infisical machine-identity credentials. Sourced here (not via systemd -# EnvironmentFile=) so the script is self-contained when called outside -# systemd — e.g. by deploy-svc-events.yaml. +# Shared devserver infisical machine identity — written by the infisical +# role. Holds the client id + secret used by every service on this host. # shellcheck source=/dev/null -source {{ svc_events_env_file }} +source {{ infisical_env_file }} # Exchange the machine-identity credentials for a short-lived access -# token. We export into the environment rather than pass --token= so the -# token never appears on a command line (visible via ps). +# token. Exported (not passed via --token=) so it never appears on a +# command line (visible via ps). export INFISICAL_TOKEN="$(infisical login \ --silent --plain \ --method=universal-auth \ - --client-id="$SVC_EVENTS_INFISICAL_CLIENT_ID" \ - --client-secret="$SVC_EVENTS_INFISICAL_CLIENT_SECRET")" + --client-id="$INFISICAL_CLIENT_ID" \ + --client-secret="$INFISICAL_CLIENT_SECRET")" -# infisical run injects the project's secrets into the environment of -# the process it spawns (the `docker compose` CLI), which (a) resolves -# ${VAR} interpolation in compose.yaml and (b) lets `environment: [VAR]` -# copy values into the container. See compose.yaml.j2 for the var list. -# -# `docker compose up` runs in the foreground so systemd (Type=simple) -# supervises it directly. +# infisical run injects the project's prod secrets into the environment +# of the spawned `docker compose` process, which (a) resolves ${VAR} +# interpolation in compose.yaml and (b) lets `environment:` copy values +# into the container. `docker compose up` runs in the foreground so +# systemd (Type=simple) supervises it directly. exec infisical run \ --env=prod \ - --projectId={{ svc_events_infisical_project_id }} \ + --projectId={{ infisical_project_id }} \ -- docker compose "${@:-up}" diff --git a/devserver/ansible/site.yaml b/devserver/ansible/site.yaml index 0a15d0f..c87f83b 100644 --- a/devserver/ansible/site.yaml +++ b/devserver/ansible/site.yaml @@ -9,3 +9,4 @@ - infisical - svc-caddy - svc-events + - svc-bot diff --git a/devserver/justfile b/devserver/justfile index 9b27d71..793dd38 100644 --- a/devserver/justfile +++ b/devserver/justfile @@ -31,3 +31,7 @@ setup: # pull latest events image + restart the service deploy-events: cd ansible && ansible-playbook deploy-svc-events.yaml + +# pull latest bot image + restart the service +deploy-bot: + cd ansible && ansible-playbook deploy-svc-bot.yaml