This commit is contained in:
2026-07-08 05:59:11 -05:00
parent f848420d8f
commit 589462e6d0
27 changed files with 771 additions and 102 deletions
+88
View File
@@ -0,0 +1,88 @@
---
# Deploy one Django app+env as its own docker compose project.
# Called per item with loop_var app_item = { name, env, port }.
- name: "django[{{ app_item.name }}/{{ app_item.env }}] locals"
ansible.builtin.set_fact:
_spec: "{{ app_catalog[app_item.name] }}"
_src: "{{ apps_src_dir }}/{{ app_item.name }}_{{ app_item.env }}"
_envfile: "{{ apps_env_dir }}/{{ app_item.name }}_{{ app_item.env }}.env"
_secret_src: "{{ secrets_dir }}/{{ app_item.name }}/{{ app_item.name }}_{{ app_item.env }}.env"
_project: "{{ app_item.name }}_{{ app_item.env }}"
_ref: "{{ app_ref | default(app_catalog[app_item.name].default_branch) }}"
- name: "django[{{ _project }}] ensure base dirs"
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0750"
loop:
- "{{ apps_src_dir }}"
- "{{ apps_env_dir }}"
- name: "django[{{ _project }}] check local secret exists"
ansible.builtin.stat:
path: "{{ _secret_src }}"
register: _secret_stat
delegate_to: localhost
become: false
- name: "django[{{ _project }}] fail when local secret missing"
ansible.builtin.fail:
msg: >-
Missing local secret {{ _secret_src }} on the control node.
Create it (DATABASE_URL to the shared external Postgres, DJANGO_SECRET_KEY,
WEB_PORT={{ app_item.port }}, etc.) before deploying. It is never committed
to git.
when: not _secret_stat.stat.exists
- name: "django[{{ _project }}] push secret to {{ _envfile }}"
ansible.builtin.copy:
src: "{{ _secret_src }}"
dest: "{{ _envfile }}"
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0600"
- name: "django[{{ _project }}] checkout {{ _ref }}"
ansible.builtin.git:
repo: "{{ _spec.repo }}"
dest: "{{ _src }}"
version: "{{ _ref }}"
force: true
accept_hostkey: true
become: true
become_user: "{{ admin_user }}"
- name: "django[{{ _project }}] install .env into checkout"
ansible.builtin.copy:
src: "{{ _envfile }}"
dest: "{{ _src }}/.env"
remote_src: true
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0600"
- name: "django[{{ _project }}] build images"
ansible.builtin.command:
cmd: "docker compose -f {{ _spec.compose_file }} --env-file .env build"
chdir: "{{ _src }}"
environment:
COMPOSE_PROJECT_NAME: "{{ _project }}"
WEB_PORT: "{{ app_item.port }}"
become: true
become_user: "{{ admin_user }}"
changed_when: true
- name: "django[{{ _project }}] start containers"
ansible.builtin.command:
cmd: "docker compose -f {{ _spec.compose_file }} --env-file .env up -d --remove-orphans"
chdir: "{{ _src }}"
environment:
COMPOSE_PROJECT_NAME: "{{ _project }}"
WEB_PORT: "{{ app_item.port }}"
become: true
become_user: "{{ admin_user }}"
changed_when: true
+53 -6
View File
@@ -1,9 +1,56 @@
---
# Stub — implement after company_site is dockerized.
# Interim: can rsync/systemd like company_site/scripts/deploy.sh
# Generic app deployment.
#
# CI passes: app=<name> app_env=<beta|prod> app_ref=<git sha>
# Manual run with none of those redeploys every app in this host's host_apps
# at branch master.
#
# host_apps (host_vars) lists what runs on THIS host; app_catalog (group_vars)
# describes how each app is built.
- name: App deploy not yet implemented
- name: Classify catalog by type
ansible.builtin.set_fact:
django_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'django') | map(attribute='key') | list }}"
node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}"
- name: Resolve deploy targets for {{ inventory_hostname }}
ansible.builtin.set_fact:
deploy_targets: >-
{{ (host_apps | default([]) | selectattr('name', 'equalto', app | default('')) | selectattr('env', 'equalto', app_env | default('')) | list)
if (app is defined and app_env is defined)
else (host_apps | default([])) }}
- name: Show deploy targets
ansible.builtin.debug:
msg: >-
app-deploy role is a stub. Set app_ref={{ app_ref | default('unset') }}.
Implement git pull / docker compose after dockerize ticket.
msg: "ref={{ app_ref | default('(per-app default branch)') }} targets={{ deploy_targets | map(attribute='name') | zip(deploy_targets | map(attribute='env')) | list }}"
- name: Deploy Django (docker compose) apps
ansible.builtin.include_tasks: django.yml
loop: "{{ deploy_targets | selectattr('name', 'in', django_names) | list }}"
loop_control:
loop_var: app_item
label: "{{ app_item.name }}/{{ app_item.env }}"
- name: Run Django migrations once (shared external DB)
ansible.builtin.command:
cmd: >-
docker compose -f {{ app_catalog[item.name].compose_file }} --env-file .env
exec -T {{ app_catalog[item.name].web_service }}
{{ app_catalog[item.name].migrate_cmd }}
chdir: "{{ apps_src_dir }}/{{ item.name }}_{{ item.env }}"
environment:
COMPOSE_PROJECT_NAME: "{{ item.name }}_{{ item.env }}"
loop: "{{ deploy_targets | selectattr('name', 'in', django_names) | list }}"
loop_control:
label: "{{ item.name }}/{{ item.env }}"
become: true
become_user: "{{ admin_user }}"
run_once: true
changed_when: true
- name: Deploy node static apps
ansible.builtin.include_tasks: node_static.yml
loop: "{{ deploy_targets | selectattr('name', 'in', node_names) | list }}"
loop_control:
loop_var: app_item
label: "{{ app_item.name }}/{{ app_item.env }}"
+53
View File
@@ -0,0 +1,53 @@
---
# Build one node/vite static site into {{ web_static_root }}/<env>_<app>.
# Called per item with loop_var app_item = { name, env, port }.
# The app's own `npm run build:<env>` script is expected to output to
# {{ web_static_root }}/<env>_dta_webapp.
- name: "static[{{ app_item.name }}/{{ app_item.env }}] locals"
ansible.builtin.set_fact:
_spec: "{{ app_catalog[app_item.name] }}"
_src: "{{ apps_src_dir }}/{{ app_item.name }}_{{ app_item.env }}"
_webroot: "{{ (app_catalog[app_item.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ app_item.name)) | replace('{env}', app_item.env) }}"
_ref: "{{ app_ref | default(app_catalog[app_item.name].default_branch) }}"
- name: "static[{{ app_item.name }}/{{ app_item.env }}] workdir"
ansible.builtin.set_fact:
_workdir: "{{ (_src ~ '/' ~ _spec.subdir) if (_spec.subdir | default('')) else _src }}"
- name: "static[{{ app_item.name }}/{{ app_item.env }}] ensure dirs"
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0755"
loop:
- "{{ apps_src_dir }}"
- "{{ _webroot }}"
- name: "static[{{ app_item.name }}/{{ app_item.env }}] checkout {{ _ref }}"
ansible.builtin.git:
repo: "{{ _spec.repo }}"
dest: "{{ _src }}"
version: "{{ _ref }}"
force: true
accept_hostkey: true
become: true
become_user: "{{ admin_user }}"
- name: "static[{{ app_item.name }}/{{ app_item.env }}] npm ci"
ansible.builtin.command:
cmd: npm ci
chdir: "{{ _workdir }}"
become: true
become_user: "{{ admin_user }}"
changed_when: true
- name: "static[{{ app_item.name }}/{{ app_item.env }}] build ({{ app_item.env }})"
ansible.builtin.command:
cmd: "npm run build:{{ app_item.env }}"
chdir: "{{ _workdir }}"
become: true
become_user: "{{ admin_user }}"
changed_when: true
+7
View File
@@ -1,4 +1,11 @@
---
- name: Allow admin user passwordless sudo for Ansible
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ admin_user }}"
content: "{{ admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
mode: "0440"
validate: visudo -cf %s
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
+8 -17
View File
@@ -1,23 +1,14 @@
---
- name: Create keyrings directory
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Add Docker GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
- name: Add Docker apt repository
ansible.builtin.apt_repository:
repo: "deb [arch={{ docker_apt_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
ansible.builtin.deb822_repository:
name: docker
types: deb
uris: "https://download.docker.com/linux/{{ ansible_distribution | lower }}"
suites: "{{ ansible_distribution_release }}"
components: stable
architectures: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
signed_by: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
state: present
filename: docker
vars:
docker_apt_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
- name: Install Docker packages
ansible.builtin.apt:
+6
View File
@@ -0,0 +1,6 @@
---
gitea_ssh_host: git.aimloperations.com
gitea_ssh_port: 30009
gitea_key_path: "/home/{{ admin_user }}/.ssh/id_ed25519"
# Any repo the deploy user should be able to read; used only as an access probe.
gitea_test_repo: "ai_ml_operations/company_site.git"
+74
View File
@@ -0,0 +1,74 @@
---
# Per-server SSH key for cloning from Gitea.
# 1. Generate an ed25519 key for the deploy user (if absent).
# 2. Configure SSH for the Gitea host (port + identity).
# 3. Test access first; only surface the "add this key" step when it's missing.
- name: gitea-key | ensure .ssh dir
ansible.builtin.file:
path: "/home/{{ admin_user }}/.ssh"
state: directory
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0700"
- name: gitea-key | generate deploy key
become_user: "{{ admin_user }}"
ansible.builtin.command:
cmd: "ssh-keygen -t ed25519 -N '' -f {{ gitea_key_path }} -C '{{ admin_user }}@{{ inventory_hostname }}-gitea'"
creates: "{{ gitea_key_path }}"
- name: gitea-key | configure SSH for Gitea host
become_user: "{{ admin_user }}"
ansible.builtin.blockinfile:
path: "/home/{{ admin_user }}/.ssh/config"
create: true
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0600"
marker: "# {mark} ANSIBLE MANAGED gitea"
block: |
Host {{ gitea_ssh_host }}
User git
Port {{ gitea_ssh_port }}
IdentityFile {{ gitea_key_path }}
IdentitiesOnly yes
StrictHostKeyChecking accept-new
- name: gitea-key | read public key
ansible.builtin.slurp:
src: "{{ gitea_key_path }}.pub"
register: _gitea_pubkey
- name: gitea-key | check Gitea access (permission probe)
become_user: "{{ admin_user }}"
ansible.builtin.command:
cmd: "git ls-remote {{ git_base_url }}/{{ gitea_test_repo }}"
register: _gitea_access
failed_when: false
changed_when: false
- name: gitea-key | access OK
ansible.builtin.debug:
msg: "{{ inventory_hostname }} already has Gitea access; nothing to add."
when: _gitea_access.rc == 0
- name: gitea-key | ADD THIS KEY to Gitea (access missing)
ansible.builtin.debug:
msg: |
================= ACTION REQUIRED on {{ inventory_hostname }} =================
This server cannot reach Gitea yet. Add its public key:
{{ _gitea_pubkey.content | b64decode | trim }}
Where (either works):
* User key : Gitea > Settings > SSH / GPG Keys > Add Key
* Deploy key: repo > Settings > Deploy Keys (per repo, read-only)
Then re-run provisioning to continue.
==============================================================================
when: _gitea_access.rc != 0
- name: gitea-key | fail until key is added
ansible.builtin.fail:
msg: "No Gitea access from {{ inventory_hostname }}. Add the key shown above, then re-run."
when: _gitea_access.rc != 0
+3
View File
@@ -0,0 +1,3 @@
---
# Node.js major version to install from NodeSource (provides npm + npx).
node_major: 22
+52
View File
@@ -0,0 +1,52 @@
---
- name: Add NodeSource apt repository
ansible.builtin.deb822_repository:
name: nodesource
types: deb
uris: "https://deb.nodesource.com/node_{{ node_major }}.x"
suites: nodistro
components: main
signed_by: "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"
state: present
register: _nodesource_repo
- name: Force NodeSource over the distro nodejs/npm
ansible.builtin.copy:
dest: /etc/apt/preferences.d/nodesource.pref
owner: root
group: root
mode: "0644"
content: |
Package: *
Pin: origin "deb.nodesource.com"
Pin-Priority: 1001
register: _nodesource_pin
- name: Refresh apt cache after NodeSource changes
ansible.builtin.apt:
update_cache: true
when: _nodesource_repo is changed or _nodesource_pin is changed
- name: Remove distro nodejs/npm (NodeSource nodejs bundles its own npm + npx)
ansible.builtin.apt:
name:
- npm
- nodejs
state: absent
purge: true
- name: Install Node.js from NodeSource (includes npm + npx)
ansible.builtin.apt:
name: nodejs
state: latest
allow_downgrade: true
update_cache: true
- name: Verify node / npm / npx available
ansible.builtin.command:
cmd: "{{ item }} --version"
loop:
- node
- npm
- npx
changed_when: false
+6
View File
@@ -0,0 +1,6 @@
---
- name: Install Tianji monitoring reporter
ansible.builtin.shell: |
curl -o- "{{ tianji_install_script_url }}" | bash
args:
creates: /usr/lib/systemd/system/tianji-reporter.service
+3
View File
@@ -0,0 +1,3 @@
---
web_static_dir: "{{ apps_base_dir }}/web-static"
web_static_image: nginx:alpine
+57
View File
@@ -0,0 +1,57 @@
---
# Serves node/vite static builds (dta_webapp beta/prod) via a single nginx
# container. Skips itself on hosts that have no node-static apps.
- name: web-static | node-static app names
ansible.builtin.set_fact:
_node_names: "{{ app_catalog | dict2items | selectattr('value.type', 'equalto', 'node-static') | map(attribute='key') | list }}"
- name: web-static | this host's static apps
ansible.builtin.set_fact:
_static_apps: "{{ host_apps | default([]) | selectattr('name', 'in', _node_names) | list }}"
- name: web-static | configure and run
when: _static_apps | length > 0
block:
- name: web-static | ensure project dir
ansible.builtin.file:
path: "{{ web_static_dir }}"
state: directory
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0750"
- name: web-static | nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ web_static_dir }}/nginx.conf"
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0644"
register: _nginx_conf
- name: web-static | compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ web_static_dir }}/docker-compose.yml"
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
mode: "0644"
register: _compose_file
- name: web-static | start container
ansible.builtin.command:
cmd: "docker compose up -d --remove-orphans{{ ' --force-recreate' if (_compose_file is changed) else '' }}"
chdir: "{{ web_static_dir }}"
become: true
become_user: "{{ admin_user }}"
changed_when: true
- name: web-static | reload nginx on config change
ansible.builtin.command:
cmd: "docker compose exec -T web-static nginx -s reload"
chdir: "{{ web_static_dir }}"
become: true
become_user: "{{ admin_user }}"
when: _nginx_conf is changed and _compose_file is not changed
changed_when: true
@@ -0,0 +1,12 @@
# Managed by Ansible (roles/web-static). Do not edit by hand.
services:
web-static:
image: {{ web_static_image }}
restart: unless-stopped
ports:
{% for a in _static_apps %}
- "{{ a.port }}:{{ a.port }}"
{% endfor %}
volumes:
- {{ web_static_root }}:{{ web_static_root }}:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+19
View File
@@ -0,0 +1,19 @@
# Managed by Ansible (roles/web-static). Do not edit by hand.
{% for a in _static_apps %}
server {
listen {{ a.port }};
server_name _;
root {{ (app_catalog[a.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ a.name)) | replace('{env}', a.env) }};
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico)$ {
expires 7d;
add_header Cache-Control "public";
}
}
{% endfor %}