commit e85158fc044b4b791350efc18acbba4b90a2e92a Author: Chezlepro Date: Wed Oct 8 16:03:46 2025 -0400 Initial commit: Traefik deployment diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..3ec254c --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,81 @@ +name: Build Traefik from Source + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + traefik_version: + description: 'Version de Traefik à compiler (ex: v3.2.0)' + required: true + default: 'v3.2.0' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Determine Traefik version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.traefik_version }}" + else + VERSION="${{ github.ref_name }}" + fi + echo "traefik_version=$VERSION" >> $GITHUB_OUTPUT + echo "🏷️ Version Traefik: $VERSION" + + - name: Clone Traefik repository + run: | + git clone --depth 1 --branch ${{ steps.version.outputs.traefik_version }} https://github.com/traefik/traefik.git traefik-src + cd traefik-src + echo "📦 Traefik ${{ steps.version.outputs.traefik_version }} cloné" + + - name: Build Traefik + run: | + cd traefik-src + export CGO_ENABLED=0 + make generate + make binary + echo "✅ Compilation terminée" + + - name: Verify binary + run: | + cd traefik-src + ./dist/traefik version + ls -lh ./dist/traefik + + - name: Create release archive + run: | + mkdir -p release + cp traefik-src/dist/traefik release/ + cd release + tar -czf traefik-${{ steps.version.outputs.traefik_version }}-linux-amd64.tar.gz traefik + sha256sum traefik-${{ steps.version.outputs.traefik_version }}-linux-amd64.tar.gz > traefik-${{ steps.version.outputs.traefik_version }}-linux-amd64.tar.gz.sha256 + echo "📦 Archive créée" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: traefik-${{ steps.version.outputs.traefik_version }} + path: release/* + retention-days: 90 + + - name: Create Release (si tag) + if: startsWith(github.ref, 'refs/tags/') + uses: actions/forgejo-release@v2 + with: + direction: upload + token: ${{ secrets.GITHUB_TOKEN }} + release-dir: release + tag: ${{ github.ref_name }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8831fcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.retry +inventory.ini +__pycache__/ +*.py[cod] +venv/ +*.key +*.pem +acme.json +*.log +.DS_Store +*.tar.gz \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..89fb5cb --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +.PHONY: help install-collections setup test-connection deploy prod rollback clean build-forgejo + +PYTHON := python3 +ANSIBLE := ansible-playbook +INVENTORY := inventory.ini + +help: + @echo "Commandes disponibles:" + @echo " make install-collections - Installe Ansible" + @echo " make setup - Configure le déploiement" + @echo " make test-connection - Teste la connexion SSH" + @echo " make build-forgejo - Déclenche la compilation sur Forgejo" + @echo " make deploy - Déploie Traefik (blue/green)" + @echo " make rollback - Revient à la version précédente" + @echo " make status - Affiche le statut" + @echo " make clean - Nettoie" + +install-collections: + @echo "📦 Installation d'Ansible..." + @$(PYTHON) -m pip install ansible jinja2 pyyaml cryptography requests + @echo "✓ Ansible installé" + +setup: + @echo "🔧 Configuration..." + @$(PYTHON) scripts/setup.py + +test-connection: + @echo "🔍 Test de connexion..." + @ansible all -i $(INVENTORY) -m ping + +build-forgejo: + @echo "🏗️ Déclenchement de la compilation sur Forgejo..." + @$(PYTHON) scripts/trigger_build.py + +deploy: + @echo "🚀 Déploiement Traefik..." + @$(ANSIBLE) -i $(INVENTORY) ansible/deploy-traefik.yml + +prod: install-collections setup test-connection deploy + @echo "✅ Déploiement complet terminé!" + +rollback: + @echo "⏮️ Rollback..." + @$(ANSIBLE) -i $(INVENTORY) ansible/rollback-traefik.yml + +status: + @echo "📊 Statut..." + @ansible all -i $(INVENTORY) -m shell -a "systemctl status traefik-*" + +clean: + @echo "🧹 Nettoyage..." + @rm -rf ansible/*.retry + @rm -f /tmp/traefik_current_color \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..367886c --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Traefik Deployment avec Blue/Green + +Projet de déploiement Traefik avec compilation sur Forgejo et déploiement sécurisé. + +## Architecture + +- **Forgejo** (http://eregion.chezlepro.ca:3000) : Compilation depuis sources +- **Serveur prod** : Binaire uniquement (sécurité maximale) +- **Stratégie** : Blue/Green deployment + +## Quick Start + +```bash +# 1. Installer Ansible +make install-collections + +# 2. Configurer +make setup + +# 3. Tester la connexion +make test-connection + +# 4. Compiler sur Forgejo (~10-15 min) +make build-forgejo + +# 5. Déployer +make deploy +``` + +## Commandes + +- `make help` - Affiche l'aide +- `make build-forgejo` - Compile sur Forgejo +- `make deploy` - Déploie en Blue/Green +- `make rollback` - Rollback instantané +- `make status` - Statut des services + +## Sécurité + +Le serveur de production ne contient : +- ❌ Aucun outil de compilation +- ❌ Aucun code source +- ✅ Binaire Traefik uniquement + +## Support + +Instance Forgejo : http://eregion.chezlepro.ca:3000 +Dépôt : http://eregion.chezlepro.ca:3000/Chezlepro/traefik-deploy \ No newline at end of file diff --git a/ansible/deploy-traefik.yml b/ansible/deploy-traefik.yml new file mode 100644 index 0000000..747bac1 --- /dev/null +++ b/ansible/deploy-traefik.yml @@ -0,0 +1,23 @@ +--- +- name: Deploy Traefik (Blue/Green) + hosts: traefik_servers + become: yes + vars: + traefik_version: "{{ traefik_version | default('v3.2.0') }}" + traefik_base_dir: "/opt/traefik" + current_color_file: "/tmp/traefik_current_color" + + pre_tasks: + - name: Read current color + slurp: + src: "{{ current_color_file }}" + register: current_color_raw + ignore_errors: yes + + - name: Set colors + set_fact: + current_color: "{{ current_color_raw.content | b64decode | trim if current_color_raw.content is defined else 'blue' }}" + new_color: "{{ 'green' if (current_color_raw.content | b64decode | trim if current_color_raw.content is defined else 'blue') == 'blue' else 'blue' }}" + + roles: + - role: traefik \ No newline at end of file diff --git a/ansible/roles/traefik/handlers/main.yml b/ansible/roles/traefik/handlers/main.yml new file mode 100644 index 0000000..9f4fae0 --- /dev/null +++ b/ansible/roles/traefik/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: reload systemd + systemd: + daemon_reload: yes \ No newline at end of file diff --git a/ansible/roles/traefik/tasks/main.yml b/ansible/roles/traefik/tasks/main.yml new file mode 100644 index 0000000..dd294c0 --- /dev/null +++ b/ansible/roles/traefik/tasks/main.yml @@ -0,0 +1,93 @@ +--- +- name: Install minimal dependencies + apt: + name: + - curl + - ca-certificates + state: present + update_cache: yes + when: ansible_os_family == "Debian" + +- name: Create directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ traefik_base_dir }}/{{ new_color }}" + - "{{ traefik_base_dir }}/{{ new_color }}/config" + - "{{ traefik_base_dir }}/{{ new_color }}/logs" + +- name: Create acme.json + file: + path: "{{ traefik_base_dir }}/{{ new_color }}/acme.json" + state: touch + mode: '0600' + +- name: Download binary from Forgejo + get_url: + url: "{{ forgejo_url }}/{{ forgejo_owner }}/{{ forgejo_repo }}/releases/download/{{ traefik_version }}/traefik-{{ traefik_version }}-linux-amd64.tar.gz" + dest: "/tmp/traefik_{{ new_color }}.tar.gz" + mode: '0644' + force: yes + +- name: Extract binary + unarchive: + src: "/tmp/traefik_{{ new_color }}.tar.gz" + dest: "{{ traefik_base_dir }}/{{ new_color }}" + remote_src: yes + +- name: Make executable + file: + path: "{{ traefik_base_dir }}/{{ new_color }}/traefik" + mode: '0755' + +- name: Copy static config + template: + src: traefik.yml.j2 + dest: "{{ traefik_base_dir }}/{{ new_color }}/config/traefik.yml" + mode: '0644' + +- name: Copy dynamic config + template: + src: dynamic.yml.j2 + dest: "{{ traefik_base_dir }}/{{ new_color }}/config/dynamic.yml" + mode: '0644' + +- name: Create systemd service + template: + src: traefik.service.j2 + dest: "/etc/systemd/system/traefik-{{ new_color }}.service" + mode: '0644' + +- name: Reload systemd + systemd: + daemon_reload: yes + +- name: Start new instance + systemd: + name: "traefik-{{ new_color }}" + state: started + enabled: yes + +- name: Wait for health check + uri: + url: "http://localhost:8080/ping" + status_code: 200 + register: result + until: result.status == 200 + retries: 30 + delay: 2 + +- name: Stop old instance + systemd: + name: "traefik-{{ current_color }}" + state: stopped + enabled: no + when: current_color != new_color + ignore_errors: yes + +- name: Update color marker + copy: + content: "{{ new_color }}" + dest: "{{ current_color_file }}" \ No newline at end of file diff --git a/ansible/roles/traefik/templates/dynamic.yml.j2 b/ansible/roles/traefik/templates/dynamic.yml.j2 new file mode 100644 index 0000000..c3098ec --- /dev/null +++ b/ansible/roles/traefik/templates/dynamic.yml.j2 @@ -0,0 +1,26 @@ +http: + routers: + forgejo: + rule: "Host(`git.example.com`)" + entryPoints: + - websecure + service: forgejo-service + tls: + certResolver: letsencrypt + + services: + forgejo-service: + loadBalancer: + servers: + - url: "http://localhost:3000" + + middlewares: + security-headers: + headers: + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 31536000 + customFrameOptionsValue: "SAMEORIGIN" \ No newline at end of file diff --git a/ansible/roles/traefik/templates/traefik.service.j2 b/ansible/roles/traefik/templates/traefik.service.j2 new file mode 100644 index 0000000..38567a4 --- /dev/null +++ b/ansible/roles/traefik/templates/traefik.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=Traefik {{ new_color }} +Documentation=https://doc.traefik.io +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +Group=root +ExecStart={{ traefik_base_dir }}/{{ new_color }}/traefik --configFile={{ traefik_base_dir }}/{{ new_color }}/config/traefik.yml +Restart=always +RestartSec=5 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ traefik_base_dir }}/{{ new_color }} +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/ansible/roles/traefik/templates/traefik.yml.j2 b/ansible/roles/traefik/templates/traefik.yml.j2 new file mode 100644 index 0000000..7bbb034 --- /dev/null +++ b/ansible/roles/traefik/templates/traefik.yml.j2 @@ -0,0 +1,44 @@ +global: + checkNewVersion: true + sendAnonymousUsage: false + +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: ":443" + http: + tls: + certResolver: letsencrypt + +certificatesResolvers: + letsencrypt: + acme: + email: {{ letsencrypt_email }} + storage: {{ traefik_base_dir }}/{{ new_color }}/acme.json + httpChallenge: + entryPoint: web + +providers: + file: + filename: {{ traefik_base_dir }}/{{ new_color }}/config/dynamic.yml + watch: true + +log: + level: INFO + filePath: {{ traefik_base_dir }}/{{ new_color }}/logs/traefik.log + +accessLog: + filePath: {{ traefik_base_dir }}/{{ new_color }}/logs/access.log + +ping: + entryPoint: "web" \ No newline at end of file diff --git a/ansible/rollback-traefik.yml b/ansible/rollback-traefik.yml new file mode 100644 index 0000000..d6c8606 --- /dev/null +++ b/ansible/rollback-traefik.yml @@ -0,0 +1,43 @@ +--- +- name: Rollback Traefik + hosts: traefik_servers + become: yes + vars: + current_color_file: "/tmp/traefik_current_color" + + tasks: + - name: Read current color + slurp: + src: "{{ current_color_file }}" + register: current_color_raw + ignore_errors: yes + + - name: Set rollback color + set_fact: + current_color: "{{ current_color_raw.content | b64decode | trim if current_color_raw.content is defined else 'blue' }}" + rollback_color: "{{ 'green' if (current_color_raw.content | b64decode | trim if current_color_raw.content is defined else 'blue') == 'blue' else 'blue' }}" + + - name: Stop current + systemd: + name: "traefik-{{ current_color }}" + state: stopped + + - name: Start previous + systemd: + name: "traefik-{{ rollback_color }}" + state: started + enabled: yes + + - name: Update marker + copy: + content: "{{ rollback_color }}" + dest: "{{ current_color_file }}" + + - name: Wait for health + uri: + url: "http://localhost:8080/ping" + status_code: 200 + register: result + until: result.status == 200 + retries: 10 + delay: 2 \ No newline at end of file diff --git a/scripts/setup.py b/scripts/setup.py new file mode 100755 index 0000000..96934d3 --- /dev/null +++ b/scripts/setup.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import sys +from pathlib import Path + +def setup(): + print("=== Configuration du déploiement Traefik ===\n") + + server_ip = input("IP du serveur de production: ").strip() + if not server_ip: + print("❌ IP requise") + sys.exit(1) + + ssh_key = input("Clé SSH [~/.ssh/id_rsa]: ").strip() + if not ssh_key: + ssh_key = str(Path.home() / ".ssh" / "id_rsa") + + ssh_key_path = Path(ssh_key).expanduser() + if not ssh_key_path.exists(): + print(f"❌ Clé SSH introuvable: {ssh_key_path}") + sys.exit(1) + + ssh_user = input("Utilisateur SSH [root]: ").strip() or "root" + letsencrypt_email = input("Email Let's Encrypt: ").strip() + if not letsencrypt_email: + print("❌ Email requis") + sys.exit(1) + + traefik_version = input("Version Traefik [v3.2.0]: ").strip() or "v3.2.0" + + inventory_content = f"""[traefik_servers] +traefik_prod ansible_host={server_ip} ansible_user={ssh_user} ansible_ssh_private_key_file={ssh_key_path} + +[traefik_servers:vars] +ansible_python_interpreter=/usr/bin/python3 +letsencrypt_email={letsencrypt_email} +traefik_version={traefik_version} +forgejo_url=http://eregion.chezlepro.ca:3000 +forgejo_owner=Chezlepro +forgejo_repo=traefik-deploy +""" + + Path("inventory.ini").write_text(inventory_content) + print(f"\n✓ Configuration créée: inventory.ini") + print("\nProchaines étapes:") + print(" make test-connection") + print(" make build-forgejo") + print(" make deploy") + +if __name__ == "__main__": + setup() \ No newline at end of file diff --git a/scripts/trigger_build.py b/scripts/trigger_build.py new file mode 100755 index 0000000..14b276b --- /dev/null +++ b/scripts/trigger_build.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +import sys +import requests +import configparser +from pathlib import Path +import getpass + +def read_inventory(): + config = configparser.ConfigParser() + config.read('inventory.ini') + + if 'traefik_servers:vars' in config: + return { + 'forgejo_url': config['traefik_servers:vars'].get('forgejo_url', 'http://eregion.chezlepro.ca:3000'), + 'forgejo_owner': config['traefik_servers:vars'].get('forgejo_owner', 'Chezlepro'), + 'forgejo_repo': config['traefik_servers:vars'].get('forgejo_repo', 'traefik-deploy'), + 'traefik_version': config['traefik_servers:vars'].get('traefik_version', 'v3.2.0') + } + return None + +def trigger_workflow(forgejo_url, owner, repo, version, token): + api_url = f"{forgejo_url}/api/v1/repos/{owner}/{repo}/actions/workflows/build.yml/dispatches" + + headers = { + "Authorization": f"token {token}", + "Content-Type": "application/json" + } + + data = { + "ref": "main", + "inputs": { + "traefik_version": version + } + } + + print(f"🏗️ Déclenchement de la compilation Traefik {version}...") + print(f" URL: {forgejo_url}/{owner}/{repo}") + + response = requests.post(api_url, headers=headers, json=data) + + if response.status_code in [200, 201, 204]: + print(f"✅ Workflow déclenché!") + print(f"\n📊 Suivez sur: {forgejo_url}/{owner}/{repo}/actions") + return True + else: + print(f"❌ Erreur {response.status_code}: {response.text}") + return False + +def main(): + config = read_inventory() + if not config: + print("❌ inventory.ini introuvable. Exécutez: make setup") + sys.exit(1) + + print("=" * 70) + print("🏗️ COMPILATION SUR FORGEJO") + print("=" * 70) + print(f"\nInstance: {config['forgejo_url']}") + print(f"Dépôt: {config['forgejo_owner']}/{config['forgejo_repo']}") + print(f"Version: {config['traefik_version']}\n") + + token = getpass.getpass("Token Forgejo: ") + if not token: + print("❌ Token requis") + sys.exit(1) + + if trigger_workflow( + config['forgejo_url'], + config['forgejo_owner'], + config['forgejo_repo'], + config['traefik_version'], + token + ): + print("\n⏳ Compilation: ~10-15 minutes") + print(" Puis: make deploy") + else: + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file