Compare commits
43 Commits
561331b825
...
vibe/batch
| Author | SHA1 | Date | |
|---|---|---|---|
| f8009989fc | |||
| fc9164f11e | |||
| c751b621ba | |||
| 07a619b274 | |||
| 9931f81998 | |||
| 437fd506ed | |||
| 943915be74 | |||
| 8a82d14797 | |||
| 0285d171ff | |||
| 55d137132f | |||
| 451dfa5133 | |||
| 17e99db641 | |||
| 07e5ff460b | |||
| 5b3c896a25 | |||
| 91219c49f1 | |||
| 74b8676244 | |||
| 1fd47e9d97 | |||
| 0fbfbd589f | |||
| 8d6be311ae | |||
| 2b4aa30a64 | |||
| cd3c4d86ff | |||
| 45d39d13b4 | |||
| f4cb04c9c9 | |||
| 17a0f23bbb | |||
| f7bfe2f71d | |||
| 72628f0f0e | |||
| b6d240ce31 | |||
| 2d8f5de482 | |||
| 140dab4f1d | |||
| 9b09e6bd86 | |||
| 83410d9eb1 | |||
| fa5bc7e30e | |||
| c19cf7eced | |||
| 68fb29357a | |||
| 6d3adb5834 | |||
| 2d4cb5d8a5 | |||
| b9a46afb82 | |||
| c6807851c5 | |||
| c5a8d5ef52 | |||
| 6ec2d299fc | |||
| 3cfc5f2bfd | |||
| 588a6482e9 | |||
| b4bde14809 |
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||||
name: Postgres
|
name: IAC
|
||||||
|
|
||||||
on: #[push,pull_request]
|
on: #[push,pull_request]
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
@@ -19,18 +19,20 @@ concurrency:
|
|||||||
|
|
||||||
.vault_step: &vault_step
|
.vault_step: &vault_step
|
||||||
name: read vault secret
|
name: read vault secret
|
||||||
uses: https://gitea.arcodange.duckdns.org/arcodange-org/vault-action.git@main
|
uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main
|
||||||
id: vault-secrets
|
id: vault-secrets
|
||||||
with:
|
with:
|
||||||
url: https://vault.arcodange.duckdns.org
|
url: https://vault.arcodange.lab
|
||||||
|
caCertificate: ${{ secrets.HOMELAB_CA_CERT }}
|
||||||
jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
||||||
role: gitea_cicd
|
role: gitea_cicd
|
||||||
method: jwt
|
method: jwt
|
||||||
path: gitea_jwt
|
path: gitea_jwt
|
||||||
secrets: |
|
secrets: |
|
||||||
kvv1/google/credentials credentials | GOOGLE_BACKEND_CREDENTIALS ;
|
kvv1/google/credentials credentials | GOOGLE_CREDENTIALS ;
|
||||||
kvv1/admin/gitea token | GITEA_TOKEN
|
kvv1/admin/gitea token | GITEA_TOKEN ;
|
||||||
|
kvv1/admin/cloudflare iam_token | CLOUDFLARE_API_TOKEN ;
|
||||||
|
kvv1/admin/ovh/app * | OVH_ ;
|
||||||
jobs:
|
jobs:
|
||||||
gitea_vault_auth:
|
gitea_vault_auth:
|
||||||
name: Auth with gitea for vault
|
name: Auth with gitea for vault
|
||||||
@@ -52,9 +54,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENTOFU_VERSION: 1.8.2
|
OPENTOFU_VERSION: 1.8.2
|
||||||
TERRAFORM_VAULT_AUTH_JWT: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
TERRAFORM_VAULT_AUTH_JWT: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
||||||
|
VAULT_CACERT: "${{ github.workspace }}/homelab.pem"
|
||||||
steps:
|
steps:
|
||||||
- *vault_step
|
- *vault_step
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: prepare vault self signed cert
|
||||||
|
run: echo -n "${{ secrets.HOMELAB_CA_CERT }}" | base64 -d > $VAULT_CACERT
|
||||||
- name: terraform apply
|
- name: terraform apply
|
||||||
uses: dflook/terraform-apply@v1
|
uses: dflook/terraform-apply@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ concurrency:
|
|||||||
|
|
||||||
.vault_step: &vault_step
|
.vault_step: &vault_step
|
||||||
name: read vault secret
|
name: read vault secret
|
||||||
uses: https://gitea.arcodange.duckdns.org/arcodange-org/vault-action.git@main
|
uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main
|
||||||
id: vault-secrets
|
id: vault-secrets
|
||||||
with:
|
with:
|
||||||
url: https://vault.arcodange.duckdns.org
|
url: https://vault.arcodange.lab
|
||||||
|
caCertificate: ${{ secrets.HOMELAB_CA_CERT }}
|
||||||
jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
||||||
role: gitea_cicd
|
role: gitea_cicd
|
||||||
method: jwt
|
method: jwt
|
||||||
@@ -50,9 +51,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENTOFU_VERSION: 1.8.2
|
OPENTOFU_VERSION: 1.8.2
|
||||||
TERRAFORM_VAULT_AUTH_JWT: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
TERRAFORM_VAULT_AUTH_JWT: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
|
||||||
|
VAULT_CACERT: "${{ github.workspace }}/homelab.pem"
|
||||||
steps:
|
steps:
|
||||||
- *vault_step
|
- *vault_step
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: prepare vault self signed cert
|
||||||
|
run: echo -n "${{ secrets.HOMELAB_CA_CERT }}" | base64 -d > $VAULT_CACERT
|
||||||
- name: terraform apply
|
- name: terraform apply
|
||||||
uses: dflook/terraform-apply@v1
|
uses: dflook/terraform-apply@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -10,41 +10,68 @@ kubectl create secret generic traefik-duckdns-token --from-literal="DUCKDNS_TOKE
|
|||||||
```mermaid
|
```mermaid
|
||||||
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||||
timeline
|
timeline
|
||||||
title ordre des playbook
|
title Playbook Execution Sequence
|
||||||
section Setup DNS, OS, ...
|
section 01_system
|
||||||
configuration manuelle
|
rpi
|
||||||
: installer OS, réserver IP statique, configurer SSH,VNC
|
: set hostname
|
||||||
: formater et créer des partitions avec gparted
|
dns
|
||||||
section Docker & K3S
|
: install pi-hole
|
||||||
system
|
ssl
|
||||||
: install Docker
|
: step-ca
|
||||||
: install K3S working with docker
|
: fetch root certificate
|
||||||
: configure Traefik
|
: build docker image with CA
|
||||||
|
prepare_disks
|
||||||
section Volume, NFS
|
: list partitions
|
||||||
setup hard_disk
|
: format disk
|
||||||
: monter les partitions
|
: mount disk
|
||||||
: installer NFS
|
system_docker
|
||||||
system
|
: install docker
|
||||||
: déployer provisionner NFS
|
: configure docker storage
|
||||||
|
: restart docker
|
||||||
section postgres
|
longhorn
|
||||||
setup
|
: deploy longhorn
|
||||||
: postgres
|
k3s
|
||||||
section gitea
|
: prepare inventory
|
||||||
setup
|
: install k3s collection
|
||||||
: gitea
|
: install socat
|
||||||
section gitea action runner
|
: deploy k3s cluster
|
||||||
setup
|
: configure kubeconfig
|
||||||
: gitea action runner
|
: configure traefik
|
||||||
section argo cd
|
: configure cert-manager
|
||||||
argo_cd
|
section 02_setup
|
||||||
: argo cd
|
backup_nfs
|
||||||
section hello world app
|
: create RWX volume
|
||||||
setup git repository
|
: create recurring job
|
||||||
: terraform
|
: deploy NFS
|
||||||
setup CI
|
: mount NFS
|
||||||
deploy
|
postgres
|
||||||
: dev : list exposed deployments with label and port as a landpage
|
: create database
|
||||||
: expose (as ngrock ? direct ? port ? )
|
: create user
|
||||||
|
gitea
|
||||||
|
: deploy gitea
|
||||||
|
: create admin user
|
||||||
|
: create organization
|
||||||
|
section 03_cicd
|
||||||
|
cicd : CI/CD
|
||||||
|
gitea_token
|
||||||
|
: generate token
|
||||||
|
deploy_docker_compose
|
||||||
|
: deploy gitea action
|
||||||
|
argocd
|
||||||
|
: generate token
|
||||||
|
: deploy argocd
|
||||||
|
section 04_tools
|
||||||
|
Hashicorp Vault
|
||||||
|
: gitea_token
|
||||||
|
: hashicorp_vault
|
||||||
|
Crowdsec
|
||||||
|
: crowdsec
|
||||||
|
section 05_backup
|
||||||
|
Gitea Backup
|
||||||
|
: gitea
|
||||||
|
K3s PVC Backup
|
||||||
|
: k3s_pvc
|
||||||
|
Postgres Backup
|
||||||
|
: create backup script
|
||||||
|
: create restore script
|
||||||
```
|
```
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
gitea_version: 1.24.3
|
gitea_version: 1.25.5
|
||||||
|
|
||||||
gitea_partition: |-
|
|
||||||
{{
|
|
||||||
hard_disk__partitions | dict2items | selectattr(
|
|
||||||
'value', 'contains', 'gitea'
|
|
||||||
) | map(attribute='key') | first
|
|
||||||
}}
|
|
||||||
|
|
||||||
gitea_database:
|
gitea_database:
|
||||||
db_name: gitea
|
db_name: gitea
|
||||||
@@ -13,13 +6,11 @@ gitea_database:
|
|||||||
db_password: gitea
|
db_password: gitea
|
||||||
|
|
||||||
gitea:
|
gitea:
|
||||||
partition: "{{ gitea_partition }}"
|
|
||||||
database:
|
|
||||||
dockercompose:
|
dockercompose:
|
||||||
name: arcodange_factory
|
name: arcodange_factory
|
||||||
networks:
|
networks:
|
||||||
gitea:
|
postgres:
|
||||||
name: arcodange_factory_gitea
|
name: arcodange_factory_postgres
|
||||||
external: true
|
external: true
|
||||||
services:
|
services:
|
||||||
gitea:
|
gitea:
|
||||||
@@ -43,14 +34,20 @@ gitea:
|
|||||||
GITEA__mailer__SMTP_PORT: 465
|
GITEA__mailer__SMTP_PORT: 465
|
||||||
GITEA__mailer__PASSWD: '{{ gitea_vault.GITEA__mailer__PASSWD }}'
|
GITEA__mailer__PASSWD: '{{ gitea_vault.GITEA__mailer__PASSWD }}'
|
||||||
GITEA__server__SSH_PORT: 2222
|
GITEA__server__SSH_PORT: 2222
|
||||||
GITEA__server__SSH_DOMAIN: "{{ lookup('dig', groups.gitea[0]) }}"
|
GITEA__server__SSH_DOMAIN: "{{ hostvars[groups.gitea[0]]['preferred_ip'] }}"
|
||||||
GITEA__server__SSH_LISTEN_PORT: 22
|
GITEA__server__SSH_LISTEN_PORT: 22
|
||||||
|
GITEA_server__DOMAIN: localhost
|
||||||
|
GITEA_server__HTTP_PORT: 3000
|
||||||
|
GITEA_server__ROOT_URL: https://gitea.arcodange.lab/
|
||||||
|
GITEA_server__START_SSH_SERVER: true
|
||||||
|
GITEA_server__OFFLINE_MODE: true
|
||||||
|
GITEA_service__DISABLE_REGISTRATION: true
|
||||||
networks:
|
networks:
|
||||||
- gitea
|
- postgres
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "2222:22"
|
- "2222:22"
|
||||||
volumes:
|
volumes:
|
||||||
- /arcodange/{{gitea_partition}}/gitea/data:/data
|
- /home/pi/arcodange/docker_composes/gitea/data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
32306161663530376161333835626633326334356137363366643838346132613666356566383638
|
||||||
|
3263386238353438376432313332393134333339306336640a366130313661316166383436346364
|
||||||
|
39613364326533376530623636636334316532343330376366333338626130333533343937623165
|
||||||
|
6538333530376132350a383934336566623866366338323034613965623237653564366435666464
|
||||||
|
33303064356161316564396439383533333139653632393332663336356430383866643337363766
|
||||||
|
30646663663737336162383663383664633030653039313565626164313134326433653965306262
|
||||||
|
39393361643639333166623631316465316564393639643764336133306663346261303137376333
|
||||||
|
30333930613062383465613139646562383836633431366637616166666131366232623065396238
|
||||||
|
35313334633064313234383537663632356466326133333238636335383666323839393930633565
|
||||||
|
36663130343035383731303332396436333333353863376461376131393834666232336138323666
|
||||||
|
38346566616137323830346433303535343030623364353364653731353233373337363633626638
|
||||||
|
32613661633962643030333662386333323035656265316537633961373537373961303134353936
|
||||||
|
33633632633835666364663964383661383830336631333531623131633763333733
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# to add/mount a partitiion, use the gparted utility to create it beforehand witht the matching name/label
|
|
||||||
hard_disk__partitions:
|
|
||||||
nfs: []
|
|
||||||
gitea_data:
|
|
||||||
- gitea
|
|
||||||
pg_data:
|
|
||||||
- postgres
|
|
||||||
|
|
||||||
hard_disk__applications:
|
|
||||||
postgres: "{{ postgres }}"
|
|
||||||
gitea: "{{ gitea }}"
|
|
||||||
|
|
||||||
hard_disk__postgres_databases:
|
|
||||||
gitea: "{{ gitea_database }}"
|
|
||||||
webapp:
|
|
||||||
db_name: webapp
|
|
||||||
|
|
||||||
hard_disk__nfs:
|
|
||||||
server_ip: "{{ ansible_host }}"
|
|
||||||
ks_namespace: kube-system
|
|
||||||
export_directory: /arcodange/nfs
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
$ANSIBLE_VAULT;1.1;AES256
|
|
||||||
66376231363631663639623736353861383337333863623761303438643831653061373338306366
|
|
||||||
3762316261326433316166393132663034373636313935660a353962653931643131306134663264
|
|
||||||
64636264393338366363333932366163393036326362353630656132326534663239306639336531
|
|
||||||
3239373433386332640a653262633333653037646236366362333838356534623935613534376465
|
|
||||||
66633335636235323035656332356566343738363661363066653239653037643539323533643534
|
|
||||||
38376465663637646637326436306631663135333361666635303936643562356365616164636565
|
|
||||||
39313231623630386332363262376364383935353534663465333362356631383334396366643463
|
|
||||||
65616130613936343035643736623137313665373462353531326365396638633165326139343233
|
|
||||||
31313933313161343265373865643638616134303834396563623366633136616333613433323035
|
|
||||||
32643336343438646361616364336466366165363464323466363034373531323839363863396236
|
|
||||||
34343731386364613739666461633564646135306231366135396562383565383562396639316164
|
|
||||||
33626266643765653765
|
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
postgres_partition: |-
|
|
||||||
{{
|
|
||||||
hard_disk__partitions | dict2items | selectattr(
|
|
||||||
'value', 'contains', 'postgres'
|
|
||||||
) | map(attribute='key') | first
|
|
||||||
}}
|
|
||||||
postgres:
|
postgres:
|
||||||
partition: "{{ postgres_partition }}"
|
|
||||||
dockercompose:
|
dockercompose:
|
||||||
name: arcodange_factory
|
name: arcodange_factory
|
||||||
networks:
|
networks:
|
||||||
gitea:
|
postgres:
|
||||||
external: false
|
external: false
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -21,11 +14,11 @@ postgres:
|
|||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
networks:
|
networks:
|
||||||
- gitea
|
- postgres
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- /arcodange/{{postgres_partition}}/postgres/data:/var/lib/postgresql/data
|
- /home/pi/arcodange/docker_composes/postgres/data:/var/lib/postgresql/data
|
||||||
|
|
||||||
pgbouncer:
|
pgbouncer:
|
||||||
auth_user: &pgbouncer_auth pgbouncer_auth
|
auth_user: &pgbouncer_auth pgbouncer_auth
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
step_ca_primary: pi1
|
||||||
|
|
||||||
|
step_ca_fqdn: ssl-ca.arcodange.lab
|
||||||
|
|
||||||
|
step_ca_user: step
|
||||||
|
step_ca_home: /home/step
|
||||||
|
step_ca_dir: /home/step/.step
|
||||||
|
|
||||||
|
step_ca_listen_address: ":8443"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
|
35633437343661363030323466313735373033373566643530653539633133623462333337393037
|
||||||
|
6336653635366439363031616637313339373465666433320a653936396438373132623264386665
|
||||||
|
66623330343439613636353963373139363531613761613864623262623661666565373137306461
|
||||||
|
3062646337353331300a636164643462343163303931646538653537323831623736393634343137
|
||||||
|
39376139306165356138383664373334353364316435303265643965386135356561316130316239
|
||||||
|
64393436363436393339393130383764353231333361313565333934313136666234356433626437
|
||||||
|
35656666386538653963653334393262366562656631376636353538383661386661366438366133
|
||||||
|
64346338666666323562313363363836613439633931306437393132616134666230613936623634
|
||||||
|
34383366663031336236316566626666303764323631363239636461396366323733393731376563
|
||||||
|
65356630326536333133393335383766616631323732333262396464326165366532383066363761
|
||||||
|
37303033316135616661623431623836313965373930376361656334323336656561643336616265
|
||||||
|
36666235623564383132
|
||||||
@@ -2,12 +2,15 @@ raspberries:
|
|||||||
hosts:
|
hosts:
|
||||||
pi1:
|
pi1:
|
||||||
ansible_host: pi1.home # setup http://192.168.1.1/ Réseau/DNS
|
ansible_host: pi1.home # setup http://192.168.1.1/ Réseau/DNS
|
||||||
|
preferred_ip: 192.168.1.201
|
||||||
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
||||||
pi2:
|
pi2:
|
||||||
ansible_host: pi2.home
|
ansible_host: pi2.home
|
||||||
|
preferred_ip: 192.168.1.202
|
||||||
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
||||||
pi3:
|
pi3:
|
||||||
ansible_host: pi3.home
|
ansible_host: pi3.home
|
||||||
|
preferred_ip: 192.168.1.203
|
||||||
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
||||||
|
|
||||||
internetPi1:
|
internetPi1:
|
||||||
@@ -31,17 +34,24 @@ local:
|
|||||||
pi2:
|
pi2:
|
||||||
pi3:
|
pi3:
|
||||||
|
|
||||||
hard_disk:
|
|
||||||
hosts:
|
|
||||||
pi2: # 4To toshiba external hard drive (/dev/sda)
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
children:
|
hosts:
|
||||||
hard_disk:
|
pi2:
|
||||||
|
|
||||||
gitea:
|
gitea:
|
||||||
children:
|
children:
|
||||||
hard_disk:
|
postgres:
|
||||||
|
|
||||||
|
pihole:
|
||||||
|
hosts:
|
||||||
|
pi1:
|
||||||
|
pi3:
|
||||||
|
|
||||||
|
step_ca:
|
||||||
|
hosts:
|
||||||
|
pi1:
|
||||||
|
pi2:
|
||||||
|
pi3:
|
||||||
|
|
||||||
all:
|
all:
|
||||||
children:
|
children:
|
||||||
|
|||||||
@@ -1,315 +1,2 @@
|
|||||||
---
|
- name: system
|
||||||
|
ansible.builtin.import_playbook: ./system/system.yml
|
||||||
- name: System Docker
|
|
||||||
hosts: raspberries:&local
|
|
||||||
gather_facts: yes
|
|
||||||
tags: never
|
|
||||||
become: yes
|
|
||||||
|
|
||||||
pre_tasks:
|
|
||||||
|
|
||||||
- name: set hostname
|
|
||||||
ansible.builtin.hostname:
|
|
||||||
name: "{{ inventory_hostname }}"
|
|
||||||
become: yes
|
|
||||||
when: inventory_hostname != ansible_hostname
|
|
||||||
|
|
||||||
- name: Prevent apt source conflict
|
|
||||||
ansible.builtin.file:
|
|
||||||
state: absent
|
|
||||||
path: /etc/apt/sources.list.d/docker.list
|
|
||||||
become: yes
|
|
||||||
|
|
||||||
- name: Install role geerlingguy.docker
|
|
||||||
community.general.ansible_galaxy_install:
|
|
||||||
type: role
|
|
||||||
name: geerlingguy.docker
|
|
||||||
run_once: true
|
|
||||||
delegate_to: localhost
|
|
||||||
become: false
|
|
||||||
|
|
||||||
- ansible.builtin.debug:
|
|
||||||
var: ansible_facts.machine
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
|
|
||||||
- include_role:
|
|
||||||
name: geerlingguy.docker
|
|
||||||
|
|
||||||
post_tasks:
|
|
||||||
- name: adding existing user '{{ ansible_user }}' to group docker
|
|
||||||
user:
|
|
||||||
name: '{{ ansible_user }}'
|
|
||||||
groups: docker
|
|
||||||
append: yes
|
|
||||||
become: yes
|
|
||||||
|
|
||||||
#---
|
|
||||||
|
|
||||||
- name: System K3S
|
|
||||||
hosts: raspberries:&local
|
|
||||||
tags: never
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: prepare inventory for k3s external playbook
|
|
||||||
tags: always
|
|
||||||
ansible.builtin.add_host:
|
|
||||||
hostname: "{{ item }}"
|
|
||||||
groups:
|
|
||||||
- k3s_cluster
|
|
||||||
- "{{ ansible_loop.first | ternary('server', 'agent') }}"
|
|
||||||
loop: "{{ groups.raspberries | intersect(groups.local) | sort }}"
|
|
||||||
loop_control:
|
|
||||||
extended: true
|
|
||||||
extended_allitems: false
|
|
||||||
|
|
||||||
- name: Install collection k3s.orchestration
|
|
||||||
local_action:
|
|
||||||
module: community.general.ansible_galaxy_install
|
|
||||||
type: collection
|
|
||||||
name: git+https://github.com/k3s-io/k3s-ansible
|
|
||||||
run_once: true
|
|
||||||
|
|
||||||
- name: k3s
|
|
||||||
tags: never
|
|
||||||
# ansible.builtin.import_playbook: k3s.orchestration.site
|
|
||||||
ansible.builtin.import_playbook: k3s.orchestration.upgrade
|
|
||||||
# ansible.builtin.import_playbook: k3s.orchestration.reset
|
|
||||||
vars:
|
|
||||||
k3s_version: v1.32.2+k3s1
|
|
||||||
extra_server_args: "--docker --disable traefik"
|
|
||||||
extra_agent_args: "--docker"
|
|
||||||
api_endpoint: "{{ hostvars[groups['server'][0]]['ansible_host'] | default(groups['server'][0]) }}"
|
|
||||||
|
|
||||||
- name: how to reach k3s
|
|
||||||
hosts: server
|
|
||||||
tasks:
|
|
||||||
- name: copy /etc/rancher/k3s/k3s.yaml to ~/.kube/config from the k3s server and replace 127.0.0.1 with the server ip or hostname
|
|
||||||
run_once: true
|
|
||||||
block:
|
|
||||||
- ansible.builtin.fetch:
|
|
||||||
src: /etc/rancher/k3s/k3s.yaml
|
|
||||||
dest: ~/.kube/config
|
|
||||||
flat: true
|
|
||||||
become: true
|
|
||||||
run_once: true
|
|
||||||
- local_action:
|
|
||||||
module: ansible.builtin.replace
|
|
||||||
path: ~/.kube/config
|
|
||||||
regexp: 'server: https://127.0.0.1:6443'
|
|
||||||
replace: 'server: https://{{ ansible_default_ipv4.address }}:6443'
|
|
||||||
- name: customize k3s traefik configuration https://docs.k3s.io/helm
|
|
||||||
block:
|
|
||||||
- name: Get my public IP
|
|
||||||
community.general.ipify_facts:
|
|
||||||
- become: true
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /var/lib/rancher/k3s/server/manifests/traefik-v3.yaml
|
|
||||||
content: |-
|
|
||||||
apiVersion: v1
|
|
||||||
data:
|
|
||||||
dynamic.yaml: |-
|
|
||||||
{{ traefik_config_yaml | to_nice_yaml | indent( width=4 ) }}
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: traefik-configmap
|
|
||||||
namespace: kube-system
|
|
||||||
---
|
|
||||||
apiVersion: helm.cattle.io/v1
|
|
||||||
kind: HelmChart
|
|
||||||
metadata:
|
|
||||||
name: traefik
|
|
||||||
namespace: kube-system
|
|
||||||
spec:
|
|
||||||
repo: https://traefik.github.io/charts
|
|
||||||
chart: traefik
|
|
||||||
version: v37.0.0
|
|
||||||
targetNamespace: kube-system
|
|
||||||
valuesContent: |-
|
|
||||||
{{ traefik_helm_values | to_nice_yaml | indent( width=4 ) }}
|
|
||||||
vars:
|
|
||||||
traefik_config_yaml:
|
|
||||||
http:
|
|
||||||
services:
|
|
||||||
gitea:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://{{ lookup('dig', groups.gitea[0]) }}:3000"
|
|
||||||
routers:
|
|
||||||
acme-challenge:
|
|
||||||
rule: Host(`arcodange.duckdns.org`) && PathPrefix(`/.well-known/acme-challenge`)
|
|
||||||
service: acme-http@internal
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
domains:
|
|
||||||
- main: "arcodange.duckdns.org"
|
|
||||||
sans:
|
|
||||||
- "*.arcodange.duckdns.org"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
- web
|
|
||||||
gitea:
|
|
||||||
rule: Host(`gitea.arcodange.duckdns.org`)
|
|
||||||
service: gitea
|
|
||||||
middlewares:
|
|
||||||
- localIp
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
domains:
|
|
||||||
- main: "arcodange.duckdns.org"
|
|
||||||
sans:
|
|
||||||
- "gitea.arcodange.duckdns.org"
|
|
||||||
entrypoints:
|
|
||||||
- websecure
|
|
||||||
middlewares:
|
|
||||||
localIp:
|
|
||||||
ipAllowList:
|
|
||||||
sourceRange:
|
|
||||||
- "192.168.1.0/24"
|
|
||||||
- "{{ ipify_public_ip }}/32"
|
|
||||||
# - "0.0.0.0/0"
|
|
||||||
# ipStrategy:
|
|
||||||
# depth: 1
|
|
||||||
traefik_helm_values:
|
|
||||||
deployment:
|
|
||||||
kind: "Deployment"
|
|
||||||
# default is https://github.com/traefik/traefik-helm-chart/blob/v25.0.0/traefik/values.yaml <- for v25 (`kubectl describe deployments.apps traefik -n kube-system | grep helm.sh/chart`)
|
|
||||||
# current is https://github.com/traefik/traefik-helm-chart/blob/v30.1.0/traefik/values.yaml
|
|
||||||
nodeSelector:
|
|
||||||
node-role.kubernetes.io/master: 'true' # make predictible choice of node to direct https traffic to this node and avoid NAT/loss of client IP
|
|
||||||
service:
|
|
||||||
spec:
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
ports:
|
|
||||||
traefik:
|
|
||||||
expose:
|
|
||||||
default: true
|
|
||||||
ingressRoute:
|
|
||||||
dashboard:
|
|
||||||
enabled: true
|
|
||||||
globalArguments: [] # deactivate --global.sendanonymoususage
|
|
||||||
env:
|
|
||||||
- name: POD_NAME
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.name
|
|
||||||
- name: POD_NAMESPACE
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
fieldPath: metadata.namespace
|
|
||||||
- name: LEGO_DISABLE_CNAME_SUPPORT
|
|
||||||
value: 'true'
|
|
||||||
logs:
|
|
||||||
general:
|
|
||||||
level: DEBUG
|
|
||||||
# format: json
|
|
||||||
access:
|
|
||||||
enabled: true
|
|
||||||
# format: json
|
|
||||||
persistence:
|
|
||||||
# -- Enable persistence using Persistent Volume Claims
|
|
||||||
# ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
|
|
||||||
# It can be used to store TLS certificates, see `storage` in certResolvers
|
|
||||||
enabled: true
|
|
||||||
name: data
|
|
||||||
# existingClaim: ""
|
|
||||||
accessMode: ReadWriteOnce
|
|
||||||
size: 128Mi
|
|
||||||
storageClass: "nfs-client"
|
|
||||||
# volumeName: ""
|
|
||||||
path: /data
|
|
||||||
annotations: {}
|
|
||||||
volumes:
|
|
||||||
- name: traefik-configmap
|
|
||||||
mountPath: /config
|
|
||||||
type: configMap
|
|
||||||
additionalArguments:
|
|
||||||
- '--providers.file.filename=/config/dynamic.yaml'
|
|
||||||
- '--providers.kubernetesingress.ingressendpoint.publishedservice=kube-system/traefik'
|
|
||||||
certificatesResolvers:
|
|
||||||
letsencrypt:
|
|
||||||
acme:
|
|
||||||
# for challenge options cf. https://doc.traefik.io/traefik/https/acme/
|
|
||||||
email: arcodange@gmail.com
|
|
||||||
tlsChallenge: true
|
|
||||||
dnsChallenge:
|
|
||||||
# requires env variable DUCKDNS_TOKEN
|
|
||||||
provider: duckdns
|
|
||||||
httpChallenge:
|
|
||||||
entryPoint: "web"
|
|
||||||
# It has to match the path with a persistent volume
|
|
||||||
storage: /data/acme.json
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: traefik-duckdns-token
|
|
||||||
# MY_TOKEN=<my token (see https://www.duckdns.org/domains)>
|
|
||||||
# kubectl create secret generic traefik-duckdns-token --from-literal="DUCKDNS_TOKEN=$MY_TOKEN" -n kube-system
|
|
||||||
- name: touch manifests/traefik.yaml to trigger update
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /var/lib/rancher/k3s/server/manifests/traefik-v3.yaml
|
|
||||||
state: touch
|
|
||||||
become: true
|
|
||||||
|
|
||||||
|
|
||||||
# ---
|
|
||||||
|
|
||||||
- name: setup hard disk
|
|
||||||
tags: never
|
|
||||||
ansible.builtin.import_playbook: ./setup/hard_disk.yml
|
|
||||||
vars:
|
|
||||||
hard_disk__partitions:
|
|
||||||
nfs: []
|
|
||||||
|
|
||||||
- name: Deploy NFS Subdir External Provisioner and alter default traefik deployment
|
|
||||||
tags: never
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
- name: Deploy NFS Subdir External Provisioner
|
|
||||||
block:
|
|
||||||
- name: Add Helm repository for NFS Subdir External Provisioner
|
|
||||||
kubernetes.core.helm_repository:
|
|
||||||
name: nfs-subdir-external-provisioner
|
|
||||||
repo_url: https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
|
|
||||||
force_update: yes
|
|
||||||
|
|
||||||
- name: Install NFS Subdir External Provisioner using Helm
|
|
||||||
# debug:
|
|
||||||
# var: hard_disk__nfs
|
|
||||||
kubernetes.core.helm:
|
|
||||||
name: nfs-subdir-external-provisioner
|
|
||||||
chart_ref: nfs-subdir-external-provisioner/nfs-subdir-external-provisioner
|
|
||||||
release_namespace: "{{ hard_disk__nfs.ks_namespace }}"
|
|
||||||
values:
|
|
||||||
nfs:
|
|
||||||
server: "{{ hard_disk__nfs.server_ip }}"
|
|
||||||
path: "{{ hard_disk__nfs.export_directory }}"
|
|
||||||
vars:
|
|
||||||
hard_disk__nfs: "{{ hostvars[groups.hard_disk[0]].hard_disk__nfs }}"
|
|
||||||
|
|
||||||
- name: redeploy traefik
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
- name: delete old traefik deployment
|
|
||||||
kubernetes.core.k8s:
|
|
||||||
api_version: v1
|
|
||||||
name: traefik
|
|
||||||
kind: Deployment
|
|
||||||
namespace: kube-system
|
|
||||||
state: "absent"
|
|
||||||
- name: delete old deployment job so the k3s helm controller redeploy with our new configuration
|
|
||||||
kubernetes.core.k8s:
|
|
||||||
api_version: batch/v1
|
|
||||||
name: helm-install-traefik
|
|
||||||
kind: Job
|
|
||||||
namespace: kube-system
|
|
||||||
state: "absent"
|
|
||||||
- name: get traefik deployment
|
|
||||||
kubernetes.core.k8s_info:
|
|
||||||
api_version: v1
|
|
||||||
name: traefik
|
|
||||||
kind: Deployment
|
|
||||||
namespace: kube-system
|
|
||||||
wait: true
|
|
||||||
register: traefik_deployment
|
|
||||||
- ansible.builtin.debug:
|
|
||||||
var: traefik_deployment
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
- name: Fetch Gitea Token for Action Runner registration
|
- name: Fetch Gitea Token for Action Runner registration
|
||||||
delegate_to: "{{ groups.gitea[0] }}"
|
delegate_to: "{{ groups.gitea[0] }}"
|
||||||
delegate_facts: true
|
delegate_facts: false
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
docker exec gitea su git -c "gitea actions generate-runner-token"
|
docker exec gitea su git -c "gitea actions generate-runner-token"
|
||||||
register: gitea_runner_token_cmd
|
register: gitea_runner_token_cmd
|
||||||
@@ -27,17 +27,18 @@
|
|||||||
container_name: gitea_action
|
container_name: gitea_action
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
|
CONFIG_FILE: /config.yaml
|
||||||
GITEA_INSTANCE_URL: >-
|
GITEA_INSTANCE_URL: >-
|
||||||
http://{{ hostvars[groups.gitea[0]].ansible_host }}:3000
|
http://{{ hostvars[groups.gitea[0]].ansible_host }}:3000
|
||||||
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ gitea_runner_token_cmd.stdout }}"
|
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ gitea_runner_token_cmd.stdout }}"
|
||||||
GITEA_RUNNER_NAME: arcodange_global_runner
|
GITEA_RUNNER_NAME: arcodange_global_runner_{{ inventory_hostname }}
|
||||||
# GITEA_RUNNER_LABELS: host={{ansible_host}},env=any
|
GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
extra_hosts:
|
- /etc/ssl/certs:/etc/ssl/certs:ro
|
||||||
gitea.arcodange.duckdns.org: '{{ lookup("dig", "gitea.arcodange.duckdns.org") }}'
|
- /usr/local/share/ca-certificates/:/usr/local/share/ca-certificates/:ro
|
||||||
configs:
|
configs:
|
||||||
- config.yaml
|
- config.yaml
|
||||||
configs:
|
configs:
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
# Where to store the registration result.
|
# Where to store the registration result.
|
||||||
file: .runner
|
file: .runner
|
||||||
# Execute how many tasks concurrently at the same time.
|
# Execute how many tasks concurrently at the same time.
|
||||||
capacity: 1
|
capacity: 2
|
||||||
# Extra environment variables to run jobs.
|
# Extra environment variables to run jobs.
|
||||||
envs:
|
envs:
|
||||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||||
@@ -78,9 +79,8 @@
|
|||||||
# If it's empty when registering, it will ask for inputting labels.
|
# If it's empty when registering, it will ask for inputting labels.
|
||||||
# If it's empty when execute `daemon`, will use labels in `.runner` file.
|
# If it's empty when execute `daemon`, will use labels in `.runner` file.
|
||||||
labels:
|
labels:
|
||||||
- "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
|
- "ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca"
|
||||||
- "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
|
- "ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca"
|
||||||
- "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04"
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
# Enable cache server to use actions/cache.
|
# Enable cache server to use actions/cache.
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
|
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
|
||||||
docker_host: ""
|
docker_host: ""
|
||||||
# Pull docker image(s) even if already present
|
# Pull docker image(s) even if already present
|
||||||
force_pull: true
|
force_pull: false
|
||||||
# Rebuild docker image(s) even if already present
|
# Rebuild docker image(s) even if already present
|
||||||
force_rebuild: false
|
force_rebuild: false
|
||||||
|
|
||||||
@@ -143,184 +143,240 @@
|
|||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "/home/pi/arcodange/docker_composes/arcodange_factory_gitea_action"
|
project_src: "/home/pi/arcodange/docker_composes/arcodange_factory_gitea_action"
|
||||||
pull: missing
|
pull: missing
|
||||||
state: present
|
state: "{{ docker_compose_down_then_up }}"
|
||||||
register: deploy_result
|
register: deploy_result
|
||||||
|
loop: ["absent", "present"]
|
||||||
|
loop_control:
|
||||||
|
loop_var: docker_compose_down_then_up
|
||||||
|
|
||||||
- name: Set PACKAGES_TOKEN secret to upload packages from CI
|
# - name: Set PACKAGES_TOKEN secret to upload packages from CI
|
||||||
run_once: True
|
# run_once: True
|
||||||
block:
|
# block:
|
||||||
- name: Generate cicd PACKAGES_TOKEN
|
# - name: Generate cicd PACKAGES_TOKEN
|
||||||
include_role:
|
# include_role:
|
||||||
name: arcodange.factory.gitea_token
|
# name: arcodange.factory.gitea_token
|
||||||
vars:
|
# vars:
|
||||||
gitea_token_name: PACKAGES_TOKEN
|
# gitea_token_name: PACKAGES_TOKEN
|
||||||
gitea_token_fact_name: cicd_PACKAGES_TOKEN
|
# gitea_token_fact_name: cicd_PACKAGES_TOKEN
|
||||||
gitea_token_scopes: write:package
|
# gitea_token_scopes: write:package
|
||||||
gitea_token_replace: true
|
# gitea_token_replace: true
|
||||||
|
|
||||||
- name: Register cicd PACKAGES_TOKEN secrets
|
# - name: Register cicd PACKAGES_TOKEN secrets
|
||||||
include_role:
|
# include_role:
|
||||||
name: arcodange.factory.gitea_secret
|
# name: arcodange.factory.gitea_secret
|
||||||
vars:
|
# vars:
|
||||||
gitea_secret_name: PACKAGES_TOKEN
|
# gitea_secret_name: PACKAGES_TOKEN
|
||||||
gitea_secret_value: "{{ cicd_PACKAGES_TOKEN }}"
|
# gitea_secret_value: "{{ cicd_PACKAGES_TOKEN }}"
|
||||||
loop: ["organization", "user"]
|
# loop: ["organization", "user"]
|
||||||
loop_control:
|
# loop_control:
|
||||||
loop_var: gitea_owner_type # Peut être "user" ou "organization"
|
# loop_var: gitea_owner_type # Peut être "user" ou "organization"
|
||||||
|
|
||||||
|
# - name: Set HOMELAB_CA_CERT secret to validate self signed ssl
|
||||||
|
# run_once: True
|
||||||
|
# block:
|
||||||
|
# - name: Download homelab CA certificate
|
||||||
|
# ansible.builtin.uri:
|
||||||
|
# url: "https://ssl-ca.arcodange.lab:8443/roots.pem"
|
||||||
|
# return_content: yes
|
||||||
|
# validate_certs: no
|
||||||
|
# register: homelab_ca_cert
|
||||||
|
# - name: Debug cert
|
||||||
|
# debug:
|
||||||
|
# msg: "{{ homelab_ca_cert.content }}..."
|
||||||
|
# - name: Register cicd HOMELAB_CA_CERT secrets
|
||||||
|
# include_role:
|
||||||
|
# name: arcodange.factory.gitea_secret
|
||||||
|
# vars:
|
||||||
|
# gitea_secret_name: HOMELAB_CA_CERT
|
||||||
|
# gitea_secret_value: "{{ homelab_ca_cert.content | b64encode }}"
|
||||||
|
# loop: ["organization", "user"]
|
||||||
|
# loop_control:
|
||||||
|
# loop_var: gitea_owner_type # Peut être "user" ou "organization"
|
||||||
|
|
||||||
post_tasks:
|
# post_tasks:
|
||||||
- include_role:
|
# - include_role:
|
||||||
name: arcodange.factory.gitea_token
|
# name: arcodange.factory.gitea_token
|
||||||
vars:
|
# vars:
|
||||||
gitea_token_delete: true
|
# gitea_token_delete: true
|
||||||
|
|
||||||
|
|
||||||
- name: Deploy Argo CD
|
# - name: Deploy Argo CD
|
||||||
hosts: localhost
|
# hosts: localhost
|
||||||
roles:
|
# roles:
|
||||||
- arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required
|
# - role: arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required
|
||||||
tasks:
|
# tags:
|
||||||
- name: Set factory repo
|
# - gitea_sync
|
||||||
include_role:
|
# tasks:
|
||||||
name: arcodange.factory.gitea_repo
|
# - name: Set factory repo
|
||||||
vars:
|
# include_role:
|
||||||
gitea_repo_name: factory
|
# name: arcodange.factory.gitea_repo
|
||||||
- name: Sync other repos
|
# vars:
|
||||||
include_role:
|
# gitea_repo_name: factory
|
||||||
name: arcodange.factory.gitea_sync
|
# - name: Sync other repos
|
||||||
- name: Generate Argo CD token
|
# tags: gitea_sync
|
||||||
include_role:
|
# include_role:
|
||||||
name: arcodange.factory.gitea_token
|
# name: arcodange.factory.gitea_sync
|
||||||
vars:
|
# apply:
|
||||||
gitea_token_name: ARGOCD_TOKEN
|
# tags: gitea_sync
|
||||||
gitea_token_fact_name: argocd_token
|
# - name: Generate Argo CD token
|
||||||
gitea_token_scopes: read:repository,read:package
|
# include_role:
|
||||||
gitea_token_replace: true
|
# name: arcodange.factory.gitea_token
|
||||||
- name: Figure out k3s master node
|
# vars:
|
||||||
shell:
|
# gitea_token_name: ARGOCD_TOKEN
|
||||||
kubectl get nodes -l node-role.kubernetes.io/master=true -o name | sed s'#node/##'
|
# gitea_token_fact_name: argocd_token
|
||||||
register: get_k3s_master_node
|
# gitea_token_scopes: read:repository,read:package
|
||||||
changed_when: false
|
# gitea_token_replace: true
|
||||||
- name: Get kubernetes server internal url
|
# - name: Figure out k3s master node
|
||||||
command: >-
|
# shell:
|
||||||
echo https://kubernetes.default.svc
|
# kubectl get nodes -l node-role.kubernetes.io/control-plane=true -o name | sed s'#node/##'
|
||||||
# {%raw%}
|
# register: get_k3s_master_node
|
||||||
# kubectl get svc/kubernetes -o template="{{.spec.clusterIP}}:{{(index .spec.ports 0).port}}"
|
# changed_when: false
|
||||||
# {%endraw%}
|
# - name: Get kubernetes server internal url
|
||||||
register: get_k3s_internal_server_url
|
# command: >-
|
||||||
changed_when: false
|
# echo https://kubernetes.default.svc
|
||||||
- set_fact:
|
# # {%raw%}
|
||||||
k3s_master_node: "{{ get_k3s_master_node.stdout }}"
|
# # kubectl get svc/kubernetes -o template="{{.spec.clusterIP}}:{{(index .spec.ports 0).port}}"
|
||||||
k3s_internal_server_url: "{{ get_k3s_internal_server_url.stdout }}"
|
# # {%endraw%}
|
||||||
- name: Install Argo CD
|
# register: get_k3s_internal_server_url
|
||||||
become: true
|
# changed_when: false
|
||||||
delegate_to: "{{ k3s_master_node }}"
|
# - set_fact:
|
||||||
vars:
|
# k3s_master_node: "{{ get_k3s_master_node.stdout }}"
|
||||||
gitea_credentials:
|
# k3s_internal_server_url: "{{ get_k3s_internal_server_url.stdout }}"
|
||||||
username: arcodange
|
# - name: Read Step CA root certificate from k3s master
|
||||||
password: "{{ argocd_token }}"
|
# become: true
|
||||||
argocd_helm_values: # https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/values.yaml
|
# delegate_to: "{{ k3s_master_node }}"
|
||||||
global:
|
# slurp:
|
||||||
domain: argocd.arcodange.duckdns.org
|
# src: /home/step/.step/certs/root_ca.crt
|
||||||
configs:
|
# register: step_ca_root_cert
|
||||||
params:
|
# - name: Decode Step CA root certificate
|
||||||
server.insecure: true # let k3s traefik do TLS termination
|
# set_fact:
|
||||||
ansible.builtin.copy:
|
# step_ca_root_cert_pem: "{{ step_ca_root_cert.content | b64decode }}"
|
||||||
dest: /var/lib/rancher/k3s/server/manifests/argocd.yaml
|
# - name: Install Argo CD
|
||||||
content: |-
|
# become: true
|
||||||
apiVersion: v1
|
# delegate_to: "{{ k3s_master_node }}"
|
||||||
kind: Namespace
|
# vars:
|
||||||
metadata:
|
# gitea_credentials:
|
||||||
name: argocd
|
# username: arcodange
|
||||||
---
|
# password: "{{ argocd_token }}"
|
||||||
apiVersion: helm.cattle.io/v1
|
# argocd_helm_values: # https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/values.yaml
|
||||||
kind: HelmChart
|
# global:
|
||||||
metadata:
|
# domain: argocd.arcodange.lab
|
||||||
name: argocd
|
# configs:
|
||||||
namespace: kube-system
|
# cm:
|
||||||
spec:
|
# kustomize.buildOptions: "--enable-helm"
|
||||||
repo: https://argoproj.github.io/argo-helm
|
# helm.enablePostRenderer: "true"
|
||||||
chart: argo-cd
|
# exec.enabled: "true"
|
||||||
targetNamespace: argocd
|
# params:
|
||||||
valuesContent: |-
|
# server.insecure: true # let k3s traefik do TLS termination
|
||||||
{{ argocd_helm_values | to_nice_yaml | indent( width=4 ) }}
|
# ansible.builtin.copy:
|
||||||
---
|
# dest: /var/lib/rancher/k3s/server/manifests/argocd.yaml
|
||||||
apiVersion: networking.k8s.io/v1
|
# content: |-
|
||||||
kind: Ingress
|
# apiVersion: v1
|
||||||
metadata:
|
# kind: Namespace
|
||||||
name: argocd-server-ingress
|
# metadata:
|
||||||
namespace: argocd
|
# name: argocd
|
||||||
annotations:
|
# ---
|
||||||
# For Traefik v2.x
|
# apiVersion: v1
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
# kind: ConfigMap
|
||||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
# metadata:
|
||||||
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
|
# name: argocd-tls-certs-cm
|
||||||
traefik.ingress.kubernetes.io/router.tls.domains.0.main: arcodange.duckdns.org
|
# namespace: argocd
|
||||||
traefik.ingress.kubernetes.io/router.tls.domains.0.sans: argocd.arcodange.duckdns.org
|
# data:
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: localIp@file
|
# gitea.arcodange.lab: |
|
||||||
spec:
|
# {{ step_ca_root_cert_pem | indent(4) }}
|
||||||
rules:
|
# ---
|
||||||
- host: argocd.arcodange.duckdns.org
|
# apiVersion: helm.cattle.io/v1
|
||||||
http:
|
# kind: HelmChart
|
||||||
paths:
|
# metadata:
|
||||||
- path: /
|
# name: argocd
|
||||||
pathType: Prefix
|
# namespace: kube-system
|
||||||
backend:
|
# spec:
|
||||||
service:
|
# repo: https://argoproj.github.io/argo-helm
|
||||||
name: argocd-server
|
# chart: argo-cd
|
||||||
port:
|
# targetNamespace: argocd
|
||||||
number: 80 #TLS is terminated at Traefik
|
# valuesContent: |-
|
||||||
---
|
# {{ argocd_helm_values | to_nice_yaml | indent( width=4 ) }}
|
||||||
apiVersion: v1
|
# ---
|
||||||
kind: Secret
|
# apiVersion: networking.k8s.io/v1
|
||||||
metadata:
|
# kind: Ingress
|
||||||
name: gitea-arcodangeorg-factory-repo
|
# metadata:
|
||||||
namespace: argocd
|
# name: argocd-server-ingress
|
||||||
labels:
|
# namespace: argocd
|
||||||
argocd.argoproj.io/secret-type: repository
|
# annotations:
|
||||||
stringData:
|
# # For Traefik v2.x
|
||||||
type: git
|
# traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
url: https://gitea.arcodange.duckdns.org/arcodange-org/factory
|
# traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
---
|
# traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
|
||||||
apiVersion: v1
|
# traefik.ingress.kubernetes.io/router.tls.domains.0.main: arcodange.lab
|
||||||
kind: Secret
|
# traefik.ingress.kubernetes.io/router.tls.domains.0.sans: argocd.arcodange.lab
|
||||||
metadata:
|
# traefik.ingress.kubernetes.io/router.middlewares: localIp@file
|
||||||
name: gitea-arcodangeorg-repo-creds
|
# spec:
|
||||||
namespace: argocd
|
# rules:
|
||||||
labels:
|
# - host: argocd.arcodange.lab
|
||||||
argocd.argoproj.io/secret-type: repo-creds
|
# http:
|
||||||
stringData:
|
# paths:
|
||||||
type: git
|
# - path: /
|
||||||
url: https://gitea.arcodange.duckdns.org/arcodange-org
|
# pathType: Prefix
|
||||||
password: {{ gitea_credentials.password }}
|
# backend:
|
||||||
username: {{ gitea_credentials.username }}
|
# service:
|
||||||
---
|
# name: argocd-server
|
||||||
apiVersion: argoproj.io/v1alpha1
|
# port:
|
||||||
kind: Application
|
# number: 80 #TLS is terminated at Traefik
|
||||||
metadata:
|
# ---
|
||||||
name: factory
|
# apiVersion: v1
|
||||||
namespace: argocd
|
# kind: Secret
|
||||||
spec:
|
# metadata:
|
||||||
project: default
|
# name: gitea-arcodangeorg-factory-repo
|
||||||
source:
|
# namespace: argocd
|
||||||
repoURL: https://gitea.arcodange.duckdns.org/arcodange-org/factory
|
# labels:
|
||||||
targetRevision: HEAD
|
# argocd.argoproj.io/secret-type: repository
|
||||||
path: argocd
|
# stringData:
|
||||||
destination:
|
# type: git
|
||||||
server: {{ k3s_internal_server_url }}
|
# url: https://gitea.arcodange.lab/arcodange-org/factory
|
||||||
namespace: argocd
|
# ---
|
||||||
syncPolicy:
|
# apiVersion: v1
|
||||||
automated:
|
# kind: Secret
|
||||||
prune: true
|
# metadata:
|
||||||
selfHeal: true
|
# name: gitea-arcodangeorg-repo-creds
|
||||||
- name: touch manifests/argocd.yaml to trigger update
|
# namespace: argocd
|
||||||
delegate_to: "{{ k3s_master_node }}"
|
# labels:
|
||||||
ansible.builtin.file:
|
# argocd.argoproj.io/secret-type: repo-creds
|
||||||
path: /var/lib/rancher/k3s/server/manifests/argocd.yaml
|
# stringData:
|
||||||
state: touch
|
# type: git
|
||||||
become: true
|
# url: https://gitea.arcodange.lab/arcodange-org
|
||||||
post_tasks:
|
# password: {{ gitea_credentials.password }}
|
||||||
- include_role:
|
# username: {{ gitea_credentials.username }}
|
||||||
name: arcodange.factory.gitea_token
|
# ---
|
||||||
vars:
|
# apiVersion: argoproj.io/v1alpha1
|
||||||
gitea_token_delete: true
|
# kind: Application
|
||||||
|
# metadata:
|
||||||
|
# name: factory
|
||||||
|
# namespace: argocd
|
||||||
|
# spec:
|
||||||
|
# project: default
|
||||||
|
# source:
|
||||||
|
# repoURL: https://gitea.arcodange.lab/arcodange-org/factory
|
||||||
|
# targetRevision: HEAD
|
||||||
|
# path: argocd
|
||||||
|
# destination:
|
||||||
|
# server: {{ k3s_internal_server_url }}
|
||||||
|
# namespace: argocd
|
||||||
|
# syncPolicy:
|
||||||
|
# automated:
|
||||||
|
# prune: true
|
||||||
|
# selfHeal: true
|
||||||
|
# - name: touch manifests/argocd.yaml to trigger update
|
||||||
|
# delegate_to: "{{ k3s_master_node }}"
|
||||||
|
# ansible.builtin.file:
|
||||||
|
# path: /var/lib/rancher/k3s/server/manifests/argocd.yaml
|
||||||
|
# state: touch
|
||||||
|
# become: true
|
||||||
|
# post_tasks:
|
||||||
|
# - include_role:
|
||||||
|
# name: arcodange.factory.gitea_token
|
||||||
|
# apply:
|
||||||
|
# tags: gitea_sync
|
||||||
|
# tags:
|
||||||
|
# - gitea_sync
|
||||||
|
# vars:
|
||||||
|
# gitea_token_delete: true
|
||||||
3
ansible/arcodange/factory/playbooks/05_backup.yml
Normal file
3
ansible/arcodange/factory/playbooks/05_backup.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
- name: backup
|
||||||
|
ansible.builtin.import_playbook: ./backup/backup.yml
|
||||||
13
ansible/arcodange/factory/playbooks/backup/README.md
Normal file
13
ansible/arcodange/factory/playbooks/backup/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Backups
|
||||||
|
|
||||||
|
Ecris les scripts de backup (pour écrire des archives dans /mnt/backups)
|
||||||
|
Ecris dans crontab pour les executer périodiquement
|
||||||
|
|
||||||
|
Aller sur la machine et utiliser
|
||||||
|
```sh
|
||||||
|
sudo su
|
||||||
|
mails
|
||||||
|
```
|
||||||
|
pour lire les erreurs
|
||||||
|
|
||||||
|
Longhorn se charge de snapshot le contenu de /mnt/backups, de le répliquer et d'en envoyer une version dans le cloud.
|
||||||
21
ansible/arcodange/factory/playbooks/backup/backup.yml
Normal file
21
ansible/arcodange/factory/playbooks/backup/backup.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
# - name: setup cron report
|
||||||
|
# ansible.builtin.import_playbook: cron_report.yml
|
||||||
|
|
||||||
|
- name: postgres
|
||||||
|
ansible.builtin.import_playbook: postgres.yml
|
||||||
|
vars:
|
||||||
|
backup_root_dir: "/mnt/backups"
|
||||||
|
backup_dirname: "postgres"
|
||||||
|
|
||||||
|
- name: gitea
|
||||||
|
ansible.builtin.import_playbook: gitea.yml
|
||||||
|
vars:
|
||||||
|
backup_root_dir: "/mnt/backups"
|
||||||
|
backup_dirname: "gitea"
|
||||||
|
|
||||||
|
- name: k3s_pvc
|
||||||
|
ansible.builtin.import_playbook: k3s_pvc.yml
|
||||||
|
vars:
|
||||||
|
backup_root_dir: "/mnt/backups"
|
||||||
|
backup_dirname: "k3s_pvc"
|
||||||
127
ansible/arcodange/factory/playbooks/backup/cron_report.yml
Normal file
127
ansible/arcodange/factory/playbooks/backup/cron_report.yml
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
- name: MTA local complet pour Raspberry Pi avec livraison automatique de cron à root
|
||||||
|
hosts: raspberries:&local
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
msmtp_log_dir: "/var/log/msmtp"
|
||||||
|
msmtp_log_file: "{{ msmtp_log_dir }}/msmtp.log"
|
||||||
|
msmtp_log_retention_days: 7
|
||||||
|
rotate_script: "/usr/local/bin/rotate_msmtp_logs.sh"
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Vérifier si le script de rotation existe
|
||||||
|
stat:
|
||||||
|
path: "{{ rotate_script }}"
|
||||||
|
register: rotate_script_stat
|
||||||
|
|
||||||
|
- name: Ignorer le reste du playbook si le script existe
|
||||||
|
meta: end_play
|
||||||
|
when: rotate_script_stat.stat.exists
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Installer Postfix, msmtp et mailutils
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- postfix
|
||||||
|
- msmtp
|
||||||
|
# - msmtp-mta # conflicts with recent pi setup - may be required by pi2 with old setup
|
||||||
|
- mailutils
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Configurer Postfix en mode local only
|
||||||
|
debconf:
|
||||||
|
name: postfix
|
||||||
|
question: "postfix/main_mailer_type"
|
||||||
|
value: "Local only"
|
||||||
|
vtype: "string"
|
||||||
|
|
||||||
|
- name: Redémarrer Postfix
|
||||||
|
service:
|
||||||
|
name: postfix
|
||||||
|
state: restarted
|
||||||
|
enabled: yes
|
||||||
|
use: init
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Créer répertoire de logs msmtp
|
||||||
|
file:
|
||||||
|
path: "{{ msmtp_log_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Créer fichier de log msmtp sécurisé
|
||||||
|
file:
|
||||||
|
path: "{{ msmtp_log_file }}"
|
||||||
|
state: touch
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0600'
|
||||||
|
|
||||||
|
- name: Configurer msmtp pour envoyer via Postfix local
|
||||||
|
copy:
|
||||||
|
dest: /etc/msmtprc
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0600'
|
||||||
|
content: |
|
||||||
|
defaults
|
||||||
|
logfile {{ msmtp_log_file }}
|
||||||
|
auth off
|
||||||
|
tls off
|
||||||
|
|
||||||
|
account default
|
||||||
|
host localhost
|
||||||
|
port 25
|
||||||
|
from root
|
||||||
|
|
||||||
|
- name: Créer script de rotation quotidienne des logs msmtp
|
||||||
|
copy:
|
||||||
|
dest: "{{ rotate_script }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
TODAY=$(date +%Y%m%d)
|
||||||
|
if [ -f "{{ msmtp_log_file }}" ]; then
|
||||||
|
mv "{{ msmtp_log_file }}" "{{ msmtp_log_dir }}/msmtp.log.$TODAY"
|
||||||
|
touch "{{ msmtp_log_file }}"
|
||||||
|
chmod 600 "{{ msmtp_log_file }}"
|
||||||
|
fi
|
||||||
|
find "{{ msmtp_log_dir }}" -type f -name 'msmtp.log.*' -mtime +{{ msmtp_log_retention_days }} -exec rm -f {} \;
|
||||||
|
|
||||||
|
- name: Créer cron pour rotation quotidienne des logs msmtp
|
||||||
|
cron:
|
||||||
|
name: "Rotation quotidienne msmtp logs"
|
||||||
|
user: root
|
||||||
|
minute: 0
|
||||||
|
hour: 3
|
||||||
|
job: "{{ rotate_script }}"
|
||||||
|
|
||||||
|
- name: S'assurer que tous les mails root arrivent dans la boîte locale
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/aliases
|
||||||
|
regexp: '^root:'
|
||||||
|
line: "root: root"
|
||||||
|
create: yes
|
||||||
|
|
||||||
|
- name: Mettre à jour les alias
|
||||||
|
command: newaliases
|
||||||
|
|
||||||
|
- name: Tester l’envoi de mail local
|
||||||
|
shell: |
|
||||||
|
echo "Test mail MTA local Pi" | mail -s "Test msmtp/Postfix Pi" root
|
||||||
|
register: mail_test
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Alerter si test mail échoue
|
||||||
|
debug:
|
||||||
|
msg: "⚠️ Envoi de mail via msmtp/Postfix sur Raspberry Pi a échoué !"
|
||||||
|
when: mail_test.rc != 0
|
||||||
|
|
||||||
|
|
||||||
|
- name: mail utility
|
||||||
|
ansible.builtin.import_playbook: cron_report_mailutility.yml
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
- name: Installer et configurer neomutt pour root
|
||||||
|
hosts: raspberries:&local
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
neomutt_config_file: "/root/.muttrc"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Installer neomutt
|
||||||
|
apt:
|
||||||
|
name: neomutt
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Créer fichier de configuration neomutt pour root
|
||||||
|
copy:
|
||||||
|
dest: "/root/.muttrc"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0600'
|
||||||
|
content: |
|
||||||
|
{% raw %}
|
||||||
|
# Fichier de configuration neomutt pour root
|
||||||
|
set spoolfile="/var/mail/root"
|
||||||
|
set folder="/var/mail"
|
||||||
|
|
||||||
|
# Affichage
|
||||||
|
set index_format="%4C %Z %{%b %d} %-15.15F (%4l) %s"
|
||||||
|
|
||||||
|
# Navigation rapide
|
||||||
|
set pager_index_lines=20
|
||||||
|
set markers=yes
|
||||||
|
set sort=reverse-date
|
||||||
|
|
||||||
|
# Sauvegarde des mails lus
|
||||||
|
set record="/var/mail/root"
|
||||||
|
|
||||||
|
# Confirmation avant suppression
|
||||||
|
set confirmappend=yes
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Créer alias pratique pour ouvrir neomutt
|
||||||
|
lineinfile:
|
||||||
|
path: /root/.bashrc
|
||||||
|
line: 'alias mails="neomutt -f /var/mail/root"'
|
||||||
|
create: yes
|
||||||
|
state: present
|
||||||
102
ansible/arcodange/factory/playbooks/backup/gitea.yml
Normal file
102
ansible/arcodange/factory/playbooks/backup/gitea.yml
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
- name: Backup Gitea
|
||||||
|
hosts: gitea
|
||||||
|
gather_facts: yes
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
gitea_container_name: "{{ gitea.dockercompose.services.gitea.container_name }}"
|
||||||
|
gitea_user: "git"
|
||||||
|
backup_dir: "{{ backup_root_dir }}/{{ backup_dirname }}"
|
||||||
|
scripts_dir: "/home/pi/arcodange/docker_composes/gitea/scripts"
|
||||||
|
keep_days: 3
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: S'assurer que le répertoire de backup existe
|
||||||
|
file:
|
||||||
|
path: "{{ backup_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: define backup command
|
||||||
|
set_fact:
|
||||||
|
backup_cmd: >-
|
||||||
|
docker exec -u {{ gitea_user }} {{ gitea_container_name }}
|
||||||
|
gitea dump --skip-log --skip-db --skip-package-data --type tar.gz -c /data/gitea/conf/app.ini -C /data/gitea/ -f -
|
||||||
|
|
||||||
|
- name: test backup_cmd
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
{{ backup_cmd }} > /dev/null
|
||||||
|
|
||||||
|
- name: Créer le script de backup
|
||||||
|
copy:
|
||||||
|
dest: "{{ scripts_dir }}/backup.sh"
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
mkdir -p {{ backup_dir }}
|
||||||
|
{{ backup_cmd }} > {{ backup_dir }}/backup_$(date +\%Y\%m\%d).gitea.gz
|
||||||
|
find {{ backup_dir }} -type f -name 'backup_*.gitea.gz' -mtime +{{ keep_days }} -delete
|
||||||
|
|
||||||
|
- name: Ajouter une tâche cron pour backup Gitea tous les jours à 4h
|
||||||
|
cron:
|
||||||
|
name: "Backup Gitea archive"
|
||||||
|
minute: "0"
|
||||||
|
hour: "4"
|
||||||
|
user: root
|
||||||
|
job: "{{ scripts_dir }}/backup.sh"
|
||||||
|
|
||||||
|
- name: Créer le script de restauration
|
||||||
|
copy:
|
||||||
|
dest: "{{ scripts_dir }}/restore.sh"
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="{{ gitea_container_name }}"
|
||||||
|
BACKUP_DIR="{{ backup_dir }}"
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
FILE=$(ls -1t "$BACKUP_DIR"/backup_*.gitea.gz | head -n 1)
|
||||||
|
echo "Aucune date fournie, restauration du dernier dump : $FILE"
|
||||||
|
else
|
||||||
|
FILE="$BACKUP_DIR/backup_$1.gitea.gz"
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "Fichier $FILE introuvable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copie du dump dans le container..."
|
||||||
|
docker cp "$FILE" "$CONTAINER_NAME":/tmp/
|
||||||
|
|
||||||
|
BASENAME=$(basename "$FILE" .gitea.gz)
|
||||||
|
docker exec --user git "$CONTAINER_NAME" mkdir -p /tmp/$BASENAME/
|
||||||
|
|
||||||
|
echo "Décompression du dump..."
|
||||||
|
docker exec --user git "$CONTAINER_NAME" tar -xzf "/tmp/"$BASENAME".gitea.gz" --directory /tmp/$BASENAME/
|
||||||
|
|
||||||
|
echo "Restauration des données..."
|
||||||
|
docker exec --user git "$CONTAINER_NAME" bash -c "
|
||||||
|
cd /tmp/$BASENAME/data &&
|
||||||
|
find . -type d -exec mkdir -p /data/gitea/{} \; &&
|
||||||
|
find . -type f -exec mv {} /data/gitea/{} \;
|
||||||
|
"
|
||||||
|
docker exec --user root "$CONTAINER_NAME" bash -c "
|
||||||
|
cd /tmp/$BASENAME/repos &&
|
||||||
|
find . -type d -exec mkdir -p /data/git/repositories/{} \; &&
|
||||||
|
find . -type f -exec mv {} /data/git/repositories/{} \;
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "Réglage des permissions..."
|
||||||
|
docker exec "$CONTAINER_NAME" chown -R git:git /data
|
||||||
|
|
||||||
|
echo "Régénération des hooks..."
|
||||||
|
docker exec --user git "$CONTAINER_NAME" /usr/local/bin/gitea -c '/data/gitea/conf/app.ini' admin regenerate hooks
|
||||||
|
|
||||||
|
echo "Netoyage"
|
||||||
|
docker exec "$CONTAINER_NAME" find /tmp/ -maxdepth 1 -name "$BASENAME*" -exec rm -rf {} + -depth
|
||||||
|
|
||||||
|
echo "Restauration Gitea terminée."
|
||||||
83
ansible/arcodange/factory/playbooks/backup/k3s_pvc.yml
Normal file
83
ansible/arcodange/factory/playbooks/backup/k3s_pvc.yml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
- name: Backup K3S Persistent Volumes
|
||||||
|
hosts: pi1
|
||||||
|
gather_facts: yes
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
backup_dir: "{{ backup_root_dir }}/{{ backup_dirname }}"
|
||||||
|
scripts_dir: "/opt/k3s_volumes"
|
||||||
|
keep_days: 3
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: S'assurer que le répertoire de backup existe
|
||||||
|
file:
|
||||||
|
path: "{{ backup_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: S'assurer que le répertoire de scripts existe
|
||||||
|
file:
|
||||||
|
path: "{{ scripts_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: define backup command
|
||||||
|
set_fact:
|
||||||
|
backup_cmd: |-
|
||||||
|
echo "
|
||||||
|
$(kubectl get -A pv -o yaml)
|
||||||
|
---
|
||||||
|
$(kubectl get -A pvc -o yaml)
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: test backup_cmd
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
{{ backup_cmd }} > /dev/null
|
||||||
|
|
||||||
|
- name: Créer le script de backup
|
||||||
|
copy:
|
||||||
|
dest: "{{ scripts_dir }}/backup.sh"
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
mkdir -p {{ backup_dir }}
|
||||||
|
{{ backup_cmd }} > {{ backup_dir }}/backup_$(date +\%Y\%m\%d).volumes
|
||||||
|
find {{ backup_dir }} -type f -name 'backup_*.volumes' -mtime +{{ keep_days }} -delete
|
||||||
|
|
||||||
|
SCRIPTS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
|
||||||
|
{{ backup_cmd }} > $SCRIPTS_DIR/backup.volumes
|
||||||
|
|
||||||
|
- name: Ajouter une tâche cron pour backup k3s volumes tous les jours à 4h
|
||||||
|
cron:
|
||||||
|
name: "Backup K3S Volumes"
|
||||||
|
minute: "0"
|
||||||
|
hour: "4"
|
||||||
|
user: root
|
||||||
|
job: "{{ scripts_dir }}/backup.sh"
|
||||||
|
|
||||||
|
- name: Créer le script de restauration
|
||||||
|
copy:
|
||||||
|
dest: "{{ scripts_dir }}/restore.sh"
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BACKUP_DIR="{{ backup_dir }}"
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
FILE=$(ls -1t "$BACKUP_DIR"/backup_*.volumes | head -n 1)
|
||||||
|
echo "Aucune date fournie, restauration du dernier dump : $FILE"
|
||||||
|
else
|
||||||
|
FILE="$BACKUP_DIR/backup_$1.volumes"
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "Fichier $FILE introuvable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl apply -f "$FILE"
|
||||||
|
|
||||||
|
echo "Restauration des volumes k3s terminée."
|
||||||
84
ansible/arcodange/factory/playbooks/backup/postgres.yml
Normal file
84
ansible/arcodange/factory/playbooks/backup/postgres.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
- name: Backup Postgres
|
||||||
|
hosts: postgres
|
||||||
|
gather_facts: yes
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
postgres_container_name: "{{ postgres.dockercompose.services.postgres.container_name }}"
|
||||||
|
postgres_user: "{{ postgres.dockercompose.services.postgres.environment.POSTGRES_USER }}"
|
||||||
|
backup_dir: "{{ backup_root_dir }}/{{ backup_dirname }}"
|
||||||
|
scripts_dir: "/home/pi/arcodange/docker_composes/postgres/scripts"
|
||||||
|
keep_days: 3
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: S'assurer que le répertoire de backup existe
|
||||||
|
file:
|
||||||
|
path: "{{ backup_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: define backup command
|
||||||
|
set_fact:
|
||||||
|
backup_cmd: "docker exec {{ postgres_container_name }} pg_dumpall -U {{ postgres_user }}"
|
||||||
|
|
||||||
|
- name: test backup_cmd
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
{{ backup_cmd }} > /dev/null
|
||||||
|
|
||||||
|
- name: Créer le script de backup
|
||||||
|
copy:
|
||||||
|
dest: "{{ scripts_dir }}/backup.sh"
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
mkdir -p {{ backup_dir }}
|
||||||
|
{{ backup_cmd }} | gzip > {{ backup_dir }}/backup_$(date +\%Y\%m\%d).sql.gz
|
||||||
|
find {{ backup_dir }} -type f -name 'backup_*.sql.gz' -mtime +{{ keep_days }} -delete
|
||||||
|
|
||||||
|
- name: Ajouter une tâche cron pour dump PostgreSQL tous les jours à 4h avec compression
|
||||||
|
cron:
|
||||||
|
name: "Backup PostgreSQL compressé"
|
||||||
|
minute: "0"
|
||||||
|
hour: "4"
|
||||||
|
user: root
|
||||||
|
job: "{{ scripts_dir }}/backup.sh"
|
||||||
|
|
||||||
|
- name: Créer le script de restauration
|
||||||
|
copy:
|
||||||
|
dest: "{{ scripts_dir }}/restore.sh"
|
||||||
|
mode: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="{{ postgres_container_name }}"
|
||||||
|
POSTGRES_USER="{{ postgres_user }}"
|
||||||
|
BACKUP_DIR="{{ backup_dir }}"
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
FILE=$(ls -1t "$BACKUP_DIR"/backup_*.sql.gz | head -n 1)
|
||||||
|
echo "Aucune date fournie, restauration du dernier dump : $FILE"
|
||||||
|
else
|
||||||
|
FILE="$BACKUP_DIR/backup_$1.sql.gz"
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "Fichier $FILE introuvable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copie du fichier dans le container..."
|
||||||
|
docker cp "$FILE" "$CONTAINER_NAME":/tmp/restore.sql.gz
|
||||||
|
|
||||||
|
echo "Décompression dans le container..."
|
||||||
|
docker exec "$CONTAINER_NAME" sh -c "gunzip -f /tmp/restore.sql.gz"
|
||||||
|
|
||||||
|
echo "Restauration dans PostgreSQL..."
|
||||||
|
docker exec -i "$CONTAINER_NAME" psql -U "$POSTGRES_USER" -f /tmp/restore.sql
|
||||||
|
|
||||||
|
echo "Restauration terminée."
|
||||||
|
echo "Si cela ne fonctionne pas, supprimez le contenu du dossier data avant de rejouer le script"
|
||||||
|
|
||||||
|
# docker exec -u git gitea
|
||||||
|
# gitea dump --skip-log --skip-db --type tar.gz -c /data/gitea/conf/app.ini -C /data/gitea/ -f /tmp/backup_20250828.gitea.gz
|
||||||
2
ansible/arcodange/factory/playbooks/dns/dns.yml
Normal file
2
ansible/arcodange/factory/playbooks/dns/dns.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- name: pihole
|
||||||
|
ansible.builtin.import_playbook: pihole.yml
|
||||||
11
ansible/arcodange/factory/playbooks/dns/pihole.yml
Normal file
11
ansible/arcodange/factory/playbooks/dns/pihole.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
- name: Installer et configurer Pi-hole sur pi1
|
||||||
|
hosts: raspberries:&local
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
|
||||||
|
pihole_custom_dns:
|
||||||
|
".arcodange.duckdns.org": "{{ hostvars['pi1'].preferred_ip }}"
|
||||||
|
".arcodange.lab": "{{ hostvars['pi1'].preferred_ip }}"
|
||||||
|
roles:
|
||||||
|
- pihole
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pihole_primary: pi1
|
||||||
|
pihole_user_gravity: pihole_gravity
|
||||||
|
pihole_gravity_home: /var/lib/pihole_gravity
|
||||||
|
pihole_dns_domain: lab
|
||||||
|
pihole_ports: '8081o,443os,[::]:8081o,[::]:443os' # web interface
|
||||||
|
pihole_gravity_conf: /etc/gravity-sync/gravity-sync.conf # should not be changed
|
||||||
|
pihole_custom_dns: {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: Restart Pi-hole
|
||||||
|
service:
|
||||||
|
name: pihole-FTL
|
||||||
|
state: restarted
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
- name: Build DNS server list (exclude self)
|
||||||
|
set_fact:
|
||||||
|
pihole_dns_servers: >-
|
||||||
|
{{
|
||||||
|
groups['pihole']
|
||||||
|
| reject('equalto', inventory_hostname)
|
||||||
|
| map('extract', hostvars, 'preferred_ip')
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
|
||||||
|
# 1️⃣ Supprimer d’éventuelles anciennes entrées Pi-hole
|
||||||
|
- name: Remove existing Pi-hole nameservers
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/resolv.conf
|
||||||
|
regexp: '^nameserver ({{ pihole_dns_servers | join("|") }})$'
|
||||||
|
state: absent
|
||||||
|
when: pihole_dns_servers | length > 0
|
||||||
|
|
||||||
|
# 2️⃣ Insérer les Pi-hole juste après la ligne search
|
||||||
|
- name: Insert Pi-hole nameservers with priority
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/resolv.conf
|
||||||
|
insertafter: '^search'
|
||||||
|
line: "nameserver {{ item }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ pihole_dns_servers }}"
|
||||||
|
|
||||||
|
|
||||||
|
# 3️⃣ Définir les priorités par interface
|
||||||
|
- name: Set DNS priority mapping
|
||||||
|
set_fact:
|
||||||
|
interface_dns_priority:
|
||||||
|
eth0: 50
|
||||||
|
wlan0: 100
|
||||||
|
|
||||||
|
# 5️⃣ Configurer les DNS Pi-hole sur toutes les interfaces actives
|
||||||
|
|
||||||
|
- name: Get active connections
|
||||||
|
command: nmcli -t -f NAME,DEVICE connection show --active
|
||||||
|
register: active_connections
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Get current DNS for each active interface
|
||||||
|
vars:
|
||||||
|
iface_name: "{{ item.split(':')[1] }}"
|
||||||
|
conn_name: "{{ item.split(':')[0] }}"
|
||||||
|
loop: "{{ active_connections.stdout_lines }}"
|
||||||
|
when: item.split(':')[1] in interface_dns_priority
|
||||||
|
command: nmcli -g IP4.DNS connection show "{{ conn_name }}"
|
||||||
|
register: current_dns
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Apply Pi-hole DNS if different
|
||||||
|
vars:
|
||||||
|
iface_name: "{{ item.split(':')[1] }}"
|
||||||
|
conn_name: "{{ item.split(':')[0] }}"
|
||||||
|
loop: "{{ active_connections.stdout_lines }}"
|
||||||
|
when: item.split(':')[1] in interface_dns_priority
|
||||||
|
command: >
|
||||||
|
nmcli connection modify "{{ conn_name }}"
|
||||||
|
ipv4.dns "{{ pihole_dns_servers | join(' ') }}"
|
||||||
|
ipv4.ignore-auto-dns yes
|
||||||
|
ipv4.dns-priority "{{ interface_dns_priority[iface_name] }}"
|
||||||
|
register: dns_changed
|
||||||
|
changed_when: dns_changed is defined and dns_changed.stdout != ""
|
||||||
|
|
||||||
|
- name: Reactivate interface if DNS changed
|
||||||
|
vars:
|
||||||
|
iface_name: "{{ item.split(':')[1] }}"
|
||||||
|
conn_name: "{{ item.split(':')[0] }}"
|
||||||
|
loop: "{{ active_connections.stdout_lines }}"
|
||||||
|
when: item.split(':')[1] in interface_dns_priority
|
||||||
|
command: nmcli connection up "{{ conn_name }}"
|
||||||
|
when: dns_changed is defined and dns_changed.changed
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Gravity Sync HA setup – final version with SSH key rotation
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
- name: Determine primary Pi-hole
|
||||||
|
set_fact:
|
||||||
|
pihole_primary: "{{ groups['pihole'] | first }}"
|
||||||
|
|
||||||
|
- name: Set secondary Pi-hole hosts
|
||||||
|
set_fact:
|
||||||
|
pihole_secondaries: "{{ groups['pihole'] | difference([pihole_primary]) }}"
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# 1️⃣ Ensure gravity user exists on all Pi-hole nodes
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Ensure gravity user exists
|
||||||
|
user:
|
||||||
|
name: "{{ pihole_user_gravity }}"
|
||||||
|
home: "{{ pihole_gravity_home }}"
|
||||||
|
shell: /bin/bash
|
||||||
|
system: yes
|
||||||
|
create_home: yes
|
||||||
|
|
||||||
|
- name: Create .ssh directory for gravity user
|
||||||
|
file:
|
||||||
|
path: "{{ pihole_gravity_home }}/.ssh"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ pihole_user_gravity }}"
|
||||||
|
group: "{{ pihole_user_gravity }}"
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# 2️⃣ Generate SSH key for each host (rotation at each run)
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Generate SSH keypair for gravity user
|
||||||
|
openssh_keypair:
|
||||||
|
path: "{{ pihole_gravity_home }}/.ssh/id_ed25519"
|
||||||
|
type: ed25519
|
||||||
|
owner: "{{ pihole_user_gravity }}"
|
||||||
|
group: "{{ pihole_user_gravity }}"
|
||||||
|
mode: '0600'
|
||||||
|
register: gravity_key
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Set gravity key in hostvars
|
||||||
|
set_fact:
|
||||||
|
gravity_pubkey: "{{ gravity_key.public_key }}"
|
||||||
|
|
||||||
|
- name: Clean authorized_keys for gravity user
|
||||||
|
file:
|
||||||
|
path: "{{ pihole_gravity_home }}/.ssh/authorized_keys"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Authorize SSH keys from other Pi-hole hosts
|
||||||
|
authorized_key:
|
||||||
|
user: "{{ pihole_user_gravity }}"
|
||||||
|
key: "{{ hostvars[item].gravity_pubkey }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ groups['pihole'] }}"
|
||||||
|
when: inventory_hostname != item
|
||||||
|
|
||||||
|
- name: Add all Pi-hole hosts to known_hosts
|
||||||
|
known_hosts:
|
||||||
|
path: "{{ pihole_gravity_home }}/.ssh/known_hosts"
|
||||||
|
name: "{{ item }}"
|
||||||
|
key: "{{ lookup('pipe', 'ssh-keyscan -t ed25519 ' ~ item) }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ groups['pihole'] }}"
|
||||||
|
when: inventory_hostname != item
|
||||||
|
become: yes
|
||||||
|
become_user: "{{ pihole_user_gravity }}"
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Install Gravity Sync binary if absent
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Check if Gravity Sync binary exists
|
||||||
|
stat:
|
||||||
|
path: /usr/local/bin/gravity-sync
|
||||||
|
register: gravity_sync_bin
|
||||||
|
|
||||||
|
- name: Download installer
|
||||||
|
get_url:
|
||||||
|
url: https://raw.githubusercontent.com/vmstan/gs-install/main/gs-install.sh
|
||||||
|
dest: /tmp/gs-install.sh
|
||||||
|
mode: '0755'
|
||||||
|
when: not gravity_sync_bin.stat.exists
|
||||||
|
|
||||||
|
- name: Give full sudo to gravity user
|
||||||
|
copy:
|
||||||
|
dest: /etc/sudoers.d/gravity-sync
|
||||||
|
mode: '0440'
|
||||||
|
content: "{{ pihole_user_gravity }} ALL=(ALL) NOPASSWD: ALL"
|
||||||
|
when: not gravity_sync_bin.stat.exists
|
||||||
|
|
||||||
|
- name: Execute Gravity Sync installer non-interactively
|
||||||
|
command: bash /tmp/gs-install.sh
|
||||||
|
become: yes
|
||||||
|
become_user: "{{ pihole_user_gravity }}"
|
||||||
|
environment:
|
||||||
|
HOME: "{{ pihole_gravity_home }}"
|
||||||
|
when: not gravity_sync_bin.stat.exists
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Generate gravity-sync.conf for non-interactive use
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Set remote host for gravity-sync.conf
|
||||||
|
set_fact:
|
||||||
|
remote_pihole: "{{ (inventory_hostname == pihole_primary) | ternary(pihole_secondaries[0] ~ '.home', pihole_primary ~ '.home') }}"
|
||||||
|
|
||||||
|
- name: Create gravity-sync.conf file
|
||||||
|
copy:
|
||||||
|
dest: "{{ pihole_gravity_conf }}"
|
||||||
|
owner: "{{ pihole_user_gravity }}"
|
||||||
|
group: "{{ pihole_user_gravity }}"
|
||||||
|
mode: '0600'
|
||||||
|
content: |
|
||||||
|
# REQUIRED SETTINGS
|
||||||
|
REMOTE_HOST='{{ remote_pihole }}'
|
||||||
|
REMOTE_USER='{{ pihole_user_gravity }}'
|
||||||
|
|
||||||
|
# CUSTOM VARIABLES
|
||||||
|
# LOCAL_PIHOLE_DIRECTORY='/etc/pihole'
|
||||||
|
# REMOTE_PIHOLE_DIRECTORY='/etc/pihole'
|
||||||
|
# LOCAL_FILE_OWNER='{{ pihole_user_gravity }}'
|
||||||
|
# REMOTE_FILE_OWNER='{{ pihole_user_gravity }}'
|
||||||
|
|
||||||
|
# LOCAL_DOCKER_CONTAINER='' # optional
|
||||||
|
# REMOTE_DOCKER_CONTAINER='' # optional
|
||||||
|
|
||||||
|
- name: Create symlink for gravity-sync.rsa
|
||||||
|
file:
|
||||||
|
src: "{{ pihole_gravity_home }}/.ssh/id_ed25519"
|
||||||
|
dest: /etc/gravity-sync/gravity-sync.rsa
|
||||||
|
owner: "{{ pihole_user_gravity }}"
|
||||||
|
group: "{{ pihole_user_gravity }}"
|
||||||
|
mode: '0600'
|
||||||
|
state: link
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Execute Gravity Sync with non-interactive config
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Run Gravity Sync script
|
||||||
|
command: bash /usr/local/bin/gravity-sync
|
||||||
|
become: yes
|
||||||
|
become_user: "{{ pihole_user_gravity }}"
|
||||||
|
environment:
|
||||||
|
HOME: "{{ pihole_gravity_home }}"
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#################################################################
|
||||||
|
# Bootstrap Pi-hole (installation manuelle attendue)
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Proposer la commande d'installation manuelle de Pi-hole
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Veuillez installer Pi-hole manuellement sur ce host avec la commande suivante :
|
||||||
|
------------------------------------------------------------
|
||||||
|
curl -sSL https://install.pi-hole.net | sudo bash
|
||||||
|
------------------------------------------------------------
|
||||||
|
L'installation sera vérifiée automatiquement dans les 10 prochaines minutes.
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Vérification installation Pi-hole
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Attendre que Pi-hole soit installé (FTL DB)
|
||||||
|
wait_for:
|
||||||
|
path: /etc/pihole/pihole-FTL.db
|
||||||
|
state: present
|
||||||
|
timeout: 600 # 10 minutes
|
||||||
|
register: pihole_config_ready
|
||||||
|
|
||||||
|
- name: Vérifier que le service pihole-FTL est actif
|
||||||
|
wait_for:
|
||||||
|
port: 53
|
||||||
|
state: started
|
||||||
|
timeout: 60
|
||||||
|
when: pihole_config_ready is succeeded
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# Configuration Pi-hole (commune HA)
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Modifier le port d'écoute Pi-hole
|
||||||
|
replace:
|
||||||
|
path: /etc/pihole/pihole.toml
|
||||||
|
regexp: '^\s*port\s*=\s*".*"'
|
||||||
|
replace: ' port = "{{ pihole_ports }}"'
|
||||||
|
notify: Restart Pi-hole
|
||||||
|
|
||||||
|
- name: Autoriser Pi-hole à écouter sur toutes les interfaces
|
||||||
|
replace:
|
||||||
|
path: /etc/pihole/pihole.toml
|
||||||
|
regexp: '^\s*listeningMode\s*=\s*".*"'
|
||||||
|
replace: ' listeningMode = "ALL"'
|
||||||
|
notify: Restart Pi-hole
|
||||||
|
|
||||||
|
- name: Activer le chargement de /etc/dnsmasq.d
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/pihole/pihole.toml
|
||||||
|
regexp: '^\s*etc_dnsmasq_d\s*='
|
||||||
|
line: ' etc_dnsmasq_d = true'
|
||||||
|
state: present
|
||||||
|
notify: Restart Pi-hole
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
# DNS custom (wildcard + locaux)
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
- name: Validate custom DNS IPs
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- ip is match('^([0-9]{1,3}\.){3}[0-9]{1,3}$')
|
||||||
|
fail_msg: "Invalid IP for {{ fqdn }}"
|
||||||
|
loop: "{{ pihole_custom_dns | dict2items }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.key }}"
|
||||||
|
vars:
|
||||||
|
fqdn: "{{ item.key }}"
|
||||||
|
ip: "{{ item.value }}"
|
||||||
|
|
||||||
|
- name: Générer les règles DNS custom (wildcards + FQDN)
|
||||||
|
copy:
|
||||||
|
dest: /etc/dnsmasq.d/10-custom-rules.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
# Generated by Ansible – Pi-hole custom DNS rules
|
||||||
|
{% for fqdn, ip in pihole_custom_dns.items() %}
|
||||||
|
address=/{{ fqdn }}/{{ ip }}
|
||||||
|
{% endfor %}
|
||||||
|
when: pihole_custom_dns | length > 0
|
||||||
|
notify: Restart Pi-hole
|
||||||
|
|
||||||
|
- name: Créer les entrées DNS locales pour les RPis
|
||||||
|
copy:
|
||||||
|
dest: /etc/dnsmasq.d/20-rpis.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
# Generated by Ansible – Raspberry Pi local DNS
|
||||||
|
{% for host in groups['raspberries']
|
||||||
|
if hostvars[host].preferred_ip is defined %}
|
||||||
|
address=/{{ host }}.home/{{ hostvars[host].preferred_ip }}
|
||||||
|
{% endfor %}
|
||||||
|
notify: Restart Pi-hole
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
- name: Setup Pi-hole HA
|
||||||
|
include_tasks: ha_pihole_setup.yml
|
||||||
|
when: "'pihole' in group_names"
|
||||||
|
|
||||||
|
- name: Setup Gravity Sync
|
||||||
|
include_tasks: gravity_setup.yml
|
||||||
|
when: "'pihole' in group_names"
|
||||||
|
|
||||||
|
- name: Setup DNS client
|
||||||
|
include_tasks: client_setup.yml
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
# Setup factory services
|
# Setup factory services
|
||||||
|
|
||||||
|
## Changements à venir (V2)
|
||||||
|
|
||||||
|
Le NFS était un Single Point of Failure car en cas de panne de disque dur tout est corrumpu et inexploitable. Le disque de 4Tb ne sera donc plus utilisé.
|
||||||
|
|
||||||
|
À la place l'outil de réplication LongHorn sera utilisé via K3S sur des disques durs de 500Gb. Le storage class NFS ne sera plus disponible au profit de LongHorn.
|
||||||
|
|
||||||
|
Pour ne plus subir la corruption de la base de données postgres qui s'execute à l'exterieur de k3s la solution suivante est mise en place:
|
||||||
|
Un volume permanent avec accès ReadWriteMany et la classe de stockage LongHorn donne accès à un volume NFS administré par LongHorn. Ce volume servira de dossier où déposer les sauvegardes de postgres ou gitea périodiquement (1 fois par jour).
|
||||||
|
|
||||||
|
De plus, [longhorn exportera les données sur un storage externe](https://longhorn.io/docs/1.9.1/snapshots-and-backups/backup-and-restore/set-backup-target/#set-up-gcp-cloud-storage-backupstore).
|
||||||
|
|
||||||
|
## V1
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'rough':true } }%%
|
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'rough':true } }%%
|
||||||
flowchart
|
flowchart
|
||||||
|
|||||||
214
ansible/arcodange/factory/playbooks/setup/backup_nfs.yml
Normal file
214
ansible/arcodange/factory/playbooks/setup/backup_nfs.yml
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
- name: Créer volume RWX Longhorn pour sauvegardes
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: no
|
||||||
|
vars:
|
||||||
|
namespace_longhorn: longhorn-system
|
||||||
|
backup_volume_name: backups-rwx
|
||||||
|
backup_size: 50Gi
|
||||||
|
access_mode: ReadWriteMany
|
||||||
|
storage_class: longhorn
|
||||||
|
recurring_job: thrice-a-month-backup
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Créer PVC RWX dans longhorn-system
|
||||||
|
tags: never
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: "{{ backup_volume_name }}"
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- "{{ access_mode }}"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: "{{ backup_size }}"
|
||||||
|
storageClassName: "{{ storage_class }}"
|
||||||
|
|
||||||
|
- name: Récupérer infos du PVC
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
name: "{{ backup_volume_name }}"
|
||||||
|
register: pvc_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
until: pvc_info.resources is defined
|
||||||
|
|
||||||
|
- name: Extraire le nom du volume
|
||||||
|
set_fact:
|
||||||
|
pvc_internal_name: "{{ pvc_info.resources[0].spec.volumeName }}"
|
||||||
|
|
||||||
|
- name: Créer un RecurringJob pour backup quotidien à 5h du matin
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: longhorn.io/v1beta2
|
||||||
|
kind: RecurringJob
|
||||||
|
metadata:
|
||||||
|
name: "{{ recurring_job }}"
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
labels:
|
||||||
|
"recurring-job.longhorn.io": "{{ recurring_job }}"
|
||||||
|
spec:
|
||||||
|
name: "{{ recurring_job }}"
|
||||||
|
groups: []
|
||||||
|
task: backup
|
||||||
|
cron: "0 5 */2 * *"
|
||||||
|
retain: 2
|
||||||
|
concurrency: 1
|
||||||
|
|
||||||
|
- name: Attacher le volume au recurring job
|
||||||
|
kubernetes.core.k8s_json_patch:
|
||||||
|
api_version: longhorn.io/v1beta2
|
||||||
|
kind: Volume
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
name: "{{ pvc_internal_name }}"
|
||||||
|
patch:
|
||||||
|
- op: replace
|
||||||
|
path: "/metadata/labels/backup-target"
|
||||||
|
value: "default"
|
||||||
|
- op: replace
|
||||||
|
path: "/metadata/labels/recurring-job.longhorn.io~1{{ recurring_job }}"
|
||||||
|
value: "enabled"
|
||||||
|
|
||||||
|
- name: Lancer un Deployment pour déclencher NFS
|
||||||
|
tags: never
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: rwx-nfs
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: rwx-nfs
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: rwx-nfs
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: busybox
|
||||||
|
image: busybox
|
||||||
|
command: ["sleep", "infinity"]
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: "/mnt/backups"
|
||||||
|
name: backup-vol
|
||||||
|
volumes:
|
||||||
|
- name: backup-vol
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: "{{ backup_volume_name }}"
|
||||||
|
|
||||||
|
- name: Attendre que le pod rwx-nfs soit Running
|
||||||
|
tags: never
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: Pod
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
label_selectors:
|
||||||
|
- app = rwx-nfs
|
||||||
|
register: pod_info
|
||||||
|
until: pod_info.resources[0].status.phase == "Running"
|
||||||
|
retries: 30
|
||||||
|
delay: 5
|
||||||
|
|
||||||
|
- name: Vérifier si un Service NFS existe déjà (par labels)
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: Service
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
label_selectors:
|
||||||
|
- "longhorn.io/managed-by=longhorn-manager"
|
||||||
|
- "longhorn.io/share-manager={{ pvc_internal_name }}"
|
||||||
|
register: existing_nfs_service
|
||||||
|
|
||||||
|
- name: Définir le nom du Service existant si trouvé
|
||||||
|
set_fact:
|
||||||
|
nfs_service_name: "{{ existing_nfs_service.resources[0].metadata.name }}"
|
||||||
|
when: existing_nfs_service.resources | length > 0
|
||||||
|
|
||||||
|
- name: Créer un Service NFS stable si aucun trouvé
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: "nfs-{{ backup_volume_name }}"
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
longhorn.io/share-manager: "{{ pvc_internal_name }}"
|
||||||
|
ports:
|
||||||
|
- name: nfs
|
||||||
|
protocol: TCP
|
||||||
|
port: 2049
|
||||||
|
targetPort: 2049
|
||||||
|
type: ClusterIP
|
||||||
|
when: existing_nfs_service.resources | length == 0
|
||||||
|
|
||||||
|
- name: Définir le nom du Service NFS créé si nécessaire
|
||||||
|
set_fact:
|
||||||
|
nfs_service_name: "nfs-{{ backup_volume_name }}"
|
||||||
|
when: existing_nfs_service.resources | length == 0
|
||||||
|
|
||||||
|
- name: Récupérer infos du Service NFS choisi
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
kind: Service
|
||||||
|
name: "{{ nfs_service_name }}"
|
||||||
|
namespace: "{{ namespace_longhorn }}"
|
||||||
|
register: nfs_service
|
||||||
|
|
||||||
|
- name: Sauvegarder infos NFS
|
||||||
|
set_fact:
|
||||||
|
nfs_info:
|
||||||
|
ip: "{{ nfs_service.resources[0].spec.clusterIP }}"
|
||||||
|
path: "/{{ pvc_internal_name }}/"
|
||||||
|
|
||||||
|
- name: Monter volume RWX Longhorn sur Raspberry
|
||||||
|
hosts: raspberries:&local
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
backup_mount: "/mnt/backups"
|
||||||
|
tasks:
|
||||||
|
- name: Installer client NFS
|
||||||
|
apt:
|
||||||
|
name: nfs-common
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Créer point de montage
|
||||||
|
file:
|
||||||
|
path: "{{ backup_mount }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Monter volume de backup
|
||||||
|
mount:
|
||||||
|
path: "{{ backup_mount }}"
|
||||||
|
src: "{{ hostvars['localhost'].nfs_info.ip }}:{{ hostvars['localhost'].nfs_info.path }}"
|
||||||
|
fstype: nfs
|
||||||
|
opts: vers=4.1,rw,nofail,_netdev,x-systemd.automount
|
||||||
|
state: mounted
|
||||||
|
|
||||||
|
- name: Ajouter entrée dans fstab pour montage automatique
|
||||||
|
mount:
|
||||||
|
path: "{{ backup_mount }}"
|
||||||
|
src: "{{ hostvars['localhost'].nfs_info.ip }}:{{ hostvars['localhost'].nfs_info.path }}"
|
||||||
|
fstype: nfs
|
||||||
|
opts: vers=4.1,rw,nofail,_netdev,x-systemd.automount
|
||||||
|
state: present
|
||||||
|
|
||||||
|
|
||||||
@@ -1,44 +1,36 @@
|
|||||||
---
|
---
|
||||||
- name: Setup Gitea
|
- name: Setup Gitea
|
||||||
hosts: gitea:&hard_disk
|
hosts: gitea
|
||||||
gather_facts: yes
|
gather_facts: yes
|
||||||
become: false
|
become: false
|
||||||
run_once: true
|
run_once: true
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
applications: "{{ hard_disk__applications }}"
|
app: "{{ gitea }}"
|
||||||
|
app_name: gitea
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Deploy gitea Docker Compose configuration
|
- name: Deploy gitea Docker Compose configuration
|
||||||
include_role:
|
include_role:
|
||||||
name: arcodange.factory.deploy_docker_compose
|
name: arcodange.factory.deploy_docker_compose
|
||||||
vars:
|
vars:
|
||||||
app_name: "{{ app.name }}"
|
dockercompose_content: "{{ app.dockercompose }}"
|
||||||
dockercompose_content: "{{ app.conf.dockercompose }}"
|
app_owner: "{{ app.owner | default('pi') }}"
|
||||||
partition: "{{ app.conf.partition }}"
|
app_group: "{{ app.group | default('docker') }}"
|
||||||
app_owner: "{{ app.conf.owner | default('pi') }}"
|
|
||||||
app_group: "{{ app.conf.group | default('docker') }}"
|
|
||||||
loop: "{{ applications | dict2items(key_name='name', value_name='conf') }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: app
|
|
||||||
label: "{{ app.name }}"
|
|
||||||
when: app.name == 'gitea'
|
|
||||||
|
|
||||||
- name: Deploy Gitea
|
- name: Deploy Gitea
|
||||||
include_role:
|
include_role:
|
||||||
name: deploy_gitea
|
name: deploy_gitea
|
||||||
vars:
|
vars:
|
||||||
app_name: gitea
|
gitea_container_name: "{{ gitea.dockercompose.services.gitea.container_name }}"
|
||||||
partition: "{{ applications.gitea.partition }}"
|
|
||||||
gitea_container_name: "{{ applications.gitea.dockercompose.services.gitea.container_name }}"
|
|
||||||
postgres_host: |-
|
postgres_host: |-
|
||||||
{{ applications.gitea.dockercompose.services.gitea.environment.GITEA__database__HOST }}
|
{{ gitea.dockercompose.services.gitea.environment.GITEA__database__HOST }}
|
||||||
postgres_db: |-
|
postgres_db: |-
|
||||||
{{ applications.gitea.dockercompose.services.gitea.environment.GITEA__database__NAME }}
|
{{ gitea.dockercompose.services.gitea.environment.GITEA__database__NAME }}
|
||||||
postgres_user: |-
|
postgres_user: |-
|
||||||
{{ applications.gitea.dockercompose.services.gitea.environment.GITEA__database__USER }}
|
{{ gitea.dockercompose.services.gitea.environment.GITEA__database__USER }}
|
||||||
postgres_password: |-
|
postgres_password: |-
|
||||||
{{ applications.gitea.dockercompose.services.gitea.environment.GITEA__database__PASSWD }}
|
{{ gitea.dockercompose.services.gitea.environment.GITEA__database__PASSWD }}
|
||||||
|
|
||||||
- name: Create admin user
|
- name: Create admin user
|
||||||
block:
|
block:
|
||||||
@@ -54,7 +46,7 @@
|
|||||||
- name: List admin users
|
- name: List admin users
|
||||||
ansible.builtin.shell:
|
ansible.builtin.shell:
|
||||||
cmd: >-
|
cmd: >-
|
||||||
docker exec -u git {{ applications.gitea.dockercompose.services.gitea.container_name }}
|
docker exec -u git {{ gitea.dockercompose.services.gitea.container_name }}
|
||||||
gitea admin user list --admin
|
gitea admin user list --admin
|
||||||
| awk '{print $2}'
|
| awk '{print $2}'
|
||||||
| tail -n +2
|
| tail -n +2
|
||||||
@@ -65,7 +57,7 @@
|
|||||||
- name: Create admin user
|
- name: Create admin user
|
||||||
when: gitea_user.name not in gitea_admin_users_list_cmd.stdout.split()
|
when: gitea_user.name not in gitea_admin_users_list_cmd.stdout.split()
|
||||||
ansible.builtin.command: >-
|
ansible.builtin.command: >-
|
||||||
docker exec -u git {{ applications.gitea.dockercompose.services.gitea.container_name }}
|
docker exec -u git {{ gitea.dockercompose.services.gitea.container_name }}
|
||||||
gitea admin user create
|
gitea admin user create
|
||||||
--username {{ gitea_user.name }}
|
--username {{ gitea_user.name }}
|
||||||
--email {{ gitea_user.email }}
|
--email {{ gitea_user.email }}
|
||||||
@@ -134,14 +126,14 @@
|
|||||||
debug:
|
debug:
|
||||||
msg: >-
|
msg: >-
|
||||||
Clé SSH ajoutée avec succès.
|
Clé SSH ajoutée avec succès.
|
||||||
Visitez https://gitea.arcodange.duckdns.org/user/settings/keys?verify_ssh={{ add_ssh_key_result.json.fingerprint }}
|
Visitez https://gitea.arcodange.lab/user/settings/keys?verify_ssh={{ add_ssh_key_result.json.fingerprint }}
|
||||||
pour vérifier la signature de vos commits avec cette clé.
|
pour vérifier la signature de vos commits avec cette clé.
|
||||||
|
|
||||||
- set_fact:
|
- set_fact:
|
||||||
gitea_org_name: arcodange-org
|
gitea_org_name: arcodange-org
|
||||||
gitea_org_full_name: Arcodange
|
gitea_org_full_name: Arcodange
|
||||||
gitea_org_description: '🏹💻🪽'
|
gitea_org_description: '🏹💻🪽'
|
||||||
gitea_org_website: https://www.arcodange.duckdns.org
|
gitea_org_website: https://www.arcodange.fr
|
||||||
gitea_org_location: Paris
|
gitea_org_location: Paris
|
||||||
gitea_org_avatar_img_path: '{{ inventory_dir }}/../img/arcodange-org.jpeg'
|
gitea_org_avatar_img_path: '{{ inventory_dir }}/../img/arcodange-org.jpeg'
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# awesome commands:
|
|
||||||
# sudo fdisk -l
|
|
||||||
# sudo parted -l
|
|
||||||
# sudo gparted -- partitionnement graphique <-- utilisé pour créer la partition et donner le nom+label 'gitea_data'
|
|
||||||
# sudo testdisk -- recuperation
|
|
||||||
# sudo blkid -- uuid des partitions pour configurer gstab (mount auto)
|
|
||||||
# lsblk -fe7 -- uuid des partitions
|
|
||||||
---
|
|
||||||
- name: Setup Hard Disk
|
|
||||||
hosts: hard_disk
|
|
||||||
gather_facts: yes
|
|
||||||
become: yes
|
|
||||||
|
|
||||||
vars:
|
|
||||||
mount_points: |
|
|
||||||
{{
|
|
||||||
(
|
|
||||||
hard_disk__partitions
|
|
||||||
| default( {
|
|
||||||
'gitea_data':[],
|
|
||||||
'pg_data':[]
|
|
||||||
} )
|
|
||||||
).keys() | list
|
|
||||||
}}
|
|
||||||
verify_partitions: true # Change this to false if you don't want to verify partitions
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Setup partitions
|
|
||||||
include_role:
|
|
||||||
name: setup_partition
|
|
||||||
loop: "{{ mount_points }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: mount_point
|
|
||||||
|
|
||||||
- name: Setup NFS
|
|
||||||
include_role:
|
|
||||||
name: nfs_setup
|
|
||||||
|
|
||||||
- name: Set permissions for group docker on /arcodange
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "/arcodange/{{ subdir }}"
|
|
||||||
state: directory
|
|
||||||
owner: pi
|
|
||||||
group: docker
|
|
||||||
mode: u=rwX,g=rX,o=r
|
|
||||||
loop: "{{ [''] + mount_points }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: subdir
|
|
||||||
|
|
||||||
- name: Set ACL for group docker on /arcodange
|
|
||||||
ansible.posix.acl:
|
|
||||||
path: "/arcodange/{{ subdir }}"
|
|
||||||
entity: "docker"
|
|
||||||
etype: "group"
|
|
||||||
permissions: "rwx"
|
|
||||||
state: present
|
|
||||||
loop: "{{ [''] + mount_points }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: subdir
|
|
||||||
|
|
||||||
- name: Mount NFS
|
|
||||||
hosts: raspberries:&local
|
|
||||||
become: yes
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Setup NFS
|
|
||||||
include_role:
|
|
||||||
name: nfs_setup
|
|
||||||
tasks_from: mount
|
|
||||||
vars:
|
|
||||||
nfs_setup_export_directory: "{{ hard_disk__nfs.export_directory | default(hostvars[groups.hard_disk[0]].hard_disk__nfs.export_directory) }}"
|
|
||||||
nfs_setup_server_ip: "{{ hard_disk__nfs.server_ip | default(hostvars[groups.hard_disk[0]].ansible_host) }}"
|
|
||||||
@@ -1,37 +1,29 @@
|
|||||||
---
|
---
|
||||||
- name: Setup Postgres
|
- name: Setup Postgres
|
||||||
hosts: hard_disk
|
hosts: postgres
|
||||||
gather_facts: yes
|
gather_facts: yes
|
||||||
become: false
|
become: false
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
applications: "{{ hard_disk__applications }}"
|
app: "{{ postgres }}"
|
||||||
applications_databases: "{{ hard_disk__postgres_databases }}"
|
app_name: postgres
|
||||||
postgres_container_name: "{{ applications.postgres.dockercompose.services.postgres.container_name }}"
|
postgres_container_name: "{{ postgres.dockercompose.services.postgres.container_name }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Deploy postgres Docker Compose configuration
|
- name: Deploy postgres Docker Compose configuration
|
||||||
include_role:
|
include_role:
|
||||||
name: arcodange.factory.deploy_docker_compose
|
name: arcodange.factory.deploy_docker_compose
|
||||||
vars:
|
vars:
|
||||||
app_name: "{{ app.name }}"
|
dockercompose_content: "{{ app.dockercompose }}"
|
||||||
dockercompose_content: "{{ app.conf.dockercompose }}"
|
app_owner: "{{ app.owner | default('pi') }}"
|
||||||
partition: "{{ app.conf.partition }}"
|
app_group: "{{ app.group | default('docker') }}"
|
||||||
app_owner: "{{ app.conf.owner | default('pi') }}"
|
|
||||||
app_group: "{{ app.conf.group | default('docker') }}"
|
|
||||||
loop: "{{ applications | dict2items(key_name='name', value_name='conf') }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: app
|
|
||||||
label: "{{ app.name }}"
|
|
||||||
when: app.name == 'postgres'
|
|
||||||
|
|
||||||
- name: Deploy PostgreSQL
|
- name: Deploy PostgreSQL
|
||||||
include_role:
|
include_role:
|
||||||
name: deploy_postgresql
|
name: deploy_postgresql
|
||||||
vars:
|
vars:
|
||||||
app_name: postgres
|
applications_databases:
|
||||||
partition: "{{ applications.postgres.partition }}"
|
gitea: "{{ gitea_database }}"
|
||||||
# applications_databases: "{{ applications_databases }}" # kept for documentation purposes
|
|
||||||
|
|
||||||
- name: Create auth_user for pgbouncer (connection pool component)
|
- name: Create auth_user for pgbouncer (connection pool component)
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
@@ -62,4 +54,124 @@
|
|||||||
loop_control:
|
loop_control:
|
||||||
loop_var: database__pg_instruction
|
loop_var: database__pg_instruction
|
||||||
loop:
|
loop:
|
||||||
"{{ applications_databases.values() | map(attribute='db_name') | product(pg_instructions) }}"
|
"{{ ['postgres', 'gitea'] | product(pg_instructions) }}"
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
- name: Change table owner (CronJob with dynamic roles and auto DB naming)
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
collections:
|
||||||
|
- kubernetes.core
|
||||||
|
|
||||||
|
vars:
|
||||||
|
|
||||||
|
namespace: kube-system
|
||||||
|
cronjob_name: pg-fix-table-ownership
|
||||||
|
|
||||||
|
pg_conf: >-
|
||||||
|
{{ hostvars[groups.postgres[0]].postgres.dockercompose.services.postgres.environment }}
|
||||||
|
postgres_admin_credentials:
|
||||||
|
username: '{{ pg_conf.POSTGRES_USER }}'
|
||||||
|
password: '{{ pg_conf.POSTGRES_PASSWORD }}'
|
||||||
|
pg_host: "{{ hostvars[groups.postgres[0]]['preferred_ip'] }}"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: Create Kubernetes Secret for PostgreSQL admin credentials
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: postgres-admin-credentials
|
||||||
|
namespace: "{{ namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
username: "{{ postgres_admin_credentials.username | b64encode }}"
|
||||||
|
password: "{{ postgres_admin_credentials.password | b64encode }}"
|
||||||
|
|
||||||
|
- name: Create cronjob to change table owners (dynamic roles, auto DB)
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: "{{ cronjob_name }}"
|
||||||
|
namespace: "{{ namespace }}"
|
||||||
|
spec:
|
||||||
|
schedule: "0 3 * * *" # Exécution quotidienne à 3h du matin
|
||||||
|
successfulJobsHistoryLimit: 1
|
||||||
|
failedJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: psql
|
||||||
|
image: postgres:16.3
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: postgres-admin-credentials
|
||||||
|
env:
|
||||||
|
- name: PGPASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: postgres-admin-credentials
|
||||||
|
key: password
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Récupérer dynamiquement les rôles PostgreSQL
|
||||||
|
echo "Fetching roles from PostgreSQL..."
|
||||||
|
ROLES=$(psql \
|
||||||
|
-h {{ pg_host }} \
|
||||||
|
-U $username \
|
||||||
|
-d postgres \
|
||||||
|
-t -A \
|
||||||
|
-c "SELECT rolname FROM pg_roles WHERE rolname LIKE '%_role';")
|
||||||
|
|
||||||
|
echo "Roles found: $ROLES"
|
||||||
|
|
||||||
|
# Pour chaque rôle, changer le propriétaire des tables dans sa base associée
|
||||||
|
for role in $ROLES; do
|
||||||
|
# Déduire le nom de la base en retirant "_role"
|
||||||
|
DB_NAME="${role%_role}"
|
||||||
|
echo "Database for $role: $DB_NAME"
|
||||||
|
|
||||||
|
# Vérifier si la base existe
|
||||||
|
if psql -h {{ pg_host }} -U $username -d postgres -t -A -c "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME';" | grep -q 1; then
|
||||||
|
echo "Changing owner to $role for all tables in $DB_NAME..."
|
||||||
|
psql \
|
||||||
|
-h {{ pg_host }} \
|
||||||
|
-U $username \
|
||||||
|
-d "$DB_NAME" \
|
||||||
|
-c "
|
||||||
|
DO \$\$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
LOOP
|
||||||
|
EXECUTE format('ALTER TABLE public.%I OWNER TO %I', r.tablename, '$role');
|
||||||
|
END LOOP;
|
||||||
|
END \$\$;
|
||||||
|
"
|
||||||
|
echo "Owner changed for $role in $DB_NAME"
|
||||||
|
else
|
||||||
|
echo "Database $DB_NAME does not exist, skipping..."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
app_owner: pi
|
app_owner: pi
|
||||||
app_group: docker
|
app_group: docker
|
||||||
app_name: gitea
|
app_name: gitea
|
||||||
partition: gitea_data
|
partition: docker_composes
|
||||||
config_path: /arcodange/{{ partition }}/{{ app_name }}/config
|
config_path: /home/pi/arcodange/{{ partition }}/{{ app_name }}/config
|
||||||
data_path: /arcodange/{{ partition }}/{{ app_name }}/data
|
data_path: /home/pi/arcodange/{{ partition }}/{{ app_name }}/data
|
||||||
gitea_user:
|
gitea_user:
|
||||||
name: arcodange
|
name: arcodange
|
||||||
email: arcodange@gmail.com
|
email: arcodange@gmail.com
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
- name: Deploy Gitea with Docker Compose
|
- name: Deploy Gitea with Docker Compose
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "/arcodange/{{ partition }}/{{ app_name }}"
|
project_src: "/home/pi/arcodange/{{ partition }}/{{ app_name }}"
|
||||||
pull: missing
|
pull: missing
|
||||||
state: present
|
state: present
|
||||||
register: deploy_result
|
register: deploy_result
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ APP_NAME = Arcodange repositories
|
|||||||
[server]
|
[server]
|
||||||
DOMAIN = localhost
|
DOMAIN = localhost
|
||||||
HTTP_PORT = 3000
|
HTTP_PORT = 3000
|
||||||
ROOT_URL = https://gitea.arcodange.duckdns.org/
|
ROOT_URL = https://gitea.arcodange.lab/
|
||||||
DISABLE_SSH = false
|
DISABLE_SSH = false
|
||||||
SSH_PORT = 22
|
SSH_PORT = 22
|
||||||
START_SSH_SERVER = true
|
START_SSH_SERVER = true
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
- name: Deploy PostgreSQL with Docker Compose
|
- name: Deploy PostgreSQL with Docker Compose
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "/arcodange/{{ partition }}/{{ app_name }}"
|
project_src: "/home/pi/arcodange/docker_composes/{{ app_name }}"
|
||||||
pull: missing
|
pull: missing
|
||||||
state: present
|
state: present
|
||||||
register: deploy_result
|
register: deploy_result
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
nfs_setup_export_directory: /arcodange/nfs
|
|
||||||
# nfs_setup_server_ip: "{{ hostvars['pi2'].ansible_default_ipv4.address }}"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install Avahi and related packages
|
|
||||||
ansible.builtin.apt: # https://www.baeldung.com/linux/conflicting-values-error-resolution
|
|
||||||
name: "{{ item }}"
|
|
||||||
state: present
|
|
||||||
update_cache: yes
|
|
||||||
with_items:
|
|
||||||
- avahi-daemon
|
|
||||||
- avahi-utils
|
|
||||||
|
|
||||||
- name: Create Avahi service file for NFS
|
|
||||||
template:
|
|
||||||
src: nfs.service.j2
|
|
||||||
dest: /etc/avahi/services/nfs.service
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: '0644'
|
|
||||||
|
|
||||||
- name: Restart Avahi daemon
|
|
||||||
service:
|
|
||||||
name: avahi-daemon
|
|
||||||
state: restarted
|
|
||||||
enabled: yes
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install NFS server package
|
|
||||||
ansible.builtin.apt: # https://www.baeldung.com/linux/conflicting-values-error-resolution
|
|
||||||
name: nfs-kernel-server
|
|
||||||
state: present
|
|
||||||
update_cache: yes
|
|
||||||
|
|
||||||
- name: Create export directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ nfs_setup_export_directory }}"
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: '0755'
|
|
||||||
|
|
||||||
- name: Configure /etc/exports
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/exports
|
|
||||||
line: "{{ nfs_setup_export_directory }} 192.168.1.0/24(rw,sync,no_subtree_check,anonuid=1000,anongid=1000)"
|
|
||||||
create: yes
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Ensure NFS service is running and enabled
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: nfs-kernel-server
|
|
||||||
state: started
|
|
||||||
enabled: yes
|
|
||||||
|
|
||||||
- name: Export the shared directories
|
|
||||||
ansible.builtin.command: exportfs -ra
|
|
||||||
|
|
||||||
- name: Verify NFS exports
|
|
||||||
ansible.builtin.command: exportfs -v
|
|
||||||
register: nfs_exports
|
|
||||||
|
|
||||||
- ansible.builtin.debug:
|
|
||||||
msg: "NFS Exports: {{ nfs_exports.stdout }}"
|
|
||||||
|
|
||||||
- include_tasks: announce.yml
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install NFS client package
|
|
||||||
ansible.builtin.apt: # https://www.baeldung.com/linux/conflicting-values-error-resolution
|
|
||||||
name: nfs-common
|
|
||||||
state: present
|
|
||||||
update_cache: yes
|
|
||||||
|
|
||||||
- name: Create local mount directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /mnt/nfs
|
|
||||||
state: directory
|
|
||||||
owner: pi
|
|
||||||
group: docker
|
|
||||||
mode: '0774'
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Mount NFS share
|
|
||||||
mount:
|
|
||||||
src: "{{ nfs_setup_server_ip }}:{{ nfs_setup_export_directory }}"
|
|
||||||
path: /mnt/nfs
|
|
||||||
fstype: nfs
|
|
||||||
opts: rw,vers=4
|
|
||||||
state: mounted
|
|
||||||
ignore_errors: true
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" standalone='no'?>
|
|
||||||
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
|
||||||
<service-group>
|
|
||||||
<name replace-wildcards="yes">%h NFS</name>
|
|
||||||
<service>
|
|
||||||
<type>_nfs._tcp</type>
|
|
||||||
<port>2049</port>
|
|
||||||
</service>
|
|
||||||
</service-group>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
Role Name
|
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
------------
|
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
|
||||||
|
|
||||||
Role Variables
|
|
||||||
--------------
|
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
|
||||||
|
|
||||||
Dependencies
|
|
||||||
------------
|
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
|
||||||
|
|
||||||
Example Playbook
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
|
||||||
|
|
||||||
- hosts: servers
|
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
# defaults file for roles/setup_partition
|
|
||||||
mount_points: []
|
|
||||||
verify_partitions: false
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Optionally verify partition existence
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
device: "/dev/sda"
|
|
||||||
when: verify_partitions | default(false)
|
|
||||||
|
|
||||||
- name: Read device information
|
|
||||||
community.general.parted:
|
|
||||||
device: "{{ device }}"
|
|
||||||
unit: GiB
|
|
||||||
register: device_info
|
|
||||||
when: verify_partitions | default(false)
|
|
||||||
|
|
||||||
- name: Select partition
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
disk: |-
|
|
||||||
{{ device + (
|
|
||||||
device_info | to_json | from_json
|
|
||||||
| community.general.json_query(jmes_path) | string
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
vars:
|
|
||||||
jmes_path: partitions[?name == '{{ mount_point }}'].num | [0]
|
|
||||||
failed_when: disk.endswith('None')
|
|
||||||
when: verify_partitions | default(false)
|
|
||||||
|
|
||||||
- name: Check if mount directory exists
|
|
||||||
stat:
|
|
||||||
path: "/arcodange/{{ mount_point }}"
|
|
||||||
register: mount_dir_stat
|
|
||||||
|
|
||||||
- name: Create the mount directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "/arcodange/{{ mount_point }}"
|
|
||||||
state: directory
|
|
||||||
when: not mount_dir_stat.stat.exists
|
|
||||||
|
|
||||||
- name: Declare mount point
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/fstab
|
|
||||||
line: "LABEL={{ mount_point }} /arcodange/{{ mount_point }} ext4 defaults 0 0"
|
|
||||||
|
|
||||||
- name: Use updated mount list
|
|
||||||
ansible.builtin.command: mount -a
|
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- ansible.builtin.ping:
|
- ansible.builtin.ping:
|
||||||
|
|
||||||
- name: setup hard disk
|
- name: prepare backups shared directory
|
||||||
ansible.builtin.import_playbook: hard_disk.yml
|
ansible.builtin.import_playbook: backup_nfs.yml
|
||||||
tags: never
|
tags: never
|
||||||
|
|
||||||
- name: setup factory postgres
|
- name: setup factory postgres
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
step_ca_primary: pi1
|
||||||
|
step_ca_user: step
|
||||||
|
step_ca_home: /home/step
|
||||||
|
step_ca_dir: /home/step/.step
|
||||||
|
|
||||||
|
step_ca_name: "Arcodange Lab CA"
|
||||||
|
step_ca_fqdn: ssl-ca.arcodange.lab
|
||||||
|
step_ca_listen_address: ":8443"
|
||||||
|
|
||||||
|
step_ca_password: "{{ vault_step_ca_password }}"
|
||||||
|
step_ca_force_reinit: false
|
||||||
|
|
||||||
|
step_ca_provisioner_name: cert-manager
|
||||||
|
step_ca_provisioner_type: JWK
|
||||||
|
step_ca_jwk_dir: "{{ step_ca_dir }}/provisioners"
|
||||||
|
step_ca_jwk_key: "{{ step_ca_jwk_dir }}/cert-manager.jwk"
|
||||||
|
step_ca_jwk_password: "{{ vault_step_ca_jwk_password }}"
|
||||||
|
step_ca_jwk_password_file: "{{ step_ca_dir }}/secrets/cert-manager.jwk.pass"
|
||||||
|
|
||||||
|
step_ca_url: "https://{{ step_ca_fqdn }}{{ step_ca_listen_address }}"
|
||||||
|
step_ca_root: "{{ step_ca_dir }}/certs/root_ca.crt"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- name: restart step-ca
|
||||||
|
systemd:
|
||||||
|
name: step-ca
|
||||||
|
state: restarted
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# can be called with -e step_ca_force_reinit=true
|
||||||
|
|
||||||
|
# 1️⃣ Vérifier si le CA est déjà initialisé
|
||||||
|
- name: Check if CA already initialized
|
||||||
|
stat:
|
||||||
|
path: "{{ step_ca_dir }}/config/ca.json"
|
||||||
|
register: step_ca_initialized
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
# 2️⃣ Arrêter step-ca si reinit forcée
|
||||||
|
- name: Stop step-ca service (reinit)
|
||||||
|
systemd:
|
||||||
|
name: step-ca
|
||||||
|
state: stopped
|
||||||
|
when:
|
||||||
|
- inventory_hostname == step_ca_primary
|
||||||
|
- step_ca_force_reinit | bool
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
# 3️⃣ Wipe complet du CA si reinit forcée
|
||||||
|
- name: Wipe existing step-ca data
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_dir }}"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- inventory_hostname == step_ca_primary
|
||||||
|
- step_ca_force_reinit | bool
|
||||||
|
|
||||||
|
# 4️⃣ Recréer le dossier CA proprement
|
||||||
|
- name: Recreate step-ca directory
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
when:
|
||||||
|
- inventory_hostname == step_ca_primary
|
||||||
|
- step_ca_force_reinit | bool
|
||||||
|
|
||||||
|
# 5️⃣ Installer le fichier de mot de passe
|
||||||
|
- name: Install step-ca password file
|
||||||
|
copy:
|
||||||
|
dest: "{{ step_ca_home }}/.step-pass"
|
||||||
|
content: "{{ step_ca_password }}"
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0600"
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
# 6️⃣ Initialiser step-ca (non interactif)
|
||||||
|
- name: Initialize step-ca
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
command: >
|
||||||
|
step ca init
|
||||||
|
--name "{{ step_ca_name }}"
|
||||||
|
--dns "{{ step_ca_fqdn }}"
|
||||||
|
--address "{{ step_ca_listen_address }}"
|
||||||
|
--provisioner admin
|
||||||
|
--password-file {{ step_ca_home }}/.step-pass
|
||||||
|
args:
|
||||||
|
creates: "{{ step_ca_dir }}/config/ca.json"
|
||||||
|
when:
|
||||||
|
- inventory_hostname == step_ca_primary
|
||||||
|
- step_ca_force_reinit | bool or not step_ca_initialized.stat.exists
|
||||||
|
notify: restart step-ca
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
- name: Install base packages
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- curl
|
||||||
|
- vim
|
||||||
|
- gpg
|
||||||
|
- ca-certificates
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
install_recommends: no
|
||||||
|
|
||||||
|
- name: Download Smallstep apt signing key
|
||||||
|
get_url:
|
||||||
|
url: https://packages.smallstep.com/keys/apt/repo-signing-key.gpg
|
||||||
|
dest: /etc/apt/trusted.gpg.d/smallstep.asc
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Add Smallstep apt repository
|
||||||
|
copy:
|
||||||
|
dest: /etc/apt/sources.list.d/smallstep.list
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
deb [signed-by=/etc/apt/trusted.gpg.d/smallstep.asc] https://packages.smallstep.com/stable/debian debs main
|
||||||
|
|
||||||
|
- name: Update apt cache
|
||||||
|
apt:
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Install step-cli and step-ca
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- step-cli
|
||||||
|
- step-ca
|
||||||
|
state: present
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Create step user
|
||||||
|
user:
|
||||||
|
name: "{{ step_ca_user }}"
|
||||||
|
system: true
|
||||||
|
shell: /usr/sbin/nologin
|
||||||
|
home: "{{ step_ca_home }}"
|
||||||
|
|
||||||
|
- name: Secure step directory
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_dir }}"
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
recurse: yes
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- import_tasks: install.yml
|
||||||
|
- import_tasks: init.yml
|
||||||
|
- import_tasks: sync.yml
|
||||||
|
- import_tasks: systemd.yml
|
||||||
|
- import_tasks: provisioners.yml
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
- name: Ensure provisioner directory exists
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_jwk_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
- name: Check if JWK provisioner already exists
|
||||||
|
command: >
|
||||||
|
step ca provisioner list
|
||||||
|
--ca-url {{ step_ca_url }}
|
||||||
|
--root {{ step_ca_root }}
|
||||||
|
register: step_ca_provisioners
|
||||||
|
changed_when: false
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
- name: Check if cert-manager provisioner exists
|
||||||
|
set_fact:
|
||||||
|
step_ca_provisioner_exists: >-
|
||||||
|
{{
|
||||||
|
(step_ca_provisioners.stdout | from_json
|
||||||
|
| selectattr('name', 'equalto', step_ca_provisioner_name)
|
||||||
|
| list
|
||||||
|
| length) > 0
|
||||||
|
}}
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
- name: Install JWK password file
|
||||||
|
copy:
|
||||||
|
dest: "{{ step_ca_jwk_password_file }}"
|
||||||
|
content: "{{ step_ca_jwk_password }}"
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0400"
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
- name: Generate JWK key for cert-manager
|
||||||
|
command: >
|
||||||
|
step crypto jwk create
|
||||||
|
{{ step_ca_jwk_key }}.pub
|
||||||
|
{{ step_ca_jwk_key }}
|
||||||
|
--password-file "{{ step_ca_jwk_password_file }}"
|
||||||
|
args:
|
||||||
|
creates: "{{ step_ca_jwk_key }}"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
- name: Add JWK provisioner to step-ca
|
||||||
|
command: >
|
||||||
|
step ca provisioner add {{ step_ca_provisioner_name }}
|
||||||
|
--type JWK
|
||||||
|
--public-key {{ step_ca_jwk_key }}.pub
|
||||||
|
--private-key {{ step_ca_jwk_key }}
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
when:
|
||||||
|
- inventory_hostname == step_ca_primary
|
||||||
|
- step_ca_provisioner_name not in step_ca_provisioners.stdout
|
||||||
|
notify: restart step-ca
|
||||||
|
|
||||||
|
- name: Secure JWK keys permissions
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_jwk_dir }}"
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
recurse: yes
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# 1️⃣ Lock sur le primaire (évite double sync concurrente)
|
||||||
|
- name: Create sync lock on primary
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_dir }}/.sync.lock"
|
||||||
|
state: touch
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0600"
|
||||||
|
delegate_to: "{{ step_ca_primary }}"
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
# 2️⃣ Calcul du checksum du CA sur le primaire
|
||||||
|
- name: Compute deterministic checksum of CA directory on primary
|
||||||
|
shell: |
|
||||||
|
set -o pipefail
|
||||||
|
tar --sort=name \
|
||||||
|
--mtime='UTC 1970-01-01' \
|
||||||
|
--owner=0 --group=0 --numeric-owner \
|
||||||
|
-cf - {{ step_ca_dir }} \
|
||||||
|
| sha256sum | awk '{print $1}'
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: step_ca_primary_checksum
|
||||||
|
changed_when: false
|
||||||
|
delegate_to: "{{ step_ca_primary }}"
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
# 3️⃣ Charger le checksum précédent (s'il existe)
|
||||||
|
- name: Load previous checksum (controller)
|
||||||
|
slurp:
|
||||||
|
src: /tmp/step-ca-sync/.checksum
|
||||||
|
register: step_ca_previous_checksum
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
run_once: true
|
||||||
|
become: false
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
# 4️⃣ Décider si une synchronisation est nécessaire
|
||||||
|
- name: Decide if sync is required
|
||||||
|
set_fact:
|
||||||
|
step_ca_sync_required: >-
|
||||||
|
{{
|
||||||
|
step_ca_previous_checksum.content | default('') | b64decode
|
||||||
|
!= step_ca_primary_checksum.stdout
|
||||||
|
}}
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Ensure temporary sync directory exists on controller
|
||||||
|
file:
|
||||||
|
path: /tmp/step-ca-sync
|
||||||
|
state: directory
|
||||||
|
mode: "0700"
|
||||||
|
delegate_to: localhost
|
||||||
|
become: false
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
# 5️⃣ Pull depuis le primaire vers le contrôleur
|
||||||
|
- name: Fetch CA data from primary to controller
|
||||||
|
synchronize:
|
||||||
|
rsync_path: "sudo -u {{ step_ca_user }} rsync"
|
||||||
|
src: "{{ step_ca_dir }}/"
|
||||||
|
dest: "/tmp/step-ca-sync/"
|
||||||
|
mode: pull
|
||||||
|
recursive: yes
|
||||||
|
delete: no
|
||||||
|
delegate_to: localhost
|
||||||
|
become: false
|
||||||
|
when: step_ca_sync_required
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
# 6️⃣ Sauvegarder le nouveau checksum (controller)
|
||||||
|
- name: Save new checksum on controller
|
||||||
|
copy:
|
||||||
|
dest: /tmp/step-ca-sync/.checksum
|
||||||
|
content: "{{ step_ca_primary_checksum.stdout }}"
|
||||||
|
mode: "0600"
|
||||||
|
when: step_ca_sync_required
|
||||||
|
run_once: true
|
||||||
|
become: false
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
# 7️⃣ Push vers les standby
|
||||||
|
- name: Push CA data to standby nodes
|
||||||
|
synchronize:
|
||||||
|
rsync_path: "sudo -u {{ step_ca_user }} rsync"
|
||||||
|
src: "/tmp/step-ca-sync/"
|
||||||
|
dest: "{{ step_ca_dir }}/"
|
||||||
|
mode: push
|
||||||
|
recursive: yes
|
||||||
|
delete: no
|
||||||
|
when:
|
||||||
|
- inventory_hostname != step_ca_primary
|
||||||
|
- step_ca_sync_required
|
||||||
|
|
||||||
|
- name: Wipe temporary CA sync directory on controller
|
||||||
|
file:
|
||||||
|
path: /tmp/step-ca-sync
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
run_once: true
|
||||||
|
become: false
|
||||||
|
when: step_ca_sync_required
|
||||||
|
|
||||||
|
# 8️⃣ Forcer permissions correctes (sécurité)
|
||||||
|
- name: Fix step directory permissions
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_dir }}"
|
||||||
|
owner: "{{ step_ca_user }}"
|
||||||
|
group: "{{ step_ca_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
recurse: yes
|
||||||
|
notify: restart step-ca
|
||||||
|
|
||||||
|
# 9️⃣ Retirer le lock sur le primaire
|
||||||
|
- name: Remove sync lock on primary
|
||||||
|
file:
|
||||||
|
path: "{{ step_ca_dir }}/.sync.lock"
|
||||||
|
state: absent
|
||||||
|
delegate_to: "{{ step_ca_primary }}"
|
||||||
|
run_once: true
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
- name: Install step-ca systemd service
|
||||||
|
template:
|
||||||
|
src: step-ca.service.j2
|
||||||
|
dest: /etc/systemd/system/step-ca.service
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Reload systemd
|
||||||
|
systemd:
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
|
- name: Enable step-ca on primary
|
||||||
|
systemd:
|
||||||
|
name: step-ca
|
||||||
|
enabled: yes
|
||||||
|
state: started
|
||||||
|
when: inventory_hostname == step_ca_primary
|
||||||
|
|
||||||
|
- name: Disable step-ca on standby nodes
|
||||||
|
systemd:
|
||||||
|
name: step-ca
|
||||||
|
enabled: no
|
||||||
|
state: stopped
|
||||||
|
when: inventory_hostname != step_ca_primary
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Smallstep CA
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User={{ step_ca_user }}
|
||||||
|
Group={{ step_ca_user }}
|
||||||
|
ExecStart=/usr/bin/step-ca \
|
||||||
|
--password-file {{ step_ca_home }}/.step-pass \
|
||||||
|
{{ step_ca_dir }}/config/ca.json
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
98
ansible/arcodange/factory/playbooks/ssl/ssl.yml
Normal file
98
ansible/arcodange/factory/playbooks/ssl/ssl.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
- name: step-ca
|
||||||
|
ansible.builtin.import_playbook: step-ca.yml
|
||||||
|
|
||||||
|
- name: Fetch Step-CA root certificate
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
step_ca_primary: pi1
|
||||||
|
step_ca_user: step
|
||||||
|
step_ca_root: "/home/step/.step/certs/root_ca.crt"
|
||||||
|
tmp_dir: "/tmp/step-ca-cert-manager"
|
||||||
|
tasks:
|
||||||
|
- name: Ensure local temp directory exists
|
||||||
|
file:
|
||||||
|
path: "{{ tmp_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Fetch root CA from step_ca_primary
|
||||||
|
fetch:
|
||||||
|
src: "{{ step_ca_root }}"
|
||||||
|
dest: "{{ tmp_dir }}/root_ca.crt"
|
||||||
|
flat: true
|
||||||
|
delegate_to: "{{ step_ca_primary }}"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Préparer le répertoire de build
|
||||||
|
file:
|
||||||
|
path: /tmp/gitea-runner-image
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Copier le root CA dans le contexte Docker
|
||||||
|
copy:
|
||||||
|
src: "{{ tmp_dir }}/root_ca.crt"
|
||||||
|
dest: /tmp/gitea-runner-image/root_ca.crt
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Créer le Dockerfile pour l'image runner avec CA custom
|
||||||
|
copy:
|
||||||
|
dest: /tmp/gitea-runner-image/Dockerfile
|
||||||
|
mode: '0644'
|
||||||
|
content: |
|
||||||
|
FROM gitea/runner-images:ubuntu-latest
|
||||||
|
|
||||||
|
COPY root_ca.crt /usr/local/share/ca-certificates/root_ca.crt
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|
||||||
|
- name: Builder l'image runner avec le CA
|
||||||
|
community.docker.docker_image:
|
||||||
|
name: gitea.arcodange.lab/arcodange-org/runner-images
|
||||||
|
tag: ubuntu-latest-ca
|
||||||
|
source: build
|
||||||
|
build:
|
||||||
|
path: /tmp/gitea-runner-image
|
||||||
|
push: true
|
||||||
|
|
||||||
|
# - /etc/ssl/certs:/etc/ssl/certs:ro
|
||||||
|
|
||||||
|
# - name: Distribute Step-CA root certificate
|
||||||
|
# hosts: all
|
||||||
|
# gather_facts: true
|
||||||
|
# become: true
|
||||||
|
# vars:
|
||||||
|
# root_ca_source: "/tmp/step-ca-cert-manager/root_ca.crt"
|
||||||
|
# root_ca_filename: "arcodange-root.crt"
|
||||||
|
|
||||||
|
# tasks:
|
||||||
|
# - name: Ensure root CA file is copied to correct location
|
||||||
|
# copy:
|
||||||
|
# src: "{{ root_ca_source }}"
|
||||||
|
# dest: "{{ ca_dest_path }}"
|
||||||
|
# owner: root
|
||||||
|
# group: root
|
||||||
|
# mode: '0644'
|
||||||
|
# vars:
|
||||||
|
# ca_dest_path: >-
|
||||||
|
# {% if ansible_facts['os_family'] == 'Debian' %}
|
||||||
|
# /usr/local/share/ca-certificates/{{ root_ca_filename }}
|
||||||
|
# {% elif ansible_facts['os_family'] in ['RedHat', 'Fedora'] %}
|
||||||
|
# /etc/pki/ca-trust/source/anchors/{{ root_ca_filename }}
|
||||||
|
# {% else %}
|
||||||
|
# /etc/ssl/certs/{{ root_ca_filename }}
|
||||||
|
# {% endif %}
|
||||||
|
|
||||||
|
# - name: Update CA trust store
|
||||||
|
# command: "{{ ca_update_command }}"
|
||||||
|
# vars:
|
||||||
|
# ca_update_command: >-
|
||||||
|
# {% if ansible_facts['os_family'] == 'Debian' %}
|
||||||
|
# update-ca-certificates
|
||||||
|
# {% elif ansible_facts['os_family'] in ['RedHat', 'Fedora'] %}
|
||||||
|
# update-ca-trust
|
||||||
|
# {% else %}
|
||||||
|
# echo 'Please update the CA trust manually'
|
||||||
|
# {% endif %}
|
||||||
6
ansible/arcodange/factory/playbooks/ssl/step-ca.yml
Normal file
6
ansible/arcodange/factory/playbooks/ssl/step-ca.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
- name: Setup step-ca on raspberries
|
||||||
|
hosts: step_ca #raspberries:&local
|
||||||
|
become: yes
|
||||||
|
roles:
|
||||||
|
- step_ca
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
- name: Install iSCSI client for Longhorn on Raspberry Pi
|
||||||
|
hosts: raspberries:&local
|
||||||
|
become: yes
|
||||||
|
tasks:
|
||||||
|
- name: Install open-iscsi
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: open-iscsi
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Enable and start iSCSI service
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: iscsid
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
|
||||||
|
- name: Installer cryptsetup
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: cryptsetup
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: Charger le module noyau dm_crypt
|
||||||
|
ansible.builtin.modprobe:
|
||||||
|
name: dm_crypt
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: S'assurer que le module dm_crypt est chargé au démarrage
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/modules
|
||||||
|
line: dm_crypt
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Créer dossier longhorn
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /mnt/arcodange/longhorn
|
||||||
|
state: directory
|
||||||
|
owner: pi
|
||||||
|
group: docker
|
||||||
|
mode: '0774'
|
||||||
|
ignore_errors: true
|
||||||
315
ansible/arcodange/factory/playbooks/system/k3s_config.yml
Normal file
315
ansible/arcodange/factory/playbooks/system/k3s_config.yml
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
- name: System K3S
|
||||||
|
hosts: raspberries:&local
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: prepare inventory for k3s external playbook
|
||||||
|
tags: always
|
||||||
|
ansible.builtin.add_host:
|
||||||
|
hostname: "{{ item }}"
|
||||||
|
groups:
|
||||||
|
- k3s_cluster
|
||||||
|
- "{{ ansible_loop.first | ternary('server', 'agent') }}"
|
||||||
|
loop: "{{ groups.raspberries | intersect(groups.local) | sort }}"
|
||||||
|
loop_control:
|
||||||
|
extended: true
|
||||||
|
extended_allitems: false
|
||||||
|
|
||||||
|
- name: how to reach k3s
|
||||||
|
hosts: server
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: setup longhorn for volumes https://docs.k3s.io/helm
|
||||||
|
become: true
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /var/lib/rancher/k3s/server/manifests/longhorn-install.yaml
|
||||||
|
content: |-
|
||||||
|
apiVersion: helm.cattle.io/v1
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
helmcharts.cattle.io/managed-by: helm-controller
|
||||||
|
finalizers:
|
||||||
|
- wrangler.cattle.io/on-helm-chart-remove
|
||||||
|
generation: 1
|
||||||
|
name: longhorn-install
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
version: v1.9.1
|
||||||
|
chart: longhorn
|
||||||
|
repo: https://charts.longhorn.io
|
||||||
|
failurePolicy: abort
|
||||||
|
targetNamespace: longhorn-system
|
||||||
|
createNamespace: true
|
||||||
|
valuesContent: |-
|
||||||
|
defaultSettings:
|
||||||
|
defaultDataPath: /mnt/arcodange/longhorn
|
||||||
|
vars:
|
||||||
|
longhorn_helm_values: {} # https://github.com/longhorn/longhorn/blob/master/chart/values.yaml
|
||||||
|
|
||||||
|
- name: customize k3s traefik configuration https://docs.k3s.io/helm
|
||||||
|
block:
|
||||||
|
- name: Get my public IP
|
||||||
|
community.general.ipify_facts:
|
||||||
|
- become: true
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /var/lib/rancher/k3s/server/manifests/traefik-v3.yaml
|
||||||
|
content: |-
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
dynamic.yaml: |-
|
||||||
|
{{ traefik_config_yaml | to_nice_yaml | indent( width=4 ) }}
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: traefik-configmap
|
||||||
|
namespace: kube-system
|
||||||
|
---
|
||||||
|
apiVersion: helm.cattle.io/v1
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
repo: https://traefik.github.io/charts
|
||||||
|
chart: traefik
|
||||||
|
version: v37.4.0
|
||||||
|
targetNamespace: kube-system
|
||||||
|
valuesContent: |-
|
||||||
|
{{ traefik_helm_values | to_nice_yaml | indent( width=4 ) }}
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: wildcard-arcodange-lab
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
secretName: wildcard-arcodange-lab
|
||||||
|
issuerRef:
|
||||||
|
name: step-issuer
|
||||||
|
kind: StepClusterIssuer
|
||||||
|
group: certmanager.step.sm
|
||||||
|
dnsNames:
|
||||||
|
- arcodange.lab
|
||||||
|
- "*.arcodange.lab"
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: TLSStore
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
defaultCertificate:
|
||||||
|
secretName: wildcard-arcodange-lab
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: gitea-external
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
type: ExternalName
|
||||||
|
externalName: {{ hostvars[groups.gitea[0]]['preferred_ip'] }}
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
targetPort: 3000
|
||||||
|
vars:
|
||||||
|
traefik_config_yaml:
|
||||||
|
http:
|
||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://{{ hostvars[groups.gitea[0]]['preferred_ip'] }}:3000"
|
||||||
|
routers:
|
||||||
|
dashboard:
|
||||||
|
# rule: Host(`traefik.arcodange.duckdns.org`)
|
||||||
|
rule: Host(`traefik.arcodange.lab`)
|
||||||
|
service: api@internal
|
||||||
|
middlewares:
|
||||||
|
- localIp
|
||||||
|
# tls:
|
||||||
|
# certResolver: letsencrypt
|
||||||
|
# domains:
|
||||||
|
# - main: "arcodange.duckdns.org"
|
||||||
|
# sans:
|
||||||
|
# - "traefik.arcodange.duckdns.org"
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
- web
|
||||||
|
acme-challenge:
|
||||||
|
rule: Host(`arcodange.duckdns.org`) && PathPrefix(`/.well-known/acme-challenge`)
|
||||||
|
service: acme-http@internal
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
domains:
|
||||||
|
- main: "arcodange.duckdns.org"
|
||||||
|
sans:
|
||||||
|
- "*.arcodange.duckdns.org"
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
- web
|
||||||
|
gitea:
|
||||||
|
# rule: Host(`gitea.arcodange.duckdns.org`)
|
||||||
|
rule: Host(`gitea.arcodange.lab`)
|
||||||
|
service: gitea
|
||||||
|
middlewares:
|
||||||
|
- localIp
|
||||||
|
# tls:
|
||||||
|
# certResolver: letsencrypt
|
||||||
|
# domains:
|
||||||
|
# - main: "arcodange.duckdns.org"
|
||||||
|
# sans:
|
||||||
|
# - "gitea.arcodange.duckdns.org"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
localIp:
|
||||||
|
ipAllowList:
|
||||||
|
sourceRange:
|
||||||
|
- "172.16.0.0/12"
|
||||||
|
- "10.42.0.0/16"
|
||||||
|
- "192.168.1.0/24"
|
||||||
|
- "{{ ipify_public_ip }}/32"
|
||||||
|
# - "0.0.0.0/0"
|
||||||
|
# ipStrategy:
|
||||||
|
# depth: 1
|
||||||
|
traefik_helm_values:
|
||||||
|
deployment:
|
||||||
|
kind: "Deployment"
|
||||||
|
initContainers:
|
||||||
|
- name: volume-permissions
|
||||||
|
image: busybox:latest
|
||||||
|
command: ["sh", "-c", "touch /data/acme.json; chmod -v 600 /data/acme.json"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
# default is https://github.com/traefik/traefik-helm-chart/blob/v25.0.0/traefik/values.yaml <- for v25 (`kubectl describe deployments.apps traefik -n kube-system | grep helm.sh/chart`)
|
||||||
|
# current is https://github.com/traefik/traefik-helm-chart/blob/v37.4.0/traefik/values.yaml
|
||||||
|
nodeSelector:
|
||||||
|
node-role.kubernetes.io/control-plane: 'true' # make predictible choice of node to direct https traffic to this node and avoid NAT/loss of client IP
|
||||||
|
service:
|
||||||
|
spec:
|
||||||
|
externalTrafficPolicy: Local
|
||||||
|
ports:
|
||||||
|
traefik:
|
||||||
|
expose:
|
||||||
|
default: true
|
||||||
|
web:
|
||||||
|
forwardedHeaders:
|
||||||
|
trustedIPs: ["10.42.0.0/16"] #default k3s cidr
|
||||||
|
ingressRoute:
|
||||||
|
dashboard:
|
||||||
|
enabled: true
|
||||||
|
globalArguments: [] # deactivate --global.sendanonymoususage
|
||||||
|
env:
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: LEGO_DISABLE_CNAME_SUPPORT
|
||||||
|
value: 'true'
|
||||||
|
logs:
|
||||||
|
general:
|
||||||
|
level: INFO
|
||||||
|
# format: json
|
||||||
|
access:
|
||||||
|
enabled: true
|
||||||
|
timezone: Europe/Paris
|
||||||
|
# format: json
|
||||||
|
podSecurityContext:
|
||||||
|
runAsGroup: 65532
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 65532
|
||||||
|
fsGroup: 65532 # else the persistent volume might be owned by root and be unwriteable
|
||||||
|
persistence:
|
||||||
|
# -- Enable persistence using Persistent Volume Claims
|
||||||
|
# ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
|
||||||
|
# It can be used to store TLS certificates, see `storage` in certResolvers
|
||||||
|
enabled: true
|
||||||
|
name: data
|
||||||
|
# existingClaim: ""
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 128Mi
|
||||||
|
storageClass: "longhorn"
|
||||||
|
# volumeName: ""
|
||||||
|
path: /data
|
||||||
|
annotations: {}
|
||||||
|
volumes:
|
||||||
|
- name: traefik-configmap
|
||||||
|
mountPath: /config
|
||||||
|
type: configMap
|
||||||
|
experimental:
|
||||||
|
plugins:
|
||||||
|
crowdsec-bouncer:
|
||||||
|
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin #https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin
|
||||||
|
version: v1.3.3
|
||||||
|
additionalArguments:
|
||||||
|
- '--providers.file.filename=/config/dynamic.yaml'
|
||||||
|
- '--providers.kubernetesingress.ingressendpoint.publishedservice=kube-system/traefik'
|
||||||
|
- "--providers.kubernetescrd.allowcrossnamespace=true"
|
||||||
|
- "--providers.kubernetescrd.allowExternalNameServices=true"
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
# for challenge options cf. https://doc.traefik.io/traefik/https/acme/
|
||||||
|
email: arcodange@gmail.com
|
||||||
|
tlsChallenge: true
|
||||||
|
dnsChallenge:
|
||||||
|
# requires env variable DUCKDNS_TOKEN
|
||||||
|
provider: duckdns
|
||||||
|
propagation:
|
||||||
|
delayBeforeChecks: 120
|
||||||
|
disableChecks: true
|
||||||
|
resolvers:
|
||||||
|
- "1.1.1.1:53"
|
||||||
|
- "8.8.8.8:53"
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: "web"
|
||||||
|
# It has to match the path with a persistent volume
|
||||||
|
storage: /data/acme.json
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: traefik-duckdns-token
|
||||||
|
# MY_TOKEN=<my token (see https://www.duckdns.org/domains)>
|
||||||
|
# kubectl create secret generic traefik-duckdns-token --from-literal="DUCKDNS_TOKEN=$MY_TOKEN" -n kube-system
|
||||||
|
- name: touch manifests/traefik.yaml to trigger update
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /var/lib/rancher/k3s/server/manifests/traefik-v3.yaml
|
||||||
|
state: touch
|
||||||
|
become: true
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
- name: redeploy traefik
|
||||||
|
hosts: localhost
|
||||||
|
tasks:
|
||||||
|
- name: delete old traefik deployment
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
api_version: v1
|
||||||
|
name: traefik
|
||||||
|
kind: Deployment
|
||||||
|
namespace: kube-system
|
||||||
|
state: "absent"
|
||||||
|
- name: delete old deployment job so the k3s helm controller redeploy with our new configuration
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
api_version: batch/v1
|
||||||
|
name: helm-install-traefik
|
||||||
|
kind: Job
|
||||||
|
namespace: kube-system
|
||||||
|
state: "absent"
|
||||||
|
- name: get traefik deployment
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: v1
|
||||||
|
name: traefik
|
||||||
|
kind: Deployment
|
||||||
|
namespace: kube-system
|
||||||
|
wait: true
|
||||||
|
register: traefik_deployment
|
||||||
|
- ansible.builtin.debug:
|
||||||
|
var: traefik_deployment
|
||||||
27
ansible/arcodange/factory/playbooks/system/k3s_dns.yml
Normal file
27
ansible/arcodange/factory/playbooks/system/k3s_dns.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# https://docs.k3s.io/advanced#coredns-custom-configuration-imports
|
||||||
|
---
|
||||||
|
- name: "Déclarer le ConfigMap coredns-custom pour arcodange.lab"
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
vars:
|
||||||
|
pihole_ip: "192.168.1.201"
|
||||||
|
coredns_namespace: "kube-system"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: "Créer / mettre à jour le ConfigMap coredns-custom"
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: coredns-custom
|
||||||
|
namespace: "{{ coredns_namespace }}"
|
||||||
|
data:
|
||||||
|
arcodange-lab.server: |
|
||||||
|
arcodange.lab:53 {
|
||||||
|
errors
|
||||||
|
cache 30
|
||||||
|
forward . {{ pihole_ip }}:53
|
||||||
|
}
|
||||||
172
ansible/arcodange/factory/playbooks/system/k3s_ssl.yml
Normal file
172
ansible/arcodange/factory/playbooks/system/k3s_ssl.yml
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
- name: System K3S
|
||||||
|
hosts: raspberries:&local
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: prepare inventory for k3s external playbook
|
||||||
|
tags: always
|
||||||
|
ansible.builtin.add_host:
|
||||||
|
hostname: "{{ item }}"
|
||||||
|
groups:
|
||||||
|
- k3s_cluster
|
||||||
|
- "{{ ansible_loop.first | ternary('server', 'agent') }}"
|
||||||
|
loop: "{{ groups.raspberries | intersect(groups.local) | sort }}"
|
||||||
|
loop_control:
|
||||||
|
extended: true
|
||||||
|
extended_allitems: false
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Play 1 — Read step-ca PKI
|
||||||
|
# =========================
|
||||||
|
- name: Collect PKI material from step-ca
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
vars:
|
||||||
|
step_ca_primary: pi1
|
||||||
|
step_ca_user: step
|
||||||
|
step_ca_root: "/home/step/.step/certs/root_ca.crt"
|
||||||
|
tmp_dir: /tmp/step-ca-cert-manager
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Ensure local temp directory exists
|
||||||
|
file:
|
||||||
|
path: "{{ tmp_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Fetch root CA
|
||||||
|
fetch:
|
||||||
|
src: "{{ step_ca_root }}"
|
||||||
|
dest: "{{ tmp_dir }}/root_ca.crt"
|
||||||
|
flat: true
|
||||||
|
delegate_to: "{{ step_ca_primary }}"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Read and decode PKI material
|
||||||
|
slurp:
|
||||||
|
src: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- "{{ tmp_dir }}/root_ca.crt"
|
||||||
|
register: pki_raw
|
||||||
|
|
||||||
|
- name: Set PKI facts
|
||||||
|
set_fact:
|
||||||
|
root_ca_b64: "{{ (pki_raw.results | selectattr('item','equalto', tmp_dir + '/root_ca.crt') | first).content }}"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Play 2 — Deploy to k3s
|
||||||
|
# =========================
|
||||||
|
- name: Deploy cert-manager and step-ca integration on k3s server
|
||||||
|
hosts: server
|
||||||
|
gather_facts: false
|
||||||
|
become: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
namespace: cert-manager
|
||||||
|
jwk_provisioner_name: cert-manager
|
||||||
|
jwk_secret_name: step-jwk-password
|
||||||
|
clusterissuer_name: step-ca
|
||||||
|
step_ca_url: "https://ssl-ca.arcodange.lab:8443"
|
||||||
|
cert_manager_version: v1.19.2
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: Get cert-manager provisioner info from step-ca
|
||||||
|
command: >
|
||||||
|
step ca provisioner list
|
||||||
|
register: provisioners_json
|
||||||
|
delegate_to: "{{ step_ca_primary }}"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ step_ca_user }}"
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Set fact jwk_kid from provisioner
|
||||||
|
set_fact:
|
||||||
|
jwk_kid: >-
|
||||||
|
{{
|
||||||
|
(provisioners_json.stdout | from_json
|
||||||
|
| selectattr('name', 'equalto', jwk_provisioner_name) | list
|
||||||
|
| first).key.kid
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Compute PKI checksum
|
||||||
|
set_fact:
|
||||||
|
pki_checksum: >-
|
||||||
|
{{
|
||||||
|
(hostvars['localhost'].root_ca_b64
|
||||||
|
~ jwk_kid
|
||||||
|
~ step_ca_url
|
||||||
|
~ cert_manager_version) | hash('sha256')
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Install cert-manager and step-ca via k3s static manifest
|
||||||
|
copy:
|
||||||
|
dest: /var/lib/rancher/k3s/server/manifests/cert-manager-step-ca.yaml
|
||||||
|
mode: "0600"
|
||||||
|
content: |-
|
||||||
|
apiVersion: helm.cattle.io/v1
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: cert-manager
|
||||||
|
namespace: kube-system
|
||||||
|
annotations:
|
||||||
|
pki.arcodange.lab/checksum: "{{ pki_checksum }}"
|
||||||
|
spec:
|
||||||
|
chart: cert-manager
|
||||||
|
repo: https://charts.jetstack.io
|
||||||
|
version: {{ cert_manager_version }}
|
||||||
|
targetNamespace: cert-manager
|
||||||
|
createNamespace: true
|
||||||
|
valuesContent: |-
|
||||||
|
installCRDs: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ jwk_secret_name }}
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
annotations:
|
||||||
|
pki.arcodange.lab/checksum: "{{ pki_checksum }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
password: >-
|
||||||
|
{{ hostvars[step_ca_primary].vault_step_ca_jwk_password }}
|
||||||
|
---
|
||||||
|
apiVersion: helm.cattle.io/v1
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: step-issuer
|
||||||
|
namespace: kube-system
|
||||||
|
annotations:
|
||||||
|
pki.arcodange.lab/checksum: "{{ pki_checksum }}"
|
||||||
|
spec:
|
||||||
|
chart: step-issuer
|
||||||
|
repo: https://smallstep.github.io/helm-charts
|
||||||
|
version: 1.9.11
|
||||||
|
targetNamespace: {{ namespace }}
|
||||||
|
createNamespace: false
|
||||||
|
valuesContent: |-
|
||||||
|
certManager:
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
stepClusterIssuer:
|
||||||
|
create: true
|
||||||
|
caUrl: "{{ step_ca_url }}"
|
||||||
|
caBundle: "{{ hostvars['localhost'].root_ca_b64 }}"
|
||||||
|
provisioner:
|
||||||
|
name: {{ jwk_provisioner_name }}
|
||||||
|
kid: "{{ jwk_kid }}"
|
||||||
|
passwordRef:
|
||||||
|
name: {{ jwk_secret_name }}
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
key: password
|
||||||
|
# Override kube-rbac-proxy image to use ARM64-compatible version.
|
||||||
|
# Note: pi3 (ARM64) requires an ARM64-compatible image, while pi2 (ARMv7) may work with AMD64 images.
|
||||||
|
# The default image (gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0) is AMD64-only and fails on pi3.
|
||||||
|
kubeRBACproxy:
|
||||||
|
image:
|
||||||
|
repository: quay.io/brancz/kube-rbac-proxy
|
||||||
|
tag: v0.15.0
|
||||||
|
|
||||||
161
ansible/arcodange/factory/playbooks/system/pki.md
Normal file
161
ansible/arcodange/factory/playbooks/system/pki.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# PKI
|
||||||
|
|
||||||
|
Explications générées par chatgpt pour expliquer le setup de ssl via "step"
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
logLevel: debug
|
||||||
|
theme: forest
|
||||||
|
---
|
||||||
|
flowchart TB
|
||||||
|
%% PKI
|
||||||
|
subgraph PKI["Step CA / PKI (Pi1)"]
|
||||||
|
style PKI fill:#ffe0b2,stroke:#ff8c00,stroke-width:2px
|
||||||
|
A[Step CA primaire]:::stepCA
|
||||||
|
B[JWK Provisioner]:::jwk
|
||||||
|
C[Root CA]:::root
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Contrôleur Ansible
|
||||||
|
subgraph Controller["Contrôleur Ansible / Mac"]
|
||||||
|
style Controller fill:#e0f7fa,stroke:#00acc1,stroke-width:2px
|
||||||
|
D[Fetch JWK + Root CA]:::ansible
|
||||||
|
E[Secrets K8s: step-jwk, step-root-ca]:::k8sSecret
|
||||||
|
F[ClusterIssuer cert-manager]:::clusterIssuer
|
||||||
|
end
|
||||||
|
|
||||||
|
%% K3s Cluster + Traefik
|
||||||
|
subgraph K3sCluster["K3s Cluster"]
|
||||||
|
style K3sCluster fill:#f1f8e9,stroke:#558b2f,stroke-width:2px
|
||||||
|
T[Traefik Ingress Controller]:::traefik
|
||||||
|
H[Webapp Pods]:::webapp
|
||||||
|
G["Gitea Service (ExternalName → pi2.home:3000)"]:::gitea
|
||||||
|
end
|
||||||
|
|
||||||
|
Users[Clients / Navigateurs]:::clients
|
||||||
|
|
||||||
|
%% Flèches
|
||||||
|
%% PKI → Controller
|
||||||
|
A --> B
|
||||||
|
C --> D
|
||||||
|
B --> D
|
||||||
|
D --> E
|
||||||
|
E --> F
|
||||||
|
|
||||||
|
%% ClusterIssuer → Traefik services
|
||||||
|
F --> H
|
||||||
|
F --> G
|
||||||
|
|
||||||
|
%% Traefik expose tous les services
|
||||||
|
T --> H
|
||||||
|
T --> G
|
||||||
|
Users -->|HTTPS / HTTP| T
|
||||||
|
|
||||||
|
%% PKI direct (optional, for clarity)
|
||||||
|
A -->|Sign initial cert| F
|
||||||
|
|
||||||
|
%% Styling classes
|
||||||
|
classDef stepCA fill:#fff3e0,stroke:#ff6f00,stroke-width:1px
|
||||||
|
classDef jwk fill:#fff9c4,stroke:#fbc02d,stroke-width:1px
|
||||||
|
classDef root fill:#ffe0b2,stroke:#ff8c00,stroke-width:1px
|
||||||
|
classDef ansible fill:#b2ebf2,stroke:#00acc1,stroke-width:1px
|
||||||
|
classDef k8sSecret fill:#b3e5fc,stroke:#0288d1,stroke-width:1px
|
||||||
|
classDef clusterIssuer fill:#81d4fa,stroke:#0277bd,stroke-width:1px
|
||||||
|
classDef gitea fill:#c8e6c9,stroke:#388e3c,stroke-width:1px
|
||||||
|
classDef webapp fill:#a5d6a7,stroke:#2e7d32,stroke-width:1px
|
||||||
|
classDef traefik fill:#ffe082,stroke:#ff8f00,stroke-width:1px
|
||||||
|
classDef clients fill:#eeeeee,stroke:#9e9e9e,stroke-width:1px
|
||||||
|
```
|
||||||
|
|
||||||
|
- 🔵 PKI (Step CA) : la source de confiance. Toutes les certificats HTTPS proviennent de là.
|
||||||
|
- 🔵 JWK Provisioner : autorise cert-manager à demander des certificats automatiquement.
|
||||||
|
- 🟢 Contrôleur Ansible : centralise les clés, crée les Secrets K8s et ClusterIssuer.
|
||||||
|
- 🟢 Secrets & ClusterIssuer : permettent à cert-manager dans K3s de s’authentifier et obtenir des certificats TLS.
|
||||||
|
- 🟢 Webapp Pods : obtiennent leurs certificats via cert-manager et HTTPS fonctionne automatiquement.
|
||||||
|
- 🔵 Gitea : reçoit directement un certificat signé par Step CA, sert HTTPS hors K3s.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
%% PKI
|
||||||
|
subgraph PKI["Step CA / PKI (Pi1)"]
|
||||||
|
style PKI fill:#ffe0b2,stroke:#ff8c00,stroke-width:2px
|
||||||
|
A[1️⃣ Initialisation Step CA primaire]:::stepCA
|
||||||
|
B[2️⃣ Création JWK Provisioner pour K3s]:::jwk
|
||||||
|
C[Root CA]:::root
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Contrôleur Ansible
|
||||||
|
subgraph Controller["Contrôleur Ansible / Mac"]
|
||||||
|
style Controller fill:#e0f7fa,stroke:#00acc1,stroke-width:2px
|
||||||
|
D[3️⃣ Fetch JWK + Root CA depuis Step CA]:::ansible
|
||||||
|
E[4️⃣ Création / Mise à jour des Secrets K8s]:::k8sSecret
|
||||||
|
F[5️⃣ Création / Mise à jour ClusterIssuer cert-manager]:::clusterIssuer
|
||||||
|
end
|
||||||
|
|
||||||
|
%% K3s Cluster + Traefik
|
||||||
|
subgraph K3sCluster["K3s Cluster"]
|
||||||
|
style K3sCluster fill:#f1f8e9,stroke:#558b2f,stroke-width:2px
|
||||||
|
T[6️⃣ Traefik Ingress Controller]:::traefik
|
||||||
|
H[7️⃣ Webapp Pods]:::webapp
|
||||||
|
G["8️⃣ Gitea Service (ExternalName → pi2.home:3000)"]:::gitea
|
||||||
|
end
|
||||||
|
|
||||||
|
Users[9️⃣ Client Mac / Navigateurs]:::clients
|
||||||
|
|
||||||
|
%% Flux
|
||||||
|
A --> B
|
||||||
|
C --> D
|
||||||
|
B --> D
|
||||||
|
D --> E
|
||||||
|
E --> F
|
||||||
|
F --> H
|
||||||
|
F --> G
|
||||||
|
T --> H
|
||||||
|
T --> G
|
||||||
|
Users -->|HTTPS / HTTP| T
|
||||||
|
|
||||||
|
%% Styling classes
|
||||||
|
classDef stepCA fill:#fff3e0,stroke:#ff6f00,stroke-width:1px
|
||||||
|
classDef jwk fill:#fff9c4,stroke:#fbc02d,stroke-width:1px
|
||||||
|
classDef root fill:#ffe0b2,stroke:#ff8c00,stroke-width:1px
|
||||||
|
classDef ansible fill:#b2ebf2,stroke:#00acc1,stroke-width:1px
|
||||||
|
classDef k8sSecret fill:#b3e5fc,stroke:#0288d1,stroke-width:1px
|
||||||
|
classDef clusterIssuer fill:#81d4fa,stroke:#0277bd,stroke-width:1px
|
||||||
|
classDef gitea fill:#c8e6c9,stroke:#388e3c,stroke-width:1px
|
||||||
|
classDef webapp fill:#a5d6a7,stroke:#2e7d32,stroke-width:1px
|
||||||
|
classDef traefik fill:#ffe082,stroke:#ff8f00,stroke-width:1px
|
||||||
|
classDef clients fill:#eeeeee,stroke:#9e9e9e,stroke-width:1px
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph Cluster["Cluster Kubernetes (k3s)"]
|
||||||
|
subgraph CertManager["Cert-Manager"]
|
||||||
|
ClusterIssuer["ClusterIssuer\n(type: smallstep)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Traefik["Traefik (Ingress Controller)"]
|
||||||
|
TLSStore["TLSStore\n(Traefik v2+)"]
|
||||||
|
IngressRoute["IngressRoute\n(TLS: my-tls-store)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Apps["Applications"]
|
||||||
|
App1[Service: my-app]
|
||||||
|
App2[Service: my-api]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Smallstep["Smallstep PKI (step-ca)"]
|
||||||
|
StepCA["step-ca\n(CA interne)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Interactions
|
||||||
|
ClusterIssuer -- "1. Demande de certificat\n(CertificateRequest)" --> StepCA
|
||||||
|
StepCA -- "2. Émet un certificat\n(signé par la CA)" --> ClusterIssuer
|
||||||
|
ClusterIssuer -- "3. Stocke le certificat\n(dans un Secret Kubernetes)" --> Secret[(Secret: my-app-tls)]
|
||||||
|
Secret -- "4. Référencé par" --> TLSStore
|
||||||
|
TLSStore -- "5. Fournit le certificat\n(TLS Termination)" --> IngressRoute
|
||||||
|
IngressRoute -- "6. Route le trafic HTTPS\nvers" --> App1
|
||||||
|
IngressRoute -- "6. Route le trafic HTTPS\nvers" --> App2
|
||||||
|
```
|
||||||
123
ansible/arcodange/factory/playbooks/system/prepare_disks.yml
Normal file
123
ansible/arcodange/factory/playbooks/system/prepare_disks.yml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
---
|
||||||
|
- name: Préparer automatiquement le disque externe en ext4 avec label
|
||||||
|
hosts: raspberries:&local
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
mount_point: /mnt/arcodange
|
||||||
|
disk_label: arcodange_500
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: Lister toutes les partitions avec labels
|
||||||
|
command: "lsblk -o NAME,LABEL,SIZE,TYPE,MOUNTPOINT -J -b"
|
||||||
|
register: lsblk_info
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Extraire toutes les partitions
|
||||||
|
set_fact:
|
||||||
|
all_partitions: >-
|
||||||
|
{{
|
||||||
|
(lsblk_info.stdout | from_json).blockdevices
|
||||||
|
| map(attribute='children', default=[]) | flatten
|
||||||
|
| selectattr('type', 'equalto', 'part')
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Rechercher si le label existe déjà
|
||||||
|
set_fact:
|
||||||
|
labeled_partition: >-
|
||||||
|
{{
|
||||||
|
all_partitions
|
||||||
|
| selectattr('label', 'equalto', disk_label)
|
||||||
|
| list | first | default(None)
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Choisir une partition candidate (hors disque système mmcblk0)
|
||||||
|
set_fact:
|
||||||
|
target_partition: "{{ (all_partitions | rejectattr('name', 'search', '^mmcblk0')) | sort(attribute='size') | last }}"
|
||||||
|
when: labeled_partition == None
|
||||||
|
|
||||||
|
- name: Définir target_device selon label existant ou partition candidate
|
||||||
|
set_fact:
|
||||||
|
target_device: >-
|
||||||
|
{{
|
||||||
|
'/dev/' + (
|
||||||
|
(labeled_partition.name | default(''))
|
||||||
|
if labeled_partition != None
|
||||||
|
else target_partition.name
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Vérifier si la partition est déjà montée au bon point
|
||||||
|
set_fact:
|
||||||
|
partition_mounted_correctly: >-
|
||||||
|
{{
|
||||||
|
(labeled_partition != None and labeled_partition.mountpoint == mount_point)
|
||||||
|
or (target_partition != None and target_partition.mountpoint == mount_point)
|
||||||
|
}}
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: partition_mounted_correctly
|
||||||
|
|
||||||
|
- name: Demander confirmation avant formatage si label inexistant
|
||||||
|
run_once: true
|
||||||
|
when:
|
||||||
|
- labeled_partition == None
|
||||||
|
- not partition_mounted_correctly
|
||||||
|
pause:
|
||||||
|
prompt: |
|
||||||
|
ATTENTION : la partition {{ target_device }} sera FORMATÉE en ext4
|
||||||
|
et recevra le label {{ disk_label }}.
|
||||||
|
Tapez 'oui' pour continuer :
|
||||||
|
register: user_confirm
|
||||||
|
|
||||||
|
- name: Annuler si l'utilisateur n'a pas confirmé
|
||||||
|
fail:
|
||||||
|
msg: "Formatage annulé."
|
||||||
|
when:
|
||||||
|
- labeled_partition == None
|
||||||
|
- not partition_mounted_correctly
|
||||||
|
- user_confirm.user_input | lower != 'oui'
|
||||||
|
|
||||||
|
- name: Démonter la partition si montée ailleurs
|
||||||
|
mount:
|
||||||
|
path: "{{ (labeled_partition.mountpoint if labeled_partition != None else target_partition.mountpoint) }}"
|
||||||
|
state: unmounted
|
||||||
|
when:
|
||||||
|
- not partition_mounted_correctly
|
||||||
|
- (labeled_partition != None and labeled_partition.mountpoint not in [mount_point, '', None])
|
||||||
|
or (target_partition != None and target_partition.mountpoint not in [mount_point, '', None])
|
||||||
|
|
||||||
|
- name: Formater avec label si nécessaire
|
||||||
|
filesystem:
|
||||||
|
fstype: ext4
|
||||||
|
dev: "{{ target_device }}"
|
||||||
|
force: true
|
||||||
|
opts: "-L {{ disk_label }}"
|
||||||
|
when:
|
||||||
|
- not partition_mounted_correctly
|
||||||
|
- labeled_partition == None
|
||||||
|
|
||||||
|
- name: Créer point de montage si absent
|
||||||
|
file:
|
||||||
|
path: "{{ mount_point }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Monter le disque par label (idempotent)
|
||||||
|
mount:
|
||||||
|
path: "{{ mount_point }}"
|
||||||
|
src: "LABEL={{ disk_label }}"
|
||||||
|
fstype: ext4
|
||||||
|
opts: defaults,nofail
|
||||||
|
state: mounted
|
||||||
|
|
||||||
|
- name: Assurer persistance dans fstab
|
||||||
|
mount:
|
||||||
|
path: "{{ mount_point }}"
|
||||||
|
src: "LABEL={{ disk_label }}"
|
||||||
|
fstype: ext4
|
||||||
|
opts: defaults,nofail
|
||||||
|
state: present
|
||||||
13
ansible/arcodange/factory/playbooks/system/rpi.yml
Normal file
13
ansible/arcodange/factory/playbooks/system/rpi.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
- name: Raspberry pi general setup
|
||||||
|
hosts: raspberries:&local
|
||||||
|
gather_facts: yes
|
||||||
|
tags: never
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: set hostname
|
||||||
|
ansible.builtin.hostname:
|
||||||
|
name: "{{ inventory_hostname }}"
|
||||||
|
become: yes
|
||||||
|
when: inventory_hostname != ansible_hostname
|
||||||
31
ansible/arcodange/factory/playbooks/system/system.yml
Normal file
31
ansible/arcodange/factory/playbooks/system/system.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
- name: Setup général des rpis
|
||||||
|
ansible.builtin.import_playbook: rpi.yml
|
||||||
|
|
||||||
|
- name: dns
|
||||||
|
ansible.builtin.import_playbook: ../dns/dns.yml
|
||||||
|
|
||||||
|
- name: ssl
|
||||||
|
ansible.builtin.import_playbook: ../ssl/ssl.yml
|
||||||
|
|
||||||
|
- name: Préparer les disques pour Longhorn
|
||||||
|
ansible.builtin.import_playbook: prepare_disks.yml
|
||||||
|
|
||||||
|
- name: Installer et configurer Docker
|
||||||
|
ansible.builtin.import_playbook: system_docker.yml
|
||||||
|
|
||||||
|
- name: Installer le client iSCSI pour Longhorn
|
||||||
|
ansible.builtin.import_playbook: iscsi_longhorn.yml
|
||||||
|
|
||||||
|
- name: Préparer l'inventaire et installer K3s
|
||||||
|
ansible.builtin.import_playbook: system_k3s.yml
|
||||||
|
|
||||||
|
- name: Configurer K3S Core DNS
|
||||||
|
ansible.builtin.import_playbook: k3s_dns.yml
|
||||||
|
|
||||||
|
- name: Configurer K3S Cert Issuer
|
||||||
|
ansible.builtin.import_playbook: k3s_ssl.yml
|
||||||
|
|
||||||
|
- name: Configurer K3s (kubeconfig, Longhorn, Traefik)
|
||||||
|
ansible.builtin.import_playbook: k3s_config.yml
|
||||||
111
ansible/arcodange/factory/playbooks/system/system_docker.yml
Normal file
111
ansible/arcodange/factory/playbooks/system/system_docker.yml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
- name: System Docker
|
||||||
|
hosts: raspberries:&local
|
||||||
|
gather_facts: yes
|
||||||
|
tags: never
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
|
||||||
|
- name: Prevent apt source conflict
|
||||||
|
ansible.builtin.file:
|
||||||
|
state: absent
|
||||||
|
path: /etc/apt/sources.list.d/docker.list
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: Install role geerlingguy.docker
|
||||||
|
community.general.ansible_galaxy_install:
|
||||||
|
type: role
|
||||||
|
name: geerlingguy.docker
|
||||||
|
run_once: true
|
||||||
|
delegate_to: localhost
|
||||||
|
become: false
|
||||||
|
|
||||||
|
- ansible.builtin.debug:
|
||||||
|
var: ansible_facts.machine
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- include_role:
|
||||||
|
name: geerlingguy.docker
|
||||||
|
|
||||||
|
|
||||||
|
- name: Créer le répertoire /etc/docker s'il n'existe pas
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /etc/docker
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Lire la configuration Docker existante
|
||||||
|
ansible.builtin.command: "cat /etc/docker/daemon.json"
|
||||||
|
register: docker_config_raw
|
||||||
|
ignore_errors: yes
|
||||||
|
changed_when: false
|
||||||
|
when: (ansible.builtin.stat.path='/etc/docker/daemon.json').stat.exists
|
||||||
|
|
||||||
|
- name: Initialiser la variable de config Docker
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
docker_config: {}
|
||||||
|
|
||||||
|
- name: Parser le JSON existant si le fichier existe
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
docker_config: "{{ docker_config_raw.stdout | from_json }}"
|
||||||
|
when: docker_config_raw.stdout is defined and docker_config_raw.stdout != ""
|
||||||
|
|
||||||
|
- name: Mettre à jour la config du logger
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
docker_config: >
|
||||||
|
{{ docker_config | combine({
|
||||||
|
'log-driver': 'json-file',
|
||||||
|
'log-opts': {
|
||||||
|
'max-size': '10m',
|
||||||
|
'max-file': '5'
|
||||||
|
}
|
||||||
|
}, recursive=True) }}
|
||||||
|
|
||||||
|
- name: Ensure Docker storage directory exists on external disk
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /mnt/arcodange/docker
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
owner: root
|
||||||
|
group: docker
|
||||||
|
when: ansible_facts.mounts | selectattr('mount', 'equalto', '/mnt/arcodange') | list | length > 0
|
||||||
|
|
||||||
|
- name: Configure Docker to use external storage
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
docker_config: >
|
||||||
|
{{ docker_config | combine({
|
||||||
|
'data-root': '/mnt/arcodange/docker',
|
||||||
|
'storage-driver': 'overlay2'
|
||||||
|
}, recursive=True) }}
|
||||||
|
when: ansible_facts.mounts | selectattr('mount', 'equalto', '/mnt/arcodange') | list | length > 0
|
||||||
|
|
||||||
|
- name: Ensure docker_config is a dictionary
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
docker_config: >
|
||||||
|
{% if docker_config is mapping %}
|
||||||
|
{{ docker_config }}
|
||||||
|
{% else %}
|
||||||
|
{}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
- name: Écrire la configuration mise à jour
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/docker/daemon.json
|
||||||
|
content: "{{ docker_config | to_nice_json(indent=2) }}"
|
||||||
|
mode: '0644'
|
||||||
|
notify: Redémarrer Docker
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Redémarrer Docker
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: docker
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
post_tasks:
|
||||||
|
- name: adding existing user '{{ ansible_user }}' to group docker
|
||||||
|
user:
|
||||||
|
name: '{{ ansible_user }}'
|
||||||
|
groups: docker
|
||||||
|
append: yes
|
||||||
|
become: yes
|
||||||
63
ansible/arcodange/factory/playbooks/system/system_k3s.yml
Normal file
63
ansible/arcodange/factory/playbooks/system/system_k3s.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
- name: System K3S
|
||||||
|
hosts: raspberries:&local
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: prepare inventory for k3s external playbook
|
||||||
|
tags: always
|
||||||
|
ansible.builtin.add_host:
|
||||||
|
hostname: "{{ item }}"
|
||||||
|
groups:
|
||||||
|
- k3s_cluster
|
||||||
|
- "{{ ansible_loop.first | ternary('server', 'agent') }}"
|
||||||
|
loop: "{{ groups.raspberries | intersect(groups.local) | sort }}"
|
||||||
|
loop_control:
|
||||||
|
extended: true
|
||||||
|
extended_allitems: false
|
||||||
|
|
||||||
|
- name: Install collection k3s.orchestration
|
||||||
|
local_action:
|
||||||
|
module: community.general.ansible_galaxy_install
|
||||||
|
type: collection
|
||||||
|
name: git+https://github.com/k3s-io/k3s-ansible
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Install socat for kubectl port forwarding
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: socat
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: k3s
|
||||||
|
ansible.builtin.import_playbook: k3s.orchestration.site
|
||||||
|
# ansible.builtin.import_playbook: k3s.orchestration.upgrade
|
||||||
|
# ansible.builtin.import_playbook: k3s.orchestration.reset
|
||||||
|
vars:
|
||||||
|
k3s_version: v1.34.3+k3s1
|
||||||
|
extra_server_args: >-
|
||||||
|
--docker --disable traefik
|
||||||
|
--kubelet-arg="container-log-max-files=5"
|
||||||
|
--kubelet-arg="container-log-max-size=10Mi"
|
||||||
|
extra_agent_args: >-
|
||||||
|
--docker
|
||||||
|
--kubelet-arg="container-log-max-files=5"
|
||||||
|
--kubelet-arg="container-log-max-size=10Mi"
|
||||||
|
api_endpoint: "{{ hostvars[groups['server'][0]]['ansible_host'] | default(groups['server'][0]) }}"
|
||||||
|
|
||||||
|
- name: how to reach k3s
|
||||||
|
hosts: server
|
||||||
|
tasks:
|
||||||
|
- name: copy /etc/rancher/k3s/k3s.yaml to ~/.kube/config from the k3s server and replace 127.0.0.1 with the server ip or hostname
|
||||||
|
run_once: true
|
||||||
|
block:
|
||||||
|
- ansible.builtin.fetch:
|
||||||
|
src: /etc/rancher/k3s/k3s.yaml
|
||||||
|
dest: ~/.kube/config
|
||||||
|
flat: true
|
||||||
|
become: true
|
||||||
|
run_once: true
|
||||||
|
- local_action:
|
||||||
|
module: ansible.builtin.replace
|
||||||
|
path: ~/.kube/config
|
||||||
|
regexp: 'server: https://127.0.0.1:6443'
|
||||||
|
replace: 'server: https://{{ ansible_default_ipv4.address }}:6443'
|
||||||
10
ansible/arcodange/factory/playbooks/tools/crowdsec.yml
Normal file
10
ansible/arcodange/factory/playbooks/tools/crowdsec.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
- name: crowdsec
|
||||||
|
# hosts: raspberries:&local
|
||||||
|
hosts: localhost
|
||||||
|
# debugger: on_failed
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Setup crowdsec middleware for traefik
|
||||||
|
include_role:
|
||||||
|
name: crowdsec
|
||||||
@@ -29,18 +29,18 @@
|
|||||||
name: hashicorp_vault
|
name: hashicorp_vault
|
||||||
vars:
|
vars:
|
||||||
pg_conf: >-
|
pg_conf: >-
|
||||||
{{ hostvars[groups.hard_disk[0]].postgres.dockercompose.services.postgres.environment }}
|
{{ hostvars[groups.postgres[0]].postgres.dockercompose.services.postgres.environment }}
|
||||||
postgres_admin_credentials:
|
postgres_admin_credentials:
|
||||||
username: '{{ pg_conf.POSTGRES_USER }}'
|
username: '{{ pg_conf.POSTGRES_USER }}'
|
||||||
password: '{{ pg_conf.POSTGRES_PASSWORD }}'
|
password: '{{ pg_conf.POSTGRES_PASSWORD }}'
|
||||||
gitea_admin_token: '{{ vault_GITEA_ADMIN_TOKEN }}'
|
gitea_admin_token: '{{ vault_GITEA_ADMIN_TOKEN }}'
|
||||||
|
|
||||||
- name: share VAULT CA
|
# - name: share VAULT CA
|
||||||
block:
|
# block:
|
||||||
|
|
||||||
- name: read traefik CA
|
# - name: read traefik CA
|
||||||
include_role:
|
# include_role:
|
||||||
name: arcodange.factory.traefik_certs
|
# name: arcodange.factory.traefik_certs
|
||||||
|
|
||||||
post_tasks:
|
post_tasks:
|
||||||
- include_role:
|
- include_role:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
traefik_pvc_name: traefik
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
- name: Inject captcha.html into Traefik PVC
|
||||||
|
block:
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# Scale to 0
|
||||||
|
# ---------------------
|
||||||
|
- name: Scale Traefik to 0
|
||||||
|
kubernetes.core.k8s_scale:
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: kube-system
|
||||||
|
name: traefik
|
||||||
|
replicas: 0
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# Create Job
|
||||||
|
# ---------------------
|
||||||
|
- name: Deploy captcha injection Job
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
namespace: kube-system
|
||||||
|
definition:
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: inject-captcha
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
volumes:
|
||||||
|
- name: traefik-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: "{{ traefik_pvc_name }}"
|
||||||
|
containers:
|
||||||
|
- name: write-captcha
|
||||||
|
image: alpine:3.20
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
echo "Writing captcha.html into PVC..."
|
||||||
|
cat << 'EOF' > /data/captcha.html
|
||||||
|
{{ lookup('template', 'captcha.html.j2') | indent(20) }}
|
||||||
|
EOF
|
||||||
|
volumeMounts:
|
||||||
|
- name: traefik-data
|
||||||
|
mountPath: /data
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# Wait for job success
|
||||||
|
# ---------------------
|
||||||
|
- name: Wait for Job completion
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
api_version: batch/v1
|
||||||
|
kind: Job
|
||||||
|
name: inject-captcha
|
||||||
|
namespace: kube-system
|
||||||
|
register: job_status
|
||||||
|
until: job_status.resources[0].status.succeeded | default(0) | int > 0
|
||||||
|
retries: 20
|
||||||
|
delay: 5
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# Clean Job
|
||||||
|
# ---------------------
|
||||||
|
- name: Remove captcha injection Job
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: absent
|
||||||
|
api_version: batch/v1
|
||||||
|
kind: Job
|
||||||
|
name: inject-captcha
|
||||||
|
namespace: kube-system
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: Log failure
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "An error occurred during captcha injection. Traefik will still be scaled back up."
|
||||||
|
|
||||||
|
always:
|
||||||
|
# ---------------------
|
||||||
|
# Ensure Traefik is scaled back to 1 NO MATTER WHAT
|
||||||
|
# ---------------------
|
||||||
|
- name: Ensure Traefik is scaled back to 1
|
||||||
|
kubernetes.core.k8s_scale:
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: kube-system
|
||||||
|
name: traefik
|
||||||
|
replicas: 1
|
||||||
|
wait: yes
|
||||||
|
wait_timeout: 300
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
- name: Créer le ServiceAccount pour l'authentification Vault
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: factory-ansible-tool-crowdsec-traefik-plugin
|
||||||
|
namespace: kube-system
|
||||||
|
wait: yes
|
||||||
|
wait_timeout: 30
|
||||||
|
|
||||||
|
- name: Créer la ressource VaultAuth
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
kind: VaultAuth
|
||||||
|
metadata:
|
||||||
|
name: factory-ansible-tool-crowdsec
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
method: kubernetes
|
||||||
|
mount: kubernetes
|
||||||
|
kubernetes:
|
||||||
|
role: factory_crowdsec_conf
|
||||||
|
serviceAccount: factory-ansible-tool-crowdsec-traefik-plugin
|
||||||
|
audiences:
|
||||||
|
- vault
|
||||||
|
wait: yes
|
||||||
|
wait_timeout: 30
|
||||||
|
|
||||||
|
- name: Créer la ressource VaultStaticSecret
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
kind: VaultStaticSecret
|
||||||
|
metadata:
|
||||||
|
name: factory-ansible-tool-crowdsec-turnstile-secret
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
type: kv-v2
|
||||||
|
mount: kvv2
|
||||||
|
path: cms/factory/turnstile
|
||||||
|
destination:
|
||||||
|
name: factory-ansible-tool-crowdsec-traefik-plugin-captcha-params
|
||||||
|
create: true
|
||||||
|
refreshAfter: 30s
|
||||||
|
vaultAuthRef: factory-ansible-tool-crowdsec
|
||||||
|
wait: yes
|
||||||
|
wait_timeout: 30
|
||||||
|
|
||||||
|
- name: Récupérer le secret Kubernetes
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
kind: Secret
|
||||||
|
name: factory-ansible-tool-crowdsec-traefik-plugin-captcha-params
|
||||||
|
namespace: kube-system
|
||||||
|
register: crowdsec_captcha_secret
|
||||||
|
|
||||||
|
- name: Récupérer le nom du pod CrowdSec LAPI
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
kind: Pod
|
||||||
|
namespace: tools
|
||||||
|
label_selectors:
|
||||||
|
- k8s-app = crowdsec
|
||||||
|
- type = lapi
|
||||||
|
register: crowdsec_lapi_pods
|
||||||
|
|
||||||
|
- name: Vérifier qu'un pod a été trouvé
|
||||||
|
assert:
|
||||||
|
that: crowdsec_lapi_pods.resources | length > 0
|
||||||
|
fail_msg: "Aucun pod CrowdSec LAPI trouvé dans le namespace 'tools' avec les labels 'k8s-app=crowdsec, type=lapi'."
|
||||||
|
|
||||||
|
- name: Définir le nom du pod CrowdSec LAPI
|
||||||
|
set_fact:
|
||||||
|
crowdsec_lapi_pod_name: "{{ crowdsec_lapi_pods.resources[0].metadata.name }}"
|
||||||
|
|
||||||
|
- name: Récupérer la clé API du bouncer CrowdSec
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: tools
|
||||||
|
pod: "{{ crowdsec_lapi_pod_name }}"
|
||||||
|
container: crowdsec-lapi
|
||||||
|
command: >
|
||||||
|
cscli bouncers add traefik-plugin
|
||||||
|
register: bouncer_key_result
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Supprimer le bouncer existant en cas d'échec
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: tools
|
||||||
|
pod: "{{ crowdsec_lapi_pod_name }}"
|
||||||
|
container: crowdsec-lapi
|
||||||
|
command: >
|
||||||
|
cscli bouncers delete traefik-plugin
|
||||||
|
when: bouncer_key_result.failed
|
||||||
|
|
||||||
|
- name: Réessayer de récupérer la clé API
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: tools
|
||||||
|
pod: "{{ crowdsec_lapi_pod_name }}"
|
||||||
|
container: crowdsec-lapi
|
||||||
|
command: >
|
||||||
|
cscli bouncers add traefik-plugin
|
||||||
|
register: bouncer_key_result
|
||||||
|
when: bouncer_key_result.failed
|
||||||
|
|
||||||
|
- name: Inject captcha.html into Traefik PVC
|
||||||
|
include_tasks: inject_captcha_html.yml
|
||||||
|
tags: never
|
||||||
|
|
||||||
|
- name: Créer le Middleware Traefik pour CrowdSec
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
plugin:
|
||||||
|
crowdsec-bouncer:
|
||||||
|
enabled: true
|
||||||
|
logLevel: DEBUG
|
||||||
|
crowdsecMode: stream
|
||||||
|
crowdsecLapiScheme: http
|
||||||
|
crowdsecLapiHost: crowdsec-service.tools.svc.cluster.local:8080
|
||||||
|
crowdsecLapiKey: "{{ bouncer_key_result.stdout_lines[2].strip() }}"
|
||||||
|
htttTimeoutSeconds: 60
|
||||||
|
crowdsecAppsecEnabled: false
|
||||||
|
crowdsecAppsecHost: crowdsec:7422
|
||||||
|
crowdsecAppsecFailureBlock: true
|
||||||
|
crowdsecAppsecUnreachableBlock: true
|
||||||
|
forwardedHeadersTrustedIPs:
|
||||||
|
- 10.0.10.23/32
|
||||||
|
- 10.0.20.0/24
|
||||||
|
clientTrustedIPs:
|
||||||
|
- 192.168.1.0/24
|
||||||
|
- 10.42.0.0/16
|
||||||
|
captchaProvider: turnstile
|
||||||
|
captchaSiteKey: "{{ crowdsec_captcha_secret.resources[0].data.sitekey | b64decode }}"
|
||||||
|
captchaSecretKey: "{{ crowdsec_captcha_secret.resources[0].data.secret | b64decode }}"
|
||||||
|
captchaHTMLFilePath: "/data/captcha.html"
|
||||||
|
redisCacheEnabled: true
|
||||||
|
redisCacheHost: "redis.tools:6379"
|
||||||
|
redisCacheDatabase: "0"
|
||||||
|
redisCacheUnreachableBlock: false
|
||||||
|
|
||||||
|
- name: Redémarrer traefik pour prendre la nouvelle configuration du middleware
|
||||||
|
block:
|
||||||
|
# ---------------------
|
||||||
|
# Scale to 0
|
||||||
|
# ---------------------
|
||||||
|
- name: Scale Traefik to 0
|
||||||
|
kubernetes.core.k8s_scale:
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: kube-system
|
||||||
|
name: traefik
|
||||||
|
replicas: 0
|
||||||
|
rescue:
|
||||||
|
- name: Log failure
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "An error occurred during traefik scale down. Traefik will still be scaled back up."
|
||||||
|
|
||||||
|
always:
|
||||||
|
# ---------------------
|
||||||
|
# Ensure Traefik is scaled back to 1 NO MATTER WHAT
|
||||||
|
# ---------------------
|
||||||
|
- name: Ensure Traefik is scaled back to 1
|
||||||
|
kubernetes.core.k8s_scale:
|
||||||
|
api_version: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
namespace: kube-system
|
||||||
|
name: traefik
|
||||||
|
replicas: 1
|
||||||
|
wait: yes
|
||||||
|
wait_timeout: 300
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Captcha verification</title>
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="cf-turnstile"
|
||||||
|
data-sitekey="{{ crowdsec_captcha_secret.resources[0].data.sitekey | b64decode }}"
|
||||||
|
data-theme="auto"
|
||||||
|
data-size="normal">
|
||||||
|
</div>
|
||||||
|
<button type="submit">Valider</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,7 +2,7 @@ vault_unseal_keys_path: ~/.arcodange/cluster-keys.json
|
|||||||
vault_unseal_keys_shares: 1
|
vault_unseal_keys_shares: 1
|
||||||
vault_unseal_keys_key_threshold: 1 # keys_key_threshold <= keys_shares
|
vault_unseal_keys_key_threshold: 1 # keys_key_threshold <= keys_shares
|
||||||
|
|
||||||
vault_address: https://vault.arcodange.duckdns.org
|
vault_address: https://vault.arcodange.lab
|
||||||
|
|
||||||
vault_oidc_gitea_setupGiteaAppJS: '{{ role_path }}/files/playwright_setupGiteaApp.js'
|
vault_oidc_gitea_setupGiteaAppJS: '{{ role_path }}/files/playwright_setupGiteaApp.js'
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ variable "admin_email" {
|
|||||||
}
|
}
|
||||||
variable "gitea_app" {
|
variable "gitea_app" {
|
||||||
type = object({
|
type = object({
|
||||||
url = optional(string, "https://gitea.arcodange.duckdns.org/")
|
url = optional(string, "https://gitea.arcodange.lab")
|
||||||
id = string
|
id = string
|
||||||
secret = string
|
secret = string
|
||||||
description = optional(string, "Arcodange Gitea Auth")
|
description = optional(string, "Arcodange Gitea Auth")
|
||||||
@@ -39,10 +39,10 @@ variable "gitea_admin_token" {
|
|||||||
sensitive = true
|
sensitive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
# kubectl -n kube-system exec $(kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik -o jsonpath="{.items[0]['.metadata.name']}") -- cat /data/acme.json | jq '(.letsencrypt.Certificates | map(select(.domain.main=="arcodange.duckdns.org")))[0]' | jq '.certificate' -r | base64 -d | openssl x509
|
# same as vault CA
|
||||||
# variable "ca_pem" {
|
variable "ca_pem" {
|
||||||
# type = string
|
type = string
|
||||||
# }
|
}
|
||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
vault = {
|
vault = {
|
||||||
@@ -63,10 +63,10 @@ resource "vault_jwt_auth_backend" "gitea" {
|
|||||||
path = "gitea"
|
path = "gitea"
|
||||||
type = "oidc"
|
type = "oidc"
|
||||||
oidc_discovery_url = var.gitea_app.url
|
oidc_discovery_url = var.gitea_app.url
|
||||||
# oidc_discovery_ca_pem = var.ca_pem
|
oidc_discovery_ca_pem = file(var.ca_pem)
|
||||||
oidc_client_id = var.gitea_app.id
|
oidc_client_id = var.gitea_app.id
|
||||||
oidc_client_secret = var.gitea_app.secret
|
oidc_client_secret = var.gitea_app.secret
|
||||||
bound_issuer = var.gitea_app.url
|
bound_issuer = trimsuffix(var.gitea_app.url, "/")
|
||||||
|
|
||||||
tune {
|
tune {
|
||||||
allowed_response_headers = []
|
allowed_response_headers = []
|
||||||
@@ -91,7 +91,8 @@ resource "vault_jwt_auth_backend_role" "gitea" {
|
|||||||
allowed_redirect_uris = [
|
allowed_redirect_uris = [
|
||||||
"http://localhost:8250/oidc/callback", # for command line login
|
"http://localhost:8250/oidc/callback", # for command line login
|
||||||
"${var.vault_address}/ui/vault/auth/gitea/oidc/callback",
|
"${var.vault_address}/ui/vault/auth/gitea/oidc/callback",
|
||||||
"https://webapp.arcodange.duckdns.org/oauth-callback",
|
"https://webapp.arcodange.fr/oauth-callback",
|
||||||
|
"https://webapp.arcodange.lab/oauth-callback",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +102,8 @@ resource "vault_jwt_auth_backend" "gitea_jwt" {
|
|||||||
path = "gitea_jwt"
|
path = "gitea_jwt"
|
||||||
type = "jwt"
|
type = "jwt"
|
||||||
oidc_discovery_url = var.gitea_app.url
|
oidc_discovery_url = var.gitea_app.url
|
||||||
# oidc_discovery_ca_pem = var.ca_pem
|
oidc_discovery_ca_pem = file(var.ca_pem)
|
||||||
bound_issuer = var.gitea_app.url
|
bound_issuer = trimsuffix(var.gitea_app.url, "/")
|
||||||
|
|
||||||
tune {
|
tune {
|
||||||
allowed_response_headers = []
|
allowed_response_headers = []
|
||||||
@@ -166,7 +167,7 @@ resource "vault_kv_secret" "google_credentials" {
|
|||||||
path = "${vault_mount.kvv1.path}/google/credentials"
|
path = "${vault_mount.kvv1.path}/google/credentials"
|
||||||
data_json = jsonencode(
|
data_json = jsonencode(
|
||||||
{
|
{
|
||||||
credentials = file("~/.config/gcloud/application_default_credentials.json")
|
credentials = file("/root/.config/gcloud/application_default_credentials.json")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const username = process.env.GITEA_USER;
|
|||||||
const password = process.env.GITEA_PASSWORD;
|
const password = process.env.GITEA_PASSWORD;
|
||||||
const debug = Boolean(process.env.DEBUG);
|
const debug = Boolean(process.env.DEBUG);
|
||||||
const vaultAddress = process.env.VAULT_ADDRESS || 'http://localhost:8200';
|
const vaultAddress = process.env.VAULT_ADDRESS || 'http://localhost:8200';
|
||||||
const giteaAddress = process.env.GITEA_ADDRESS || 'https://gitea.arcodange.duckdns.org';
|
const giteaAddress = process.env.GITEA_ADDRESS || 'https://gitea.arcodange.lab';
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
console.error('Veuillez définir les variables d\'environnement GITEA_USER et GITEA_PASSWORD.');
|
console.error('Veuillez définir les variables d\'environnement GITEA_USER et GITEA_PASSWORD.');
|
||||||
@@ -22,7 +22,7 @@ const browser = await chromium.launch({
|
|||||||
log: (name, severity, message, args) => console.warn(`${severity}| ${name} :: ${message} __ ${args}`)
|
log: (name, severity, message, args) => console.warn(`${severity}| ${name} :: ${message} __ ${args}`)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const context = await browser.newContext({locale: "gb-GB"});
|
const context = await browser.newContext({locale: "gb-GB", ignoreHTTPSErrors: true}); // Using self signed cert - could improve with NODE_EXTRA_CA_CERTS env variable
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
async function doLogin() {
|
async function doLogin() {
|
||||||
@@ -70,11 +70,13 @@ async function setupApp() {
|
|||||||
await applicationsPanel.locator('form[action$="/regenerate_secret"] > button').click();
|
await applicationsPanel.locator('form[action$="/regenerate_secret"] > button').click();
|
||||||
} else {
|
} else {
|
||||||
console.warn('app not found');
|
console.warn('app not found');
|
||||||
|
await applicationsPanel.getByText("Créer une nouvelle application OAuth2").click();
|
||||||
await applicationsPanel.locator('input[name="application_name"]').fill(appName);
|
await applicationsPanel.locator('input[name="application_name"]').fill(appName);
|
||||||
await applicationsPanel.locator('textarea[name="redirect_uris"]').fill([
|
await applicationsPanel.locator('textarea[name="redirect_uris"]').fill([
|
||||||
'http://localhost:8250/oidc/callback', // for command line login
|
'http://localhost:8250/oidc/callback', // for command line login
|
||||||
`${vaultAddress}/ui/vault/auth/gitea/oidc/callback`,
|
`${vaultAddress}/ui/vault/auth/gitea/oidc/callback`,
|
||||||
'https://webapp.arcodange.duckdns.org/oauth-callback',
|
'https://webapp.arcodange.lab/oauth-callback',
|
||||||
|
'https://webapp.arcodange.fr/oauth-callback',
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
await applicationsPanel.locator('form[action="/-/admin/applications/oauth2"] > button').dblclick()
|
await applicationsPanel.locator('form[action="/-/admin/applications/oauth2"] > button').dblclick()
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,44 @@
|
|||||||
GITEA_USER: '{{ gitea_admin_user }}'
|
GITEA_USER: '{{ gitea_admin_user }}'
|
||||||
GITEA_PASSWORD: '{{ gitea_admin_password }}'
|
GITEA_PASSWORD: '{{ gitea_admin_password }}'
|
||||||
VAULT_ADDRESS: '{{ vault_address }}'
|
VAULT_ADDRESS: '{{ vault_address }}'
|
||||||
|
NODE_EXTRA_CA_CERTS: ''
|
||||||
|
|
||||||
- include_role:
|
- include_role:
|
||||||
name: arcodange.factory.playwright
|
name: arcodange.factory.playwright
|
||||||
|
|
||||||
- include_role:
|
# - include_role:
|
||||||
name: arcodange.factory.traefik_certs
|
# name: arcodange.factory.traefik_certs
|
||||||
|
|
||||||
- set_fact:
|
- set_fact:
|
||||||
gitea_app: '{{ playwright_job.stdout | from_json }}'
|
gitea_app: '{{ playwright_job.stdout | from_json }}'
|
||||||
|
|
||||||
volume_name: tofu-{{ ansible_date_time.iso8601.replace(':','-') }}
|
volume_name: tofu-{{ ansible_date_time.iso8601.replace(':','-') }}
|
||||||
|
|
||||||
|
- name: Check SSL certificate for Gitea
|
||||||
|
shell: >-
|
||||||
|
openssl s_client -connect gitea.arcodange.lab:443 -CAfile /etc/ssl/certs/arcodange-root.pem -servername gitea.arcodange.lab < /dev/null 2>&1 | grep -E "Verify return code:|subject=|issuer="
|
||||||
|
register: ssl_check
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Debug SSL certificate check
|
||||||
|
debug:
|
||||||
|
var: ssl_check.stdout_lines
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Delete existing Gitea OIDC backends if they exist
|
||||||
|
include_tasks: vault_cmd.yml
|
||||||
|
vars:
|
||||||
|
vault_cmd: vault auth disable {{ backend_name }}
|
||||||
|
vault_cmd_can_fail: true
|
||||||
|
vault_cmd_json_attr: ''
|
||||||
|
vault_cmd_output_var: false
|
||||||
|
loop:
|
||||||
|
- gitea
|
||||||
|
- gitea_jwt
|
||||||
|
loop_control:
|
||||||
|
loop_var: backend_name
|
||||||
|
|
||||||
- name: use tofu to provision vault
|
- name: use tofu to provision vault
|
||||||
block:
|
block:
|
||||||
- shell: docker volume create {{ volume_name }}
|
- shell: docker volume create {{ volume_name }}
|
||||||
@@ -31,12 +57,22 @@
|
|||||||
-v {{ volume_name }}:/tofu -w /tofu
|
-v {{ volume_name }}:/tofu -w /tofu
|
||||||
-v {{ role_path }}/files/hashicorp_vault.tf:/tofu/hashicorp_vault.tf
|
-v {{ role_path }}/files/hashicorp_vault.tf:/tofu/hashicorp_vault.tf
|
||||||
-v ~/.config/gcloud:/root/.config/gcloud
|
-v ~/.config/gcloud:/root/.config/gcloud
|
||||||
|
-v /etc/ssl/certs/arcodange-root.pem:/etc/ssl/custom/arcodange-root.pem:ro
|
||||||
|
-e VAULT_CACERT=/etc/ssl/custom/arcodange-root.pem
|
||||||
--entrypoint=''
|
--entrypoint=''
|
||||||
ghcr.io/opentofu/opentofu:latest
|
ghcr.io/opentofu/opentofu:latest
|
||||||
{{ command }}
|
{{ command }}
|
||||||
register: last_tofu_command
|
register: last_tofu_command
|
||||||
loop:
|
loop:
|
||||||
- tofu init -no-color
|
- tofu init -no-color
|
||||||
|
# - >-
|
||||||
|
# tofu destroy -auto-approve -no-color
|
||||||
|
# -var='gitea_app={{ gitea_app | to_json }}'
|
||||||
|
# -var='vault_address={{ vault_address }}'
|
||||||
|
# -var='vault_token={{ vault_root_token }}'
|
||||||
|
# -var='postgres_admin_credentials={{ postgres_admin_credentials | to_json }}'
|
||||||
|
# -var='gitea_admin_token={{ gitea_admin_token }}'
|
||||||
|
# -var="ca_pem=/etc/ssl/custom/arcodange-root.pem"
|
||||||
- >-
|
- >-
|
||||||
tofu apply -auto-approve -no-color
|
tofu apply -auto-approve -no-color
|
||||||
-var='gitea_app={{ gitea_app | to_json }}'
|
-var='gitea_app={{ gitea_app | to_json }}'
|
||||||
@@ -44,6 +80,7 @@
|
|||||||
-var='vault_token={{ vault_root_token }}'
|
-var='vault_token={{ vault_root_token }}'
|
||||||
-var='postgres_admin_credentials={{ postgres_admin_credentials | to_json }}'
|
-var='postgres_admin_credentials={{ postgres_admin_credentials | to_json }}'
|
||||||
-var='gitea_admin_token={{ gitea_admin_token }}'
|
-var='gitea_admin_token={{ gitea_admin_token }}'
|
||||||
|
-var="ca_pem=/etc/ssl/custom/arcodange-root.pem"
|
||||||
loop_control:
|
loop_control:
|
||||||
loop_var: command
|
loop_var: command
|
||||||
extended: true
|
extended: true
|
||||||
@@ -64,7 +101,7 @@
|
|||||||
gitea_secret_name: vault_oauth__sh_b64
|
gitea_secret_name: vault_oauth__sh_b64
|
||||||
gitea_secret_value: >-
|
gitea_secret_value: >-
|
||||||
{{ lookup('ansible.builtin.template', 'oidc_jwt_token.sh.j2', template_vars = {
|
{{ lookup('ansible.builtin.template', 'oidc_jwt_token.sh.j2', template_vars = {
|
||||||
'GITEA_BASE_URL': 'https://gitea.arcodange.duckdns.org',
|
'GITEA_BASE_URL': 'https://gitea.arcodange.lab',
|
||||||
'OIDC_CLIENT_ID': gitea_app.id,
|
'OIDC_CLIENT_ID': gitea_app.id,
|
||||||
'OIDC_CLIENT_SECRET': gitea_app.secret,
|
'OIDC_CLIENT_SECRET': gitea_app.secret,
|
||||||
}) | b64encode }}
|
}) | b64encode }}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ set -eu
|
|||||||
# Variables à ajuster selon ta configuration
|
# Variables à ajuster selon ta configuration
|
||||||
CLIENT_ID="{{ OIDC_CLIENT_ID }}"
|
CLIENT_ID="{{ OIDC_CLIENT_ID }}"
|
||||||
CLIENT_SECRET="{{ OIDC_CLIENT_SECRET }}"
|
CLIENT_SECRET="{{ OIDC_CLIENT_SECRET }}"
|
||||||
REDIRECT_URI="{{ OIDC_CLIENT_CALLBACK | default('https://webapp.arcodange.duckdns.org/oauth-callback') }}" # Redirige ici après l'authentification
|
REDIRECT_URI="{{ OIDC_CLIENT_CALLBACK | default('https://webapp.arcodange.lab/oauth-callback') }}" # Redirige ici après l'authentification
|
||||||
AUTH_URL="{{ GITEA_BASE_URL | default('https://gitea.arcodange.duckdns.org') }}/login/oauth/authorize"
|
AUTH_URL="{{ GITEA_BASE_URL | default('https://gitea.arcodange.lab') }}/login/oauth/authorize"
|
||||||
TOKEN_URL="{{ GITEA_BASE_URL | default('https://gitea.arcodange.duckdns.org') }}/login/oauth/access_token"
|
TOKEN_URL="{{ GITEA_BASE_URL | default('https://gitea.arcodange.lab') }}/login/oauth/access_token"
|
||||||
ISSUER="https://gitea.arcodange.duckdns.org/"
|
ISSUER="https://gitea.arcodange.lab/"
|
||||||
# SCOPE="openid email profile groups" # Scope que tu souhaites obtenir - profile groups
|
# SCOPE="openid email profile groups" # Scope que tu souhaites obtenir - profile groups
|
||||||
SCOPE="email openid read:user" # Scope que tu souhaites obtenir - profile groups
|
SCOPE="email openid read:user" # Scope que tu souhaites obtenir - profile groups
|
||||||
set +u
|
set +u
|
||||||
@@ -26,7 +26,7 @@ poll_state() {
|
|||||||
#echo "Tentative $attempt/$MAX_ATTEMPTS: Requête à l'endpoint /retrieve pour state=$STATE..."
|
#echo "Tentative $attempt/$MAX_ATTEMPTS: Requête à l'endpoint /retrieve pour state=$STATE..."
|
||||||
|
|
||||||
# Effectuer la requête GET
|
# Effectuer la requête GET
|
||||||
RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/response_body "https://webapp.arcodange.duckdns.org/retrieve?state=$STATE")
|
RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/response_body "https://webapp.arcodange.lab/retrieve?state=$STATE")
|
||||||
HTTP_CODE=$(tail -n1 <<< "$RESPONSE")
|
HTTP_CODE=$(tail -n1 <<< "$RESPONSE")
|
||||||
|
|
||||||
if [ "$HTTP_CODE" == "200" ]; then
|
if [ "$HTTP_CODE" == "200" ]; then
|
||||||
@@ -50,6 +50,9 @@ poll_state() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 0. Installer le certificat arcodange.lab (droits sudo)
|
||||||
|
# curl https://ssl-ca.arcodange.lab:8443/roots.pem -ks > /usr/local/share/ca-certificates/arcodange-root.crt && update-ca-certificates 2>/dev/null >/dev/null && export VAULT_CACERT=/usr/local/share/ca-certificates/arcodange-root.crt || echo "couldn't install self signed .crt" >&2
|
||||||
|
|
||||||
# 1. Rediriger l'utilisateur vers l'URL d'authentification
|
# 1. Rediriger l'utilisateur vers l'URL d'authentification
|
||||||
echo "Ouvrez le lien suivant dans votre navigateur pour vous authentifier dans Gitea:"
|
echo "Ouvrez le lien suivant dans votre navigateur pour vous authentifier dans Gitea:"
|
||||||
echo "$AUTH_URL?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=$(sed 's/ /%20/g' <<<$SCOPE)&state=$STATE"
|
echo "$AUTH_URL?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=$(sed 's/ /%20/g' <<<$SCOPE)&state=$STATE"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
---
|
---
|
||||||
- name: hashicorp_vault
|
- name: hashicorp_vault
|
||||||
ansible.builtin.import_playbook: hashicorp_vault.yml
|
ansible.builtin.import_playbook: hashicorp_vault.yml
|
||||||
|
- name: crowdsec
|
||||||
|
ansible.builtin.import_playbook: crowdsec.yml
|
||||||
@@ -2,6 +2,7 @@ app_name: "{{ (dockercompose_content | from_yaml).name }}"
|
|||||||
app_owner: pi
|
app_owner: pi
|
||||||
app_group: docker
|
app_group: docker
|
||||||
partition: docker_composes
|
partition: docker_composes
|
||||||
hard_disk_root_path: /arcodange
|
# hard_disk_root_path: /arcodange
|
||||||
no_hard_disk_root_path: /home/pi/arcodange
|
# no_hard_disk_root_path: /home/pi/arcodange
|
||||||
root_path: "{{ ('hard_disk' in group_names) | ansible.builtin.ternary(hard_disk_root_path, no_hard_disk_root_path) }}"
|
# root_path: "{{ ('hard_disk' in group_names) | ansible.builtin.ternary(hard_disk_root_path, no_hard_disk_root_path) }}"
|
||||||
|
root_path: /home/pi/arcodange
|
||||||
@@ -4,5 +4,5 @@ gitea_token_scopes: write:admin,write:organization,write:package,write:repositor
|
|||||||
gitea_token_fact_name: gitea_api_token
|
gitea_token_fact_name: gitea_api_token
|
||||||
gitea_base_url: 'http://{{ groups.gitea[0] }}:3000'
|
gitea_base_url: 'http://{{ groups.gitea[0] }}:3000'
|
||||||
gitea_token_replace: false
|
gitea_token_replace: false
|
||||||
gitea_token_name: ansible-{{ ansible_date_time.iso8601 }} # require gathering facts
|
gitea_token_name: ansible-{{ ansible_date_time.iso8601 }}-{{ inventory_hostname }} # require gathering facts
|
||||||
gitea_token_delete: false # only delete token
|
gitea_token_delete: false # only delete token
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# to see generated tokens
|
# to see generated tokens
|
||||||
# go to https://gitea.arcodange.duckdns.org/user/settings/applications
|
# go to https://gitea.arcodange.lab/user/settings/applications
|
||||||
|
|
||||||
- when: >-
|
- when: >-
|
||||||
lookup('ansible.builtin.varnames', '^' ~ gitea_token_fact_name ~ '$') | length == 0
|
lookup('ansible.builtin.varnames', '^' ~ gitea_token_fact_name ~ '$') | length == 0
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const username = process.env.GITEA_USER;
|
|||||||
const password = process.env.GITEA_PASSWORD;
|
const password = process.env.GITEA_PASSWORD;
|
||||||
const debug = Boolean(process.env.DEBUG);
|
const debug = Boolean(process.env.DEBUG);
|
||||||
const vaultAddress = process.env.VAULT_ADDRESS || 'http://localhost:8200';
|
const vaultAddress = process.env.VAULT_ADDRESS || 'http://localhost:8200';
|
||||||
const giteaAddress = process.env.GITEA_ADDRESS || 'https://gitea.arcodange.duckdns.org';
|
const giteaAddress = process.env.GITEA_ADDRESS || 'https://gitea.arcodange.lab';
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
console.error('Veuillez définir les variables d\'environnement GITEA_USER et GITEA_PASSWORD.');
|
console.error('Veuillez définir les variables d\'environnement GITEA_USER et GITEA_PASSWORD.');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
kubectl -n kube-system exec
|
kubectl -n kube-system exec
|
||||||
$(kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik
|
$(kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik
|
||||||
-o jsonpath="{.items[0]['.metadata.name']}") --
|
-o jsonpath="{.items[0]['.metadata.name']}") --
|
||||||
cat /data/acme.json | jq '(.letsencrypt.Certificates | map(select(.domain.main=="*.arcodange.duckdns.org")))[0]'
|
cat /data/acme.json | jq '(.letsencrypt.Certificates | map(select(.domain.main=="*.arcodange.lab")))[0]'
|
||||||
| jq '.certificate' -r | base64 -d | openssl x509
|
| jq '.certificate' -r | base64 -d | openssl x509
|
||||||
register: traefik_certs_cmd
|
register: traefik_certs_cmd
|
||||||
- set_fact:
|
- set_fact:
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ roles:
|
|||||||
- name: geerlingguy.docker
|
- name: geerlingguy.docker
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
- name: community.general
|
|
||||||
- name: community.docker
|
|
||||||
- name: ansible.posix
|
- name: ansible.posix
|
||||||
|
- name: community.crypto
|
||||||
|
- name: community.docker
|
||||||
|
- name: community.general
|
||||||
- name: kubernetes.core
|
- name: kubernetes.core
|
||||||
- name: git+https://github.com/k3s-io/k3s-ansible.git
|
- name: git+https://github.com/k3s-io/k3s-ansible.git
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{{- range $app_name := .Values.gitea_applications -}}
|
{{- range $app_name, $app_attr := .Values.gitea_applications -}}
|
||||||
|
{{- $org := default "arcodange-org" $app_attr.org -}}
|
||||||
---
|
---
|
||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
@@ -7,10 +8,14 @@ metadata:
|
|||||||
namespace: argocd
|
namespace: argocd
|
||||||
finalizers:
|
finalizers:
|
||||||
- resources-finalizer.argocd.argoproj.io
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
{{- with $app_attr.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
project: default
|
project: default
|
||||||
source:
|
source:
|
||||||
repoURL: https://gitea.arcodange.duckdns.org/arcodange-org/{{ $app_name }}
|
repoURL: https://gitea.arcodange.lab/{{ $org }}/{{ $app_name }}
|
||||||
targetRevision: HEAD
|
targetRevision: HEAD
|
||||||
path: chart
|
path: chart
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
14
argocd/templates/argocd_image_updater.yaml
Normal file
14
argocd/templates/argocd_image_updater.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{{ with ( .Values.argocd_image_updater_chart_values ) }}
|
||||||
|
apiVersion: helm.cattle.io/v1
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: argocd-image-updater
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
repo: https://argoproj.github.io/argo-helm
|
||||||
|
chart: argocd-image-updater
|
||||||
|
targetNamespace: argocd
|
||||||
|
valuesContent: |-
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end -}}
|
||||||
|
---
|
||||||
49
argocd/templates/longhorn_backup_target.yaml
Normal file
49
argocd/templates/longhorn_backup_target.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: longhorn-vault-secret-reader
|
||||||
|
namespace: longhorn-system
|
||||||
|
---
|
||||||
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
kind: VaultAuth
|
||||||
|
metadata:
|
||||||
|
name: longhorn-vault-secret-reader
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
method: kubernetes
|
||||||
|
mount: kubernetes
|
||||||
|
kubernetes:
|
||||||
|
role: longhorn
|
||||||
|
serviceAccount: longhorn-vault-secret-reader # le même que dans TF
|
||||||
|
audiences:
|
||||||
|
- vault
|
||||||
|
---
|
||||||
|
apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
kind: VaultStaticSecret
|
||||||
|
metadata:
|
||||||
|
name: longhorn-gcs-backup-credentials
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
type: kv-v2
|
||||||
|
mount: kvv2
|
||||||
|
|
||||||
|
path: longhorn/gcs-backup
|
||||||
|
|
||||||
|
destination:
|
||||||
|
name: longhorn-gcs-backup-credentials
|
||||||
|
create: true
|
||||||
|
|
||||||
|
refreshAfter: 1h
|
||||||
|
|
||||||
|
vaultAuthRef: longhorn-vault-secret-reader
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: longhorn-default-resource
|
||||||
|
namespace: longhorn-system
|
||||||
|
data:
|
||||||
|
default-resource.yaml: |
|
||||||
|
"backup-target": "s3://arcodange-backup@us-east-1/"
|
||||||
|
"backup-target-credential-secret": "longhorn-gcs-backup-credentials"
|
||||||
|
"backupstore-poll-interval": "180"
|
||||||
@@ -2,7 +2,30 @@
|
|||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
# Declare variables to be passed into your templates.
|
# Declare variables to be passed into your templates.
|
||||||
gitea_applications:
|
gitea_applications:
|
||||||
- url-shortener
|
url-shortener:
|
||||||
- tools
|
annotations: {}
|
||||||
- webapp
|
tools:
|
||||||
- erp
|
annotations: {}
|
||||||
|
webapp:
|
||||||
|
annotations:
|
||||||
|
argocd-image-updater.argoproj.io/image-list: webapp=gitea.arcodange.lab/arcodange-org/webapp:latest
|
||||||
|
argocd-image-updater.argoproj.io/webapp.update-strategy: digest
|
||||||
|
erp:
|
||||||
|
annotations: {}
|
||||||
|
cms:
|
||||||
|
annotations:
|
||||||
|
argocd-image-updater.argoproj.io/image-list: cms=gitea.arcodange.lab/arcodange-org/cms:latest
|
||||||
|
argocd-image-updater.argoproj.io/cms.update-strategy: digest
|
||||||
|
dance-lessons-coach:
|
||||||
|
org: arcodange
|
||||||
|
annotations:
|
||||||
|
argocd-image-updater.argoproj.io/image-list: dance-lessons-coach=gitea.arcodange.lab/arcodange/dance-lessons-coach:latest
|
||||||
|
argocd-image-updater.argoproj.io/dance-lessons-coach.update-strategy: digest
|
||||||
|
|
||||||
|
argocd_image_updater_chart_values:
|
||||||
|
config:
|
||||||
|
argocd:
|
||||||
|
grpcWeb: false
|
||||||
|
serverAddress: "https://argocd.arcodange.lab/"
|
||||||
|
insecure: true
|
||||||
|
plaintext: true
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
>L'unsealKey, le vaultRootToken initial et l'authentification au backend terraform sont pour le moment configurés sur le controleur ansible (Macbook Pro).
|
>L'unsealKey, le vaultRootToken initial et l'authentification au backend terraform sont pour le moment configurés sur le controleur ansible (Macbook Pro).
|
||||||
|
|
||||||
>[!NOTE]
|
>[!NOTE]
|
||||||
> Vault est déployé via [argo cd](https://gitea.arcodange.duckdns.org/arcodange-org/tools/src/branch/main/hashicorp-vault)
|
> Vault est déployé via [argo cd](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
%%{init: { 'logLevel': 'debug', 'theme': 'base',
|
%%{init: { 'logLevel': 'debug', 'theme': 'base',
|
||||||
|
|||||||
8
iac/README.md
Normal file
8
iac/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#
|
||||||
|
|
||||||
|
Provisionne un utilisateur gitea "tofu_module_reader",
|
||||||
|
autorisé à lire certains projets il est utilisé par la CI pour récupérer des blueprints terraform
|
||||||
|
via sa clé ssh répertoriée dans vault.
|
||||||
|
|
||||||
|
#
|
||||||
|
configure les tokens ovh et cloudflare pour permettre aux autre projet de gérer des resources du cloud
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
terraform {
|
terraform {
|
||||||
backend "gcs" {
|
backend "gcs" {
|
||||||
bucket = "arcodange-tf"
|
bucket = "arcodange-tf"
|
||||||
prefix = "factory/main"
|
prefix = "factory/main"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
101
iac/cloudflare.tf
Normal file
101
iac/cloudflare.tf
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
data "cloudflare_account" "arcodange" {
|
||||||
|
filter = {
|
||||||
|
name = "arcodange@gmail.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
cloudflare_account_id = data.cloudflare_account.arcodange.account_id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_r2_bucket" "arcodange_tf" {
|
||||||
|
account_id = local.cloudflare_account_id
|
||||||
|
name = "arcodange-tf"
|
||||||
|
jurisdiction = "eu"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "cf_r2_arcodange_tf_token" {
|
||||||
|
source = "./modules/cloudflare_token"
|
||||||
|
account_id = local.cloudflare_account_id
|
||||||
|
bucket = cloudflare_r2_bucket.arcodange_tf
|
||||||
|
token_name = "r2_arcodange_tf_token"
|
||||||
|
permissions = {
|
||||||
|
bucket = [
|
||||||
|
"account:Workers R2 Storage Read",
|
||||||
|
"bucket:Workers R2 Storage Bucket Item Write",
|
||||||
|
]
|
||||||
|
account = [
|
||||||
|
"account:Account Settings Read",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource "vault_kv_secret" "cf_r2_arcodange_tf" {
|
||||||
|
path = "kvv1/cloudflare/r2/arcodange-tf"
|
||||||
|
data_json = jsonencode({
|
||||||
|
S3_SECRET_ACCESS_KEY = module.cf_r2_arcodange_tf_token.r2_credentials.secret_access_key
|
||||||
|
S3_ACCESS_KEY = module.cf_r2_arcodange_tf_token.r2_credentials.access_key_id
|
||||||
|
S3_ENDPOINT = "https://${local.cloudflare_account_id}.eu.r2.cloudflarestorage.com"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data "vault_policy_document" "cf_r2_arcodange_tf" {
|
||||||
|
rule {
|
||||||
|
path = "kvv1/cloudflare/r2/arcodange-tf"
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
path = "kvv1/zoho/self_client" # zoho mail client is created manually
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource "vault_policy" "cf_r2_arcodange_tf" {
|
||||||
|
name = "factory__cf_r2_arcodange_tf"
|
||||||
|
policy = data.vault_policy_document.cf_r2_arcodange_tf.hcl
|
||||||
|
}
|
||||||
|
|
||||||
|
data "gitea_repo" "cms" {
|
||||||
|
name = "cms"
|
||||||
|
username = "arcodange-org"
|
||||||
|
}
|
||||||
|
module "cf_arcodange_cms_token" {
|
||||||
|
source = "./modules/cloudflare_token"
|
||||||
|
account_id = local.cloudflare_account_id
|
||||||
|
bucket = cloudflare_r2_bucket.arcodange_tf
|
||||||
|
token_name = "cf_arcodange_cms_token"
|
||||||
|
permissions = {
|
||||||
|
account = [
|
||||||
|
"account:Pages Write",
|
||||||
|
"account:Account DNS Settings Write",
|
||||||
|
"account:Account Settings Read",
|
||||||
|
"zone:Zone Write",
|
||||||
|
"zone:Zone Settings Write",
|
||||||
|
"zone:DNS Write",
|
||||||
|
"account:Cloudflare Tunnel Write",
|
||||||
|
"account:Turnstile Sites Write",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource "gitea_repository_actions_secret" "cf_arcodange_cms_token" {
|
||||||
|
repository = data.gitea_repo.cms.name
|
||||||
|
repository_owner = data.gitea_repo.cms.username
|
||||||
|
secret_name = "CLOUDFLARE_API_TOKEN"
|
||||||
|
secret_value = module.cf_arcodange_cms_token.token
|
||||||
|
}
|
||||||
|
resource "gitea_repository_actions_secret" "cf_account_id_cms" {
|
||||||
|
repository = data.gitea_repo.cms.name
|
||||||
|
repository_owner = data.gitea_repo.cms.username
|
||||||
|
secret_name = "CLOUDFLARE_ACCOUNT_ID"
|
||||||
|
secret_value = local.cloudflare_account_id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "token" {
|
||||||
|
value = module.cf_arcodange_cms_token.token
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "vault_kv_secret" "cf_arcodange_cms_token" {
|
||||||
|
path = "kvv1/cloudflare/cms/cf_arcodange_cms_token"
|
||||||
|
data_json = jsonencode({
|
||||||
|
token = module.cf_arcodange_cms_token.token
|
||||||
|
})
|
||||||
|
}
|
||||||
64
iac/gcs_backup.tf
Normal file
64
iac/gcs_backup.tf
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# https://longhorn.io/docs/1.9.1/snapshots-and-backups/backup-and-restore/set-backup-target/#set-up-gcp-cloud-storage-backupstore
|
||||||
|
resource "google_storage_bucket" "longhorn_backup" {
|
||||||
|
name = "arcodange-backup"
|
||||||
|
location = "NAM4" # https://cloud.google.com/storage/docs/locations#location-dr
|
||||||
|
force_destroy = true
|
||||||
|
|
||||||
|
public_access_prevention = "enforced"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_service_account" "longhorn_backup" {
|
||||||
|
account_id = "longhorn-backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_storage_bucket_iam_member" "longhorn_backup" {
|
||||||
|
bucket = google_storage_bucket.longhorn_backup.name
|
||||||
|
role = "roles/storage.admin"
|
||||||
|
member = "serviceAccount:${google_service_account.longhorn_backup.email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_storage_hmac_key" "longhorn_backup" {
|
||||||
|
service_account_email = google_service_account.longhorn_backup.email
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
vault_mount_kvv2 = { path = "kvv2" }
|
||||||
|
}
|
||||||
|
data "vault_auth_backend" "kubernetes" {
|
||||||
|
path = "kubernetes"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "vault_kv_secret_v2" "longhorn_gcs_backup" {
|
||||||
|
mount = local.vault_mount_kvv2.path
|
||||||
|
name = "longhorn/gcs-backup"
|
||||||
|
cas = 1
|
||||||
|
delete_all_versions = true
|
||||||
|
data_json = jsonencode({
|
||||||
|
AWS_ACCESS_KEY_ID = google_storage_hmac_key.longhorn_backup.access_id
|
||||||
|
AWS_SECRET_ACCESS_KEY = google_storage_hmac_key.longhorn_backup.secret
|
||||||
|
AWS_ENDPOINTS : "https://storage.googleapis.com"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data "vault_policy_document" "longhorn_gcs_backup" {
|
||||||
|
rule {
|
||||||
|
path = "${local.vault_mount_kvv2.path}/data/longhorn/gcs-backup"
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "vault_policy" "longhorn_gcs_backup" {
|
||||||
|
name = "longhorn-gcs-backup"
|
||||||
|
policy = data.vault_policy_document.longhorn_gcs_backup.hcl
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "vault_kubernetes_auth_backend_role" "longhorn" {
|
||||||
|
backend = data.vault_auth_backend.kubernetes.path
|
||||||
|
role_name = "longhorn"
|
||||||
|
bound_service_account_names = ["longhorn-vault-secret-reader"] # le meme que dans le manifest VaultAuth
|
||||||
|
bound_service_account_namespaces = ["longhorn-system"]
|
||||||
|
token_policies = [vault_policy.longhorn_gcs_backup.name]
|
||||||
|
audience = "vault"
|
||||||
|
alias_name_source = "serviceaccount_name"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
resource "random_password" "tofu" {
|
resource "random_password" "tofu" {
|
||||||
length = 32
|
length = 32
|
||||||
}
|
}
|
||||||
resource "gitea_user" "tofu" {
|
resource "gitea_user" "tofu" {
|
||||||
username = "tofu_module_reader"
|
username = "tofu_module_reader"
|
||||||
@@ -8,24 +8,24 @@ resource "gitea_user" "tofu" {
|
|||||||
password = random_password.tofu.result
|
password = random_password.tofu.result
|
||||||
email = "tofu-module-reader@arcodange.fake"
|
email = "tofu-module-reader@arcodange.fake"
|
||||||
must_change_password = false
|
must_change_password = false
|
||||||
full_name = "restricted CI user"
|
full_name = "restricted CI user"
|
||||||
prohibit_login = true
|
prohibit_login = false
|
||||||
restricted = true
|
restricted = true
|
||||||
visibility = "private"
|
visibility = "private"
|
||||||
}
|
}
|
||||||
resource "tls_private_key" "tofu" {
|
resource "tls_private_key" "tofu" {
|
||||||
algorithm = "ED25519"
|
algorithm = "ED25519"
|
||||||
}
|
}
|
||||||
resource "gitea_public_key" "tofu" {
|
resource "gitea_public_key" "tofu" {
|
||||||
title = "tofu"
|
title = "tofu"
|
||||||
key = tls_private_key.tofu.public_key_openssh
|
key = tls_private_key.tofu.public_key_openssh
|
||||||
username = gitea_user.tofu.username
|
username = gitea_user.tofu.username
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "vault_kv_secret" "gitea_admin_token" {
|
resource "vault_kv_secret" "gitea_admin_token" {
|
||||||
path = "kvv1/gitea/tofu_module_reader"
|
path = "kvv1/gitea/tofu_module_reader"
|
||||||
data_json = jsonencode({
|
data_json = jsonencode({
|
||||||
ssh_private_key = tls_private_key.tofu.private_key_openssh
|
ssh_private_key = tls_private_key.tofu.private_key_openssh
|
||||||
ssh_public_key = tls_private_key.tofu.public_key_openssh
|
ssh_public_key = tls_private_key.tofu.public_key_openssh
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
81
iac/modules/cloudflare_token/main.tf
Normal file
81
iac/modules/cloudflare_token/main.tf
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Récupère toutes les permissions Cloudflare disponibles
|
||||||
|
data "cloudflare_account_api_token_permission_groups_list" "all" {
|
||||||
|
account_id = var.account_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sélectionne uniquement les permissions demandées
|
||||||
|
locals {
|
||||||
|
# Simplifie le scope Cloudflare (ex: "account" depuis "com.cloudflare.api.account")
|
||||||
|
permission_map = {
|
||||||
|
for p in data.cloudflare_account_api_token_permission_groups_list.all.result :
|
||||||
|
"${split(".", p.scopes[0])[length(split(".", p.scopes[0])) - 1]}:${p.name}" => p.id
|
||||||
|
}
|
||||||
|
permission_map_from_id = zipmap(values(local.permission_map), keys(local.permission_map))
|
||||||
|
|
||||||
|
# Résout les permissions (si présentes) pour chaque catégorie
|
||||||
|
selected_account_permissions = var.permissions.account != null ? compact([
|
||||||
|
for name in var.permissions.account : lookup(local.permission_map, name, null)
|
||||||
|
]) : []
|
||||||
|
|
||||||
|
selected_bucket_permissions = var.bucket != null && try(var.permissions.bucket, null) != null ? compact([
|
||||||
|
for name in var.permissions.bucket : lookup(local.permission_map, name, null)
|
||||||
|
]) : []
|
||||||
|
|
||||||
|
# Validation des permissions introuvables
|
||||||
|
missing_permissions = concat(
|
||||||
|
[for name in coalesce(var.permissions.account, []) : name if lookup(local.permission_map, name, null) == null],
|
||||||
|
[for name in coalesce(var.permissions.bucket, []) : name if lookup(local.permission_map, name, null) == null]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ressources cibles
|
||||||
|
account_resource = {
|
||||||
|
"com.cloudflare.api.account.${var.account_id}" = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket_resource = var.bucket != null ? {
|
||||||
|
"com.cloudflare.edge.r2.bucket.${var.account_id}_${var.bucket.jurisdiction}_${var.bucket.name}" = "*"
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
# Policies construites dynamiquement
|
||||||
|
policies = [for policy in [
|
||||||
|
length(local.selected_account_permissions) > 0 ? {
|
||||||
|
effect = "allow"
|
||||||
|
permission_groups = [for id in local.selected_account_permissions : { id = id }]
|
||||||
|
resources = local.account_resource
|
||||||
|
} : null,
|
||||||
|
|
||||||
|
length(local.selected_bucket_permissions) > 0 ? {
|
||||||
|
effect = "allow"
|
||||||
|
permission_groups = [for id in local.selected_bucket_permissions : { id = id }]
|
||||||
|
resources = local.bucket_resource
|
||||||
|
} : null
|
||||||
|
] : policy if policy != null]
|
||||||
|
|
||||||
|
error_message = length(local.missing_permissions) > 0 ? format("Permissions introuvables : %s", join(", ", local.missing_permissions)) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Création du token
|
||||||
|
resource "cloudflare_account_token" "token" {
|
||||||
|
account_id = var.account_id
|
||||||
|
name = var.token_name
|
||||||
|
|
||||||
|
policies = local.policies
|
||||||
|
|
||||||
|
expires_on = null
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
ignore_changes = [expires_on, policies] # ignore permission id change as unstable
|
||||||
|
replace_triggered_by = [null_resource.cloudflare_account_token_replace] # replace permission name change d
|
||||||
|
precondition {
|
||||||
|
condition = length(local.missing_permissions) == 0
|
||||||
|
error_message = local.error_message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "null_resource" "cloudflare_account_token_replace" { # replace token when permission names change
|
||||||
|
triggers = {
|
||||||
|
"account_permissions" = sha256(join("", sort([for p_id in local.selected_account_permissions : lookup(local.permission_map_from_id, p_id)])))
|
||||||
|
"bucket_permissions" = sha256(join("", sort([for p_id in local.selected_bucket_permissions : lookup(local.permission_map_from_id, p_id)])))
|
||||||
|
}
|
||||||
|
}
|
||||||
35
iac/modules/cloudflare_token/outputs.tf
Normal file
35
iac/modules/cloudflare_token/outputs.tf
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
output "token" {
|
||||||
|
description = "Valeur du token Cloudflare"
|
||||||
|
value = cloudflare_account_token.token.value
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "token_id" {
|
||||||
|
description = "ID du token Cloudflare (sert de Access Key ID pour R2 si bucket défini)"
|
||||||
|
value = cloudflare_account_token.token.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "token_sha256" {
|
||||||
|
description = "SHA-256 du token Cloudflare (sert de Secret Access Key pour R2 si bucket défini)"
|
||||||
|
value = sha256(cloudflare_account_token.token.value)
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "r2_credentials" {
|
||||||
|
description = "Credentials R2 si bucket configuré (AccessKeyId, SecretAccessKey)"
|
||||||
|
value = var.bucket != null ? {
|
||||||
|
access_key_id = cloudflare_account_token.token.id
|
||||||
|
secret_access_key = sha256(cloudflare_account_token.token.value)
|
||||||
|
} : null
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "permissions" {
|
||||||
|
description = "Liste des permissions introuvables (si existantes)"
|
||||||
|
value = compact(concat(local.selected_account_permissions, local.selected_bucket_permissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
output "resources" {
|
||||||
|
description = "Map des resources assignées au token"
|
||||||
|
value = keys(merge(local.account_resource, local.bucket_resource))
|
||||||
|
}
|
||||||
37
iac/modules/cloudflare_token/variables.tf
Normal file
37
iac/modules/cloudflare_token/variables.tf
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
variable "account_id" {
|
||||||
|
description = "Cloudflare account ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "token_name" {
|
||||||
|
description = "Nom du token Cloudflare à créer"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "permissions" {
|
||||||
|
description = <<-EOT
|
||||||
|
Liste des permissions Cloudflare (ex: [\"Pages Deploy\", \"Zone DNS Edit\"])
|
||||||
|
you can check required permissions per service
|
||||||
|
https://developers.cloudflare.com/api/node/
|
||||||
|
EOT
|
||||||
|
type = object({
|
||||||
|
account = optional(list(string))
|
||||||
|
bucket = optional(list(string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bucket" {
|
||||||
|
description = <<-EOT
|
||||||
|
Objet optionnel représentant un bucket R2.
|
||||||
|
Exemple :
|
||||||
|
{
|
||||||
|
name = "mon-bucket"
|
||||||
|
jurisdiction = "eu"
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
type = object({
|
||||||
|
name = string
|
||||||
|
jurisdiction = string
|
||||||
|
})
|
||||||
|
default = null
|
||||||
|
}
|
||||||
57
iac/ovh.tf
Normal file
57
iac/ovh.tf
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
data "ovh_me" "account" {}
|
||||||
|
data "ovh_iam_reference_actions" "domain" {
|
||||||
|
type = "domain"
|
||||||
|
}
|
||||||
|
locals {
|
||||||
|
domain_read_permissions = [for a in data.ovh_iam_reference_actions.domain.actions : a if contains(a.categories, "READ")]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "ovh_me_api_oauth2_client" "cms" {
|
||||||
|
name = "cms repo"
|
||||||
|
description = "arcodange.fr management"
|
||||||
|
flow = "CLIENT_CREDENTIALS"
|
||||||
|
}
|
||||||
|
resource "ovh_iam_policy" "cms" {
|
||||||
|
name = "cms_manager"
|
||||||
|
description = "Permissions related to www.arcodange.fr domain"
|
||||||
|
identities = [ovh_me_api_oauth2_client.cms.identity]
|
||||||
|
resources = [
|
||||||
|
data.ovh_me.account.urn,
|
||||||
|
# ovh_me_api_oauth2_client.cms.identity,
|
||||||
|
"urn:v1:eu:resource:domain:arcodange.fr",
|
||||||
|
]
|
||||||
|
# these are all the actions
|
||||||
|
allow = concat([
|
||||||
|
"account:apiovh:me/get",
|
||||||
|
"account:apiovh:me/supportLevel/get",
|
||||||
|
"account:apiovh:me/certificates/get",
|
||||||
|
"account:apiovh:me/tag/get",
|
||||||
|
"account:apiovh:services/get",
|
||||||
|
],
|
||||||
|
local.domain_read_permissions[*].action,
|
||||||
|
[
|
||||||
|
"domain:apiovh:nameServer/edit",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "gitea_repository_actions_secret" "ovh_cms_client_id" {
|
||||||
|
repository = data.gitea_repo.cms.name
|
||||||
|
repository_owner = data.gitea_repo.cms.username
|
||||||
|
secret_name = "OVH_CLIENT_ID"
|
||||||
|
secret_value = ovh_me_api_oauth2_client.cms.client_id
|
||||||
|
}
|
||||||
|
resource "gitea_repository_actions_secret" "ovh_cms_client_secret" {
|
||||||
|
repository = data.gitea_repo.cms.name
|
||||||
|
repository_owner = data.gitea_repo.cms.username
|
||||||
|
secret_name = "OVH_CLIENT_SECRET"
|
||||||
|
secret_value = ovh_me_api_oauth2_client.cms.client_secret
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "vault_kv_secret" "ovh_cms_token" {
|
||||||
|
path = "kvv1/ovh/cms/app"
|
||||||
|
data_json = jsonencode({
|
||||||
|
client_id = ovh_me_api_oauth2_client.cms.client_id
|
||||||
|
client_secret = ovh_me_api_oauth2_client.cms.client_secret
|
||||||
|
urn = ovh_me_api_oauth2_client.cms.identity
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user