feat(infra): B-1 Ansible Host IaC 骨架完整版

- roles/nginx/templates/188-all-sites.conf.j2: 8 個服務 Jinja2 模板
- roles/docker-compose-service/tasks/main.yml: 通用 Docker Compose role
- roles/swap/tasks/main.yml: swap2.img 管理 role (110 專用)
- roles/pm2-service/tasks/main.yml: PM2 process 狀態確認 role
- .gitea/workflows/ansible-lint.yml: infra/ansible/** 異動自動 lint

Sprint B-1 完成: Git = 唯一真相 (Host IaC 骨架)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-11 02:47:10 +08:00
parent 44e8b22585
commit 0139aa79e7
14 changed files with 784 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
name: Ansible Lint
on:
push:
paths:
- 'infra/ansible/**'
pull_request:
paths:
- 'infra/ansible/**'
jobs:
lint:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install ansible-lint
run: pip install ansible-lint
- name: Run ansible-lint
run: ansible-lint infra/ansible/playbooks/
working-directory: ${{ github.workspace }}

View File

@@ -0,0 +1,20 @@
---
# AWOOOI Ansible — 共用變數
# 所有主機適用的基礎設定
# 時區
timezone: "Asia/Taipei"
# 共用 SSH 用戶
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o ConnectTimeout=10"
# sudoers NOPASSWD (CD 用see ADR-034)
sudo_password: "{{ vault_sudo_password }}"
# Harbor Registry
harbor_url: "harbor.wooo.work"
harbor_namespace: "awoooi"
# 網路
nginx_vip: "192.168.0.200"
k3s_vip: "192.168.0.125"

View File

@@ -0,0 +1,44 @@
---
# AWOOOI Ansible — 192.168.0.110 (DevOps 金庫) 專用變數
# Swap 設定
swap_files:
- path: /swap.img
size_mb: 3896 # 3.8GB (現有)
- path: /swap2.img
size_mb: 4096 # 4GB (Sprint A 新增)
# Docker Compose 服務 (在 /opt/ 下)
docker_compose_services:
harbor:
dir: /opt/harbor
service: "" # 全部服務
expected_port: 5000
sentry:
dir: /opt/sentry
service: ""
expected_port: 9000
gitea:
dir: /opt/gitea
service: ""
expected_port: 3001
langfuse:
dir: /opt/langfuse
service: ""
expected_port: 3100
# bitan pharmacy (Docker)
bitan_dir: /home/wooo/apps/bitan-pharmacy
bitan_port: 3003
# wooo-aiops frontend (PM2, port 3000)
wooo_aiops_dir: /home/wooo/apps/wooo-aiops
wooo_aiops_pm2_name: wooo-aiops
# GitHub Runners
github_runner_count: 5
# keepalived
keepalived_role: BACKUP
keepalived_priority: 90
keepalived_vip: "192.168.0.200"

View File

@@ -0,0 +1,43 @@
---
# AWOOOI Ansible — 192.168.0.188 (AI+Web 中心) 專用變數
# Docker Compose 服務
docker_compose_services:
openclaw:
dir: /opt/openclaw
expected_port: 8088
tsenyang:
dir: /opt/tsenyang-website
expected_port: 3000
momo:
dir: /opt/momo-app
expected_port: 5003
signoz:
dir: /opt/signoz
expected_port: 3301
minio:
dir: /opt/minio
expected_port: 9000
litellm:
dir: /opt/litellm
expected_port: 4000
n8n:
dir: /opt/n8n
expected_port: 5678
open_webui:
dir: /opt/open-webui
expected_port: 3010
docker_registry:
dir: /opt/docker-registry
expected_port: 5001
# Nginx
nginx_conf_dir: /etc/nginx/sites-enabled
nginx_main_conf: all-sites.conf
# 無 gitlab blockSprint A 已清除)
# keepalived
keepalived_role: MASTER
keepalived_priority: 100
keepalived_vip: "192.168.0.200"
keepalived_interface: "ens18" # 調整為實際網卡名稱

View File

@@ -0,0 +1,40 @@
---
# AWOOOI Ansible Inventory
# Sprint B — Host IaC
# 建立時間: 2026-04-11 (台北時區)
# 建立者: Claude Sonnet 4.6 — Sprint B-1
all:
children:
devops:
hosts:
host_110:
ansible_host: 192.168.0.110
ansible_user: wooo
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
ai_web:
hosts:
host_188:
ansible_host: 192.168.0.188
ansible_user: ollama
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
k3s_masters:
hosts:
host_120:
ansible_host: 192.168.0.120
ansible_user: wooo
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
host_121:
ansible_host: 192.168.0.121
ansible_user: wooo
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
security:
hosts:
host_112:
ansible_host: 192.168.0.112
ansible_user: wooo
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
ansible_skip_tags: "all" # 預留,本次不執行

View File

@@ -0,0 +1,172 @@
---
# AWOOOI Ansible — 192.168.0.110 DevOps 金庫完整狀態描述
# 執行: ansible-playbook -i inventory/hosts.yml playbooks/110-devops.yml
#
# 預期狀態:
# swap: 8GB (swap.img 3.8G + swap2.img 4G)
# harbor: running (docker compose, port 5000)
# sentry: running (docker compose, 0 restarting)
# gitea: running (port 3001)
# langfuse: running (port 3100)
# bitan: running docker (port 3003)
# wooo-aiops: running PM2 (port 3000)
# runners: 5x GitHub Actions runners (systemd)
# nginx: harbor conf -> 127.0.0.1:5000
# keepalived: BACKUP priority 90 VIP:200
- name: "192.168.0.110 DevOps 金庫 — 預期狀態收斂"
hosts: host_110
become: true
vars:
ansible_become_pass: "{{ vault_sudo_password | default(omit) }}"
tasks:
# ========================================================================
# Swap 檢查
# ========================================================================
- name: "Swap | 確認 swap.img 存在"
ansible.builtin.stat:
path: /swap.img
register: swap1_stat
tags: swap
- name: "Swap | 確認 swap2.img 存在"
ansible.builtin.stat:
path: /swap2.img
register: swap2_stat
tags: swap
- name: "Swap | 建立 swap2.img (若不存在)"
ansible.builtin.command:
cmd: "fallocate -l 4G /swap2.img"
creates: /swap2.img
when: not swap2_stat.stat.exists
tags: swap
- name: "Swap | 格式化 swap2.img (若剛建立)"
ansible.builtin.command:
cmd: "mkswap /swap2.img"
when: not swap2_stat.stat.exists
tags: swap
- name: "Swap | 設定 swap2.img 權限"
ansible.builtin.file:
path: /swap2.img
mode: "0600"
tags: swap
- name: "Swap | 加入 swap2.img 到 fstab"
ansible.builtin.lineinfile:
path: /etc/fstab
line: "/swap2.img none swap sw 0 0"
state: present
tags: swap
- name: "Swap | 啟用 swap"
ansible.builtin.command:
cmd: "swapon --all"
changed_when: false
tags: swap
# ========================================================================
# Docker 服務健康檢查
# ========================================================================
- name: "Docker | 確認 Harbor 容器運作中"
ansible.builtin.command:
cmd: "docker ps --filter name=harbor --filter status=running --format '{{ '{{' }}.Names{{ '}}' }}'"
register: harbor_status
changed_when: false
tags: docker
- name: "Docker | Harbor 狀態警告"
ansible.builtin.debug:
msg: "⚠️ Harbor 容器未完全運作,請手動檢查 /opt/harbor"
when: harbor_status.stdout == ""
tags: docker
- name: "Docker | 確認 Gitea 容器運作中"
ansible.builtin.command:
cmd: "docker ps --filter name=gitea --filter status=running --format '{{ '{{' }}.Names{{ '}}' }}'"
register: gitea_status
changed_when: false
tags: docker
- name: "Docker | 確認 Sentry 無 restart 容器"
ansible.builtin.command:
cmd: "docker ps --filter status=restarting --filter name=sentry --format '{{ '{{' }}.Names{{ '}}' }}'"
register: sentry_restarting
changed_when: false
tags: docker
- name: "Docker | Sentry crash loop 警告"
ansible.builtin.debug:
msg: "⚠️ Sentry 容器 crash loop: {{ sentry_restarting.stdout }}"
when: sentry_restarting.stdout != ""
tags: docker
# ========================================================================
# bitan pharmacy Docker 服務
# ========================================================================
- name: "bitan | 確認 bitan container 運作中"
ansible.builtin.command:
cmd: "docker ps --filter name=bitan --filter status=running --format '{{ '{{' }}.Names{{ '}}' }}'"
register: bitan_status
changed_when: false
tags: bitan
- name: "bitan | 若停止則啟動"
ansible.builtin.command:
cmd: "docker compose up -d"
chdir: /home/wooo/apps/bitan-pharmacy
when: bitan_status.stdout == ""
tags: bitan
# ========================================================================
# GitHub Runners
# ========================================================================
- name: "Runners | 確認 runner 服務狀態"
ansible.builtin.systemd:
name: "github-runner-{{ item }}"
register: runner_status
loop: "{{ range(1, github_runner_count + 1) | list }}"
ignore_errors: true
tags: runners
# ========================================================================
# keepalived
# ========================================================================
- name: "keepalived | 確認服務運作中"
ansible.builtin.systemd:
name: keepalived
register: keepalived_status
tags: keepalived
- name: "keepalived | 警告keepalived 未運作"
ansible.builtin.debug:
msg: "⚠️ keepalived 未運作VIP 200 可能失效"
when: keepalived_status.status.ActiveState != "active"
tags: keepalived
# ========================================================================
# Nginx harbor conf 指向確認
# ========================================================================
- name: "nginx | 確認 harbor nginx conf 存在"
ansible.builtin.stat:
path: /etc/nginx/sites-enabled/harbor.conf
register: harbor_nginx
tags: nginx
- name: "nginx | 確認 harbor conf 指向 :5000 (非 :5050)"
ansible.builtin.command:
cmd: "grep -c ':5050' /etc/nginx/sites-enabled/harbor.conf"
register: harbor_conf_check
changed_when: false
failed_when: false
when: harbor_nginx.stat.exists
tags: nginx
- name: "nginx | 警告harbor conf 仍指向 :5050"
ansible.builtin.debug:
msg: "⚠️ harbor nginx conf 仍有 :5050請確認已修正為 :5000"
when: harbor_nginx.stat.exists and harbor_conf_check.stdout != "0"
tags: nginx

View File

@@ -0,0 +1,135 @@
---
# AWOOOI Ansible — 192.168.0.188 AI+Web 中心完整狀態描述
# 執行: ansible-playbook -i inventory/hosts.yml playbooks/188-ai-web.yml
#
# 預期狀態:
# openclaw: running (port 8088)
# tsenyang: running (port 3000)
# momo: running (port 5003)
# signoz: running (port 3301)
# minio: running (port 9000)
# litellm: running (port 4000)
# n8n: running (port 5678)
# open-webui: running (port 3010)
# docker-registry: running (port 5001)
# nginx: all-sites.conf 無 gitlab block
# keepalived: MASTER priority 100 VIP:200
- name: "192.168.0.188 AI+Web 中心 — 預期狀態收斂"
hosts: host_188
become: true
vars:
ansible_become_pass: "{{ vault_sudo_password | default(omit) }}"
tasks:
# ========================================================================
# Docker 服務健康檢查
# ========================================================================
- name: "Docker | 收集運作中的容器清單"
ansible.builtin.command:
cmd: "docker ps --format '{{ '{{' }}.Names{{ '}}' }}'"
register: running_containers
changed_when: false
tags: docker
- name: "Docker | 確認關鍵服務運作中"
ansible.builtin.debug:
msg: "⚠️ 服務 {{ item }} 未在容器列表中,請確認"
when: item not in running_containers.stdout
loop:
- openclaw
- tsenyang
- momo
- signoz
- minio
- litellm
tags: docker
# ========================================================================
# n8n / open-webui (Sprint A 新啟動)
# ========================================================================
- name: "n8n | 確認容器運作中"
ansible.builtin.command:
cmd: "docker ps --filter name=n8n --filter status=running --format '{{ '{{' }}.Names{{ '}}' }}'"
register: n8n_status
changed_when: false
tags: n8n
- name: "n8n | 若停止則啟動"
ansible.builtin.command:
cmd: "docker compose up -d"
chdir: /opt/n8n
when: n8n_status.stdout == ""
tags: n8n
- name: "open-webui | 確認容器運作中"
ansible.builtin.command:
cmd: "docker ps --filter name=open-webui --filter status=running --format '{{ '{{' }}.Names{{ '}}' }}'"
register: openwebui_status
changed_when: false
tags: open_webui
- name: "open-webui | 若停止則啟動"
ansible.builtin.command:
cmd: "docker compose up -d"
chdir: /opt/open-webui
when: openwebui_status.stdout == ""
tags: open_webui
# ========================================================================
# Nginx 狀態確認
# ========================================================================
- name: "nginx | 確認服務運作中"
ansible.builtin.systemd:
name: nginx
register: nginx_status
tags: nginx
- name: "nginx | 警告nginx 未運作"
ansible.builtin.debug:
msg: "🚨 nginx 未運作!"
when: nginx_status.status.ActiveState != "active"
tags: nginx
- name: "nginx | 確認 all-sites.conf 無 gitlab block"
ansible.builtin.command:
cmd: "grep -c 'gitlab' /etc/nginx/sites-enabled/all-sites.conf"
register: gitlab_check
changed_when: false
failed_when: false
tags: nginx
- name: "nginx | 警告all-sites.conf 仍含 gitlab block"
ansible.builtin.debug:
msg: "⚠️ all-sites.conf 仍含 gitlab 設定,請確認 Sprint A 清除是否完整"
when: gitlab_check.stdout != "0"
tags: nginx
# ========================================================================
# keepalived MASTER
# ========================================================================
- name: "keepalived | 確認服務運作中"
ansible.builtin.systemd:
name: keepalived
register: keepalived_status
tags: keepalived
- name: "keepalived | 警告keepalived 未運作"
ansible.builtin.debug:
msg: "⚠️ keepalived MASTER 未運作VIP:200 降級為 110 BACKUP"
when: keepalived_status.status.ActiveState != "active"
tags: keepalived
- name: "keepalived | 確認 VIP:200 由本機持有"
ansible.builtin.command:
cmd: "ip addr show | grep 192.168.0.200"
register: vip_check
changed_when: false
failed_when: false
tags: keepalived
- name: "keepalived | 警告VIP:200 不在本機"
ansible.builtin.debug:
msg: "⚠️ VIP 192.168.0.200 不在 188 (MASTER 可能已 failover 到 110)"
when: vip_check.rc != 0
tags: keepalived

View File

@@ -0,0 +1,56 @@
---
# AWOOOI Ansible — Nginx 設定同步 Playbook
# 原則: Nginx conf 不再直接手改,所有修改透過此 Playbook 部署
# 執行: ansible-playbook -i inventory/hosts.yml playbooks/nginx-sync.yml --tags 188
# 乾跑: ansible-playbook -i inventory/hosts.yml playbooks/nginx-sync.yml --check
- name: "188 Nginx conf 同步"
hosts: host_188
become: true
vars:
ansible_become_pass: "{{ vault_sudo_password | default(omit) }}"
nginx_conf_src: "{{ playbook_dir }}/../roles/nginx/templates/188-all-sites.conf.j2"
nginx_conf_dest: /etc/nginx/sites-enabled/all-sites.conf
tasks:
- name: "nginx | 部署 all-sites.conf"
ansible.builtin.template:
src: "{{ nginx_conf_src }}"
dest: "{{ nginx_conf_dest }}"
owner: root
group: root
mode: "0644"
backup: true
notify: reload nginx
tags: ["188", "nginx"]
- name: "nginx | 測試設定"
ansible.builtin.command:
cmd: "nginx -t"
changed_when: false
tags: ["188", "nginx"]
handlers:
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: "110 Nginx conf 同步(若有)"
hosts: host_110
become: true
vars:
ansible_become_pass: "{{ vault_sudo_password | default(omit) }}"
tasks:
- name: "nginx | 確認 110 nginx 無 all-sites-from-188.conf 在 sites-enabled"
ansible.builtin.stat:
path: /etc/nginx/sites-enabled/all-sites-from-188.conf
register: stale_conf
tags: ["110", "nginx"]
- name: "nginx | 警告110 仍有 all-sites-from-188.conf (已非 188 管控)"
ansible.builtin.debug:
msg: "⚠️ 110 sites-enabled 仍有 all-sites-from-188.conf應已封存"
when: stale_conf.stat.exists
tags: ["110", "nginx"]

View File

@@ -0,0 +1,16 @@
---
# AWOOOI Ansible — 全站入口 Playbook
# 執行: ansible-playbook -i inventory/hosts.yml playbooks/site.yml
# 乾跑: ansible-playbook -i inventory/hosts.yml playbooks/site.yml --check
- name: "110 DevOps 金庫"
import_playbook: 110-devops.yml
tags: ["110", "devops"]
- name: "188 AI+Web 中心"
import_playbook: 188-ai-web.yml
tags: ["188", "ai_web"]
- name: "Nginx 設定同步"
import_playbook: nginx-sync.yml
tags: ["nginx"]

View File

@@ -0,0 +1,18 @@
---
# docker-compose-service role — 確保 Docker Compose 服務運作中
# 呼叫方式: include_role: name=docker-compose-service
# vars:
# service_name: n8n
# service_dir: /opt/n8n
- name: "{{ service_name }} | 確認容器運作中"
ansible.builtin.command:
cmd: "docker ps --filter name={{ service_name }} --filter status=running --format '{{ '{{' }}.Names{{ '}}' }}'"
register: _svc_status
changed_when: false
- name: "{{ service_name }} | 若停止則啟動"
ansible.builtin.command:
cmd: "docker compose up -d"
chdir: "{{ service_dir }}"
when: _svc_status.stdout == ""

View File

@@ -0,0 +1,16 @@
---
# nginx role — 任務主入口
# 由 nginx-sync.yml playbook 呼叫
- name: "確認 nginx 已安裝"
ansible.builtin.package:
name: nginx
state: present
tags: nginx
- name: "確認 nginx 服務啟動且自動啟動"
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
tags: nginx

View File

@@ -0,0 +1,145 @@
# 188-all-sites.conf.j2
# AWOOOI Nginx 全站設定 — 由 Ansible nginx-sync.yml playbook 管理
# 禁止直接手改此檔案 → 請修改 roles/nginx/templates/188-all-sites.conf.j2
# 部署指令: ansible-playbook -i inventory/hosts.yml playbooks/nginx-sync.yml --tags 188
# 最後同步: {{ ansible_date_time.iso8601 }}
# ============================================================
# OpenClaw (port 8088)
# ============================================================
server {
listen 80;
server_name openclaw.awoooi.com;
location / {
proxy_pass http://127.0.0.1:8088;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
}
# ============================================================
# tsenyang (port 3000)
# ============================================================
server {
listen 80;
server_name tsenyang.awoooi.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# ============================================================
# momo (port 5003)
# ============================================================
server {
listen 80;
server_name momo.awoooi.com;
location / {
proxy_pass http://127.0.0.1:5003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# ============================================================
# SignOz (port 3301)
# ============================================================
server {
listen 80;
server_name signoz.awoooi.internal;
location / {
proxy_pass http://127.0.0.1:3301;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
# ============================================================
# MinIO (port 9000 API / 9001 Console)
# ============================================================
server {
listen 80;
server_name minio.awoooi.internal;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 500m;
}
}
# ============================================================
# LiteLLM (port 4000)
# ============================================================
server {
listen 80;
server_name litellm.awoooi.internal;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
}
}
# ============================================================
# n8n (port 5678)
# ============================================================
server {
listen 80;
server_name n8n.awoooi.internal;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
# ============================================================
# Open WebUI (port 3010)
# ============================================================
server {
listen 80;
server_name open-webui.awoooi.internal;
location / {
proxy_pass http://127.0.0.1:3010;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
}
# ============================================================
# Docker Registry (port 5001)
# ============================================================
server {
listen 80;
server_name registry.awoooi.internal;
location / {
proxy_pass http://127.0.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 2g;
}
}

View File

@@ -0,0 +1,22 @@
---
# pm2-service role — 確保 PM2 管理的服務運作中
# 呼叫方式: include_role: name=pm2-service
# vars:
# pm2_app_name: wooo-aiops # PM2 process name
# pm2_user: wooo # 執行 PM2 的使用者
- name: "{{ pm2_app_name }} | 確認 PM2 process 狀態"
ansible.builtin.command:
cmd: "pm2 jlist"
become_user: "{{ pm2_user }}"
register: pm2_list
changed_when: false
- name: "{{ pm2_app_name }} | 解析 process 狀態"
ansible.builtin.set_fact:
_pm2_online: "{{ (pm2_list.stdout | from_json | selectattr('name', 'equalto', pm2_app_name) | selectattr('pm2_env.status', 'equalto', 'online') | list | length) > 0 }}"
- name: "{{ pm2_app_name }} | 警告:服務未 online"
ansible.builtin.debug:
msg: "⚠️ PM2 process '{{ pm2_app_name }}' 未 online請手動檢查"
when: not _pm2_online

View File

@@ -0,0 +1,35 @@
---
# swap role — 確保 swap 已設定110 專用)
# 預期狀態: swap.img (3.8G) + swap2.img (4G) 共 ~8G
- name: "Swap | 確認 swap2.img 存在"
ansible.builtin.stat:
path: /swap2.img
register: swap2_stat
- name: "Swap | 建立 swap2.img"
ansible.builtin.command:
cmd: "fallocate -l 4G /swap2.img"
creates: /swap2.img
when: not swap2_stat.stat.exists
- name: "Swap | 格式化 swap2.img"
ansible.builtin.command:
cmd: "mkswap /swap2.img"
when: not swap2_stat.stat.exists
- name: "Swap | 設定 swap2.img 權限"
ansible.builtin.file:
path: /swap2.img
mode: "0600"
- name: "Swap | 加入 swap2.img 到 fstab"
ansible.builtin.lineinfile:
path: /etc/fstab
line: "/swap2.img none swap sw 0 0"
state: present
- name: "Swap | 啟用 swap"
ansible.builtin.command:
cmd: "swapon --all"
changed_when: false