diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..791729a --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "Deploy (dry run)", + "runtimeExecutable": "ansible-playbook", + "runtimeArgs": ["playbooks/deploy.yml", "--check", "-i", "inventory/"], + "port": 0 + }, + { + "name": "Deploy", + "runtimeExecutable": "ansible-playbook", + "runtimeArgs": ["playbooks/deploy.yml", "-i", "inventory/"], + "port": 0 + }, + { + "name": "Syntax check", + "runtimeExecutable": "ansible-playbook", + "runtimeArgs": ["playbooks/deploy.yml", "--syntax-check", "-i", "inventory/"], + "port": 0 + }, + { + "name": "Bootstrap (first-time, as root)", + "runtimeExecutable": "ansible-playbook", + "runtimeArgs": ["playbooks/bootstrap.yml", "-u", "root", "-i", "inventory/"], + "port": 0 + } + ] +} diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..8db845b --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,51 @@ +name: CI/CD + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + syntax-check: + runs-on: ubuntu-latest + container: + image: python:3.12-slim + steps: + - uses: actions/checkout@v4 + + - name: Install ansible + run: pip install ansible --quiet + + - name: Syntax check + run: ansible-playbook playbooks/deploy.yml --syntax-check -i inventory/ + + deploy: + needs: syntax-check + if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'push' }} + runs-on: ubuntu-latest + container: + image: python:3.12-slim + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt-get update -qq && apt-get install -y openssh-client --no-install-recommends + pip install ansible --quiet + ansible-galaxy collection install ansible.posix community.general community.docker --quiet + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -p 22 87.249.49.32 >> ~/.ssh/known_hosts + + - name: Write vault password + run: | + echo "${{ secrets.VAULT_PASSWORD }}" > ~/.vault-password-file + chmod 600 ~/.vault-password-file + + - name: Deploy + run: ansible-playbook playbooks/deploy.yml -i inventory/ diff --git a/inventory/group_vars/all/main.yml b/inventory/group_vars/all/main.yml index d13d8c9..380d52a 100644 --- a/inventory/group_vars/all/main.yml +++ b/inventory/group_vars/all/main.yml @@ -23,3 +23,7 @@ plane_secret_key: "{{ vault_plane_secret_key }}" plane_minio_password: "{{ vault_plane_minio_password }}" traefik_dashboard_htpasswd: "{{ vault_traefik_dashboard_htpasswd }}" syncthing_basic_auth_htpasswd: "{{ vault_syncthing_basic_auth_htpasswd }}" +forgejo_runner_token: "{{ vault_forgejo_runner_token }}" + +# CI/CD deploy key (public key — not a secret) +ci_deploy_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF6kK8+/9cMo9sFUIQAupPfcD3A6UixmAzB0r8jAf0kz ci-deploy@forgejo-runner" diff --git a/roles/base/tasks/users.yml b/roles/base/tasks/users.yml index 574e532..1c4e47f 100644 --- a/roles/base/tasks/users.yml +++ b/roles/base/tasks/users.yml @@ -20,3 +20,9 @@ create: true mode: "0440" validate: "visudo -cf %s" + +- name: Add CI deploy public key to authorized_keys + ansible.posix.authorized_key: + user: "{{ deploy_user }}" + state: present + key: "{{ ci_deploy_pubkey }}" diff --git a/roles/services/defaults/main.yml b/roles/services/defaults/main.yml index 857555a..d4381b5 100644 --- a/roles/services/defaults/main.yml +++ b/roles/services/defaults/main.yml @@ -17,3 +17,4 @@ plane_redis_image: "redis:7-alpine" # Рекомендуется перейти на alpine/minio или собирать из исходников. plane_minio_image: "minio/minio:RELEASE.2025-04-22T22-12-26Z" # https://hub.docker.com/r/minio/minio/tags syncthing_image: "syncthing/syncthing:1.27" # https://hub.docker.com/r/syncthing/syncthing/tags +act_runner_image: "gitea/act_runner:0.3.0" # https://hub.docker.com/r/gitea/act_runner/tags diff --git a/roles/services/tasks/configs.yml b/roles/services/tasks/configs.yml index f15ccce..949c42e 100644 --- a/roles/services/tasks/configs.yml +++ b/roles/services/tasks/configs.yml @@ -26,6 +26,24 @@ mode: "0644" notify: Restart stack +- name: Deploy Traefik dynamic routes + ansible.builtin.template: + src: traefik/dynamic/routes.yml.j2 + dest: "{{ services_root }}/traefik/dynamic/routes.yml" + owner: "{{ deploy_user }}" + group: "{{ deploy_group }}" + mode: "0644" + notify: Restart stack + +- name: Deploy act_runner config + ansible.builtin.template: + src: act_runner_config.yaml.j2 + dest: "{{ services_root }}/act_runner/config.yaml" + owner: "{{ deploy_user }}" + group: "{{ deploy_group }}" + mode: "0644" + notify: Restart stack + - name: Create acme.json for Let's Encrypt certificates ansible.builtin.file: path: "{{ services_root }}/traefik/acme.json" diff --git a/roles/services/tasks/directories.yml b/roles/services/tasks/directories.yml index 373dd7a..070b637 100644 --- a/roles/services/tasks/directories.yml +++ b/roles/services/tasks/directories.yml @@ -24,3 +24,4 @@ - plane/media - syncthing/config - syncthing/data + - act_runner diff --git a/roles/services/tasks/main.yml b/roles/services/tasks/main.yml index 3cf1b82..0d4b023 100644 --- a/roles/services/tasks/main.yml +++ b/roles/services/tasks/main.yml @@ -15,6 +15,7 @@ - "{{ plane_redis_image }}" - "{{ plane_minio_image }}" - "{{ syncthing_image }}" + - "{{ act_runner_image }}" register: pull_result changed_when: "'Status: Downloaded newer image' in pull_result.stdout" retries: 5 diff --git a/roles/services/templates/act_runner_config.yaml.j2 b/roles/services/templates/act_runner_config.yaml.j2 new file mode 100644 index 0000000..49f5171 --- /dev/null +++ b/roles/services/templates/act_runner_config.yaml.j2 @@ -0,0 +1,23 @@ +# Generated by Ansible — do not edit manually +log: + level: info + +runner: + capacity: 2 + timeout: 3h + insecure: false + +cache: + enabled: true + +container: + # Job containers run on runner-jobs network (has internet access) + network: runner-jobs + privileged: false + valid_volumes: + - "**" + docker_host: "" + force_pull: false + +host: + workdir_parent: diff --git a/roles/services/templates/docker-compose.yml.j2 b/roles/services/templates/docker-compose.yml.j2 index d2e4a55..1d3be45 100644 --- a/roles/services/templates/docker-compose.yml.j2 +++ b/roles/services/templates/docker-compose.yml.j2 @@ -13,9 +13,13 @@ networks: forgejo-db: driver: bridge internal: true + forgejo-ssh: + driver: bridge plane-internal: driver: bridge internal: true + runner-jobs: + driver: bridge volumes: vaultwarden_data: @@ -27,6 +31,7 @@ volumes: plane_media: syncthing_config: syncthing_data: + act_runner_data: services: @@ -39,13 +44,10 @@ services: ports: - "80:80" - "443:443" - environment: - - DOCKER_API_VERSION=1.45 networks: - proxy - backend volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - {{ services_root }}/traefik/traefik.yml:/etc/traefik/traefik.yml:ro - {{ services_root }}/traefik/dynamic:/etc/traefik/dynamic:ro - {{ services_root }}/traefik/acme.json:/acme/acme.json @@ -93,6 +95,7 @@ services: networks: - backend - forgejo-db + - forgejo-ssh volumes: - forgejo_data:/data - /etc/timezone:/etc/timezone:ro @@ -332,3 +335,25 @@ services: - "traefik.http.routers.syncthing.middlewares=syncthing-auth" - "traefik.http.middlewares.syncthing-auth.basicauth.users={{ syncthing_basic_auth_htpasswd }}" - "traefik.http.services.syncthing.loadbalancer.server.port=8384" + + # ── Forgejo Actions Runner ───────────────────────────────────────────────── + # backend — для связи с Forgejo по внутренней сети (http://forgejo:3000) + # runner-jobs — сеть с интернет-доступом для job-контейнеров + act_runner: + image: {{ act_runner_image }} + container_name: act_runner + restart: unless-stopped + depends_on: + - forgejo + environment: + - GITEA_INSTANCE_URL=http://forgejo:3000 + - GITEA_RUNNER_REGISTRATION_TOKEN=${FORGEJO_RUNNER_TOKEN} + - GITEA_RUNNER_NAME=vps-runner + - CONFIG_FILE=/data/config.yaml + volumes: + - act_runner_data:/data + - /var/run/docker.sock:/var/run/docker.sock + - {{ services_root }}/act_runner/config.yaml:/data/config.yaml:ro + networks: + - backend + - runner-jobs diff --git a/roles/services/templates/env.j2 b/roles/services/templates/env.j2 index 66e0453..a250ac8 100644 --- a/roles/services/templates/env.j2 +++ b/roles/services/templates/env.j2 @@ -10,3 +10,4 @@ DOMAIN_GIT={{ domain_git }} DOMAIN_PLANE={{ domain_plane }} DOMAIN_SYNC={{ domain_sync }} DOMAIN_TRAEFIK={{ domain_traefik }} +FORGEJO_RUNNER_TOKEN={{ forgejo_runner_token }} diff --git a/roles/services/templates/traefik/dynamic/routes.yml.j2 b/roles/services/templates/traefik/dynamic/routes.yml.j2 new file mode 100644 index 0000000..01192e5 --- /dev/null +++ b/roles/services/templates/traefik/dynamic/routes.yml.j2 @@ -0,0 +1,85 @@ +# Traefik dynamic routing config — generated by Ansible +# Do not edit manually; re-run ansible-playbook deploy.yml + +http: + routers: + traefik-dashboard: + rule: "Host(`{{ domain_traefik }}`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: api@internal + middlewares: [traefik-auth] + + vaultwarden: + rule: "Host(`{{ domain_vault }}`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: vaultwarden + + forgejo: + rule: "Host(`{{ domain_git }}`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: forgejo + + plane-api: + rule: "Host(`{{ domain_plane }}`) && (PathPrefix(`/api/`) || PathPrefix(`/auth/`))" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: plane-api + + plane: + rule: "Host(`{{ domain_plane }}`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: plane-web + + syncthing: + rule: "Host(`{{ domain_sync }}`)" + entrypoints: [websecure] + tls: + certresolver: letsencrypt + service: syncthing + middlewares: [syncthing-auth] + + services: + vaultwarden: + loadBalancer: + servers: + - url: "http://vaultwarden:80" + + forgejo: + loadBalancer: + servers: + - url: "http://forgejo:3000" + + plane-api: + loadBalancer: + servers: + - url: "http://plane-api:8000" + + plane-web: + loadBalancer: + servers: + - url: "http://plane-web:3000" + + syncthing: + loadBalancer: + servers: + - url: "http://syncthing:8384" + + middlewares: + traefik-auth: + basicAuth: + users: + - "{{ traefik_dashboard_htpasswd }}" + + syncthing-auth: + basicAuth: + users: + - "{{ syncthing_basic_auth_htpasswd }}" diff --git a/roles/services/templates/traefik/traefik.yml.j2 b/roles/services/templates/traefik/traefik.yml.j2 index 7362020..ea71d08 100644 --- a/roles/services/templates/traefik/traefik.yml.j2 +++ b/roles/services/templates/traefik/traefik.yml.j2 @@ -34,9 +34,6 @@ certificatesResolvers: entryPoint: web providers: - docker: - exposedByDefault: false - network: backend file: directory: /etc/traefik/dynamic watch: true