From c80899066c9127d3fc7684b16698bcb770b31ee3 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:46:39 +0100 Subject: [PATCH 1/7] infra(registry): add foxhunt-ci namespace to Terraform Co-Authored-By: Claude Opus 4.6 --- infra/modules/registry/main.tf | 6 ++++++ infra/modules/registry/outputs.tf | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/infra/modules/registry/main.tf b/infra/modules/registry/main.tf index f688d8a19..c43c9fc8c 100644 --- a/infra/modules/registry/main.tf +++ b/infra/modules/registry/main.tf @@ -3,3 +3,9 @@ resource "scaleway_registry_namespace" "foxhunt" { region = var.region is_public = false } + +resource "scaleway_registry_namespace" "foxhunt_ci" { + name = "${var.namespace_name}-ci" + region = var.region + is_public = false +} diff --git a/infra/modules/registry/outputs.tf b/infra/modules/registry/outputs.tf index 16f15992a..84a21a8b7 100644 --- a/infra/modules/registry/outputs.tf +++ b/infra/modules/registry/outputs.tf @@ -7,3 +7,13 @@ output "namespace_id" { description = "Registry namespace ID" value = scaleway_registry_namespace.foxhunt.id } + +output "ci_endpoint" { + description = "CI registry endpoint URL" + value = scaleway_registry_namespace.foxhunt_ci.endpoint +} + +output "ci_namespace_id" { + description = "CI registry namespace ID" + value = scaleway_registry_namespace.foxhunt_ci.id +} From b321fa47a70c0e80ec8c041d32e6550f8a1dd9e6 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:46:45 +0100 Subject: [PATCH 2/7] infra(object-storage): add sccache bucket to Terraform Co-Authored-By: Claude Opus 4.6 --- infra/modules/object-storage/main.tf | 18 ++++++++++++++++++ infra/modules/object-storage/outputs.tf | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/infra/modules/object-storage/main.tf b/infra/modules/object-storage/main.tf index 1d4c386d0..2e8da1fe0 100644 --- a/infra/modules/object-storage/main.tf +++ b/infra/modules/object-storage/main.tf @@ -42,3 +42,21 @@ resource "scaleway_object_bucket" "gitlab_artifacts" { enabled = false } } + +resource "scaleway_object_bucket" "sccache" { + name = "${var.bucket_name_prefix}-sccache" + region = var.region + + lifecycle_rule { + enabled = true + prefix = "" + + expiration { + days = 14 + } + } + + versioning { + enabled = false + } +} diff --git a/infra/modules/object-storage/outputs.tf b/infra/modules/object-storage/outputs.tf index 35d7c88bd..2b6e70263 100644 --- a/infra/modules/object-storage/outputs.tf +++ b/infra/modules/object-storage/outputs.tf @@ -22,3 +22,8 @@ output "gitlab_artifacts_bucket_name" { description = "Name of the GitLab artifacts bucket" value = scaleway_object_bucket.gitlab_artifacts.name } + +output "sccache_bucket_name" { + description = "Name of the sccache bucket" + value = scaleway_object_bucket.sccache.name +} From d271315e7ea8b39ae2f9f44ac26108d9d7f67e03 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:46:56 +0100 Subject: [PATCH 3/7] infra(dns): add grafana + prometheus A records to Terraform Co-Authored-By: Claude Opus 4.6 --- infra/modules/dns/main.tf | 16 ++++++++++++++++ infra/modules/dns/outputs.tf | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/infra/modules/dns/main.tf b/infra/modules/dns/main.tf index f6165e2b6..132232222 100644 --- a/infra/modules/dns/main.tf +++ b/infra/modules/dns/main.tf @@ -7,3 +7,19 @@ resource "scaleway_domain_record" "git" { data = var.git_ip ttl = 300 } + +resource "scaleway_domain_record" "grafana" { + dns_zone = var.dns_zone + name = "grafana" + type = "A" + data = var.git_ip + ttl = 300 +} + +resource "scaleway_domain_record" "prometheus" { + dns_zone = var.dns_zone + name = "prometheus" + type = "A" + data = var.git_ip + ttl = 300 +} diff --git a/infra/modules/dns/outputs.tf b/infra/modules/dns/outputs.tf index 9473c4078..8d57146c3 100644 --- a/infra/modules/dns/outputs.tf +++ b/infra/modules/dns/outputs.tf @@ -2,3 +2,13 @@ output "git_fqdn" { description = "FQDN for the GitLab instance" value = "${scaleway_domain_record.git.name}.${scaleway_domain_record.git.dns_zone}" } + +output "grafana_fqdn" { + description = "FQDN for the Grafana instance" + value = "${scaleway_domain_record.grafana.name}.${scaleway_domain_record.grafana.dns_zone}" +} + +output "prometheus_fqdn" { + description = "FQDN for the Prometheus instance" + value = "${scaleway_domain_record.prometheus.name}.${scaleway_domain_record.prometheus.dns_zone}" +} From 3f4ae18a4c0341cabb7b8f4971b654e03af345e6 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:47:59 +0100 Subject: [PATCH 4/7] infra: add Dockerfile for IaC CI runner (terraform + terragrunt + scw + glab) Co-Authored-By: Claude Opus 4.6 --- infra/docker/Dockerfile.infra-runner | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 infra/docker/Dockerfile.infra-runner diff --git a/infra/docker/Dockerfile.infra-runner b/infra/docker/Dockerfile.infra-runner new file mode 100644 index 000000000..348fb8e0d --- /dev/null +++ b/infra/docker/Dockerfile.infra-runner @@ -0,0 +1,46 @@ +# Lightweight IaC runner: Terraform + Terragrunt + Scaleway CLI + glab +FROM alpine:3.21 + +ARG TERRAFORM_VERSION=1.11.2 +ARG TERRAGRUNT_VERSION=0.72.6 +ARG SCW_CLI_VERSION=2.52.0 +ARG GLAB_VERSION=1.55.0 + +# Install base dependencies +RUN apk add --no-cache \ + bash \ + curl \ + git \ + jq \ + openssh-client \ + unzip \ + aws-cli + +# Install Terraform (OpenTofu-compatible) +RUN curl -fsSL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ + -o /tmp/terraform.zip && \ + unzip /tmp/terraform.zip -d /usr/local/bin/ && \ + rm /tmp/terraform.zip && \ + terraform version + +# Install Terragrunt +RUN curl -fsSL "https://github.com/gruntwork-io/terragrunt/releases/download/v${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" \ + -o /usr/local/bin/terragrunt && \ + chmod +x /usr/local/bin/terragrunt && \ + terragrunt --version + +# Install Scaleway CLI +RUN curl -fsSL "https://github.com/scaleway/scaleway-cli/releases/download/v${SCW_CLI_VERSION}/scaleway-cli_${SCW_CLI_VERSION}_linux_amd64" \ + -o /usr/local/bin/scw && \ + chmod +x /usr/local/bin/scw && \ + scw version + +# Install glab (GitLab CLI) +RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_amd64.tar.gz" \ + -o /tmp/glab.tar.gz && \ + tar -xzf /tmp/glab.tar.gz -C /tmp/ && \ + mv /tmp/bin/glab /usr/local/bin/glab && \ + rm -rf /tmp/glab.tar.gz /tmp/bin && \ + glab version + +WORKDIR /app From 9d20c3c97c4a4292fc6ef123ba4ce316de8300ec Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:48:33 +0100 Subject: [PATCH 5/7] infra: migrate tfstate backend from nl-ams to fr-par Co-Authored-By: Claude Opus 4.6 --- infra/live/production/root.hcl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/live/production/root.hcl b/infra/live/production/root.hcl index 9dede28b5..af45e3b3c 100644 --- a/infra/live/production/root.hcl +++ b/infra/live/production/root.hcl @@ -8,7 +8,7 @@ locals { } # Remote state in Scaleway Object Storage (S3-compatible) -# Bootstrap: scw object bucket create name=foxhunt-tfstate region=nl-ams +# Bootstrap: scw object bucket create name=foxhunt-tfstate region=fr-par remote_state { backend = "s3" generate = { @@ -18,11 +18,11 @@ remote_state { config = { bucket = "foxhunt-tfstate" key = "${path_relative_to_include()}/terraform.tfstate" - region = "nl-ams" + region = "fr-par" # Scaleway S3 endpoint endpoints = { - s3 = "https://s3.nl-ams.scw.cloud" + s3 = "https://s3.fr-par.scw.cloud" } skip_credentials_validation = true skip_metadata_api_check = true From 1caeb0b0d68e3f8b51ccb3879c58d1acd57a2749 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:48:57 +0100 Subject: [PATCH 6/7] ci: add IaC pipeline (plan on MR, apply on merge, weekly drift check) Co-Authored-By: Claude Opus 4.6 --- .gitlab-ci.yml | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84e1bda32..de22ad97c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,6 +29,8 @@ variables: CI_BUILDER_IMAGE: rg.fr-par.scw.cloud/foxhunt-ci/ci-builder:latest # Scaleway Container Registry for service images REGISTRY: rg.fr-par.scw.cloud/foxhunt-ci + # IaC runner image on Scaleway Container Registry + INFRA_RUNNER_IMAGE: rg.fr-par.scw.cloud/foxhunt-ci/infra-runner:latest # -------------------------------------------------------------------------- # Stage 0: Build CI builder image → push to Scaleway CR @@ -86,6 +88,34 @@ build-devcontainer: --destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:${CI_COMMIT_SHA}" --destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:latest" +# -------------------------------------------------------------------------- +# Stage 0c: Build infra-runner image → push to Scaleway CR +# -------------------------------------------------------------------------- +build-infra-runner: + stage: prepare + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + tags: + - kapsule + - docker + rules: + - if: $CI_PIPELINE_SOURCE == "push" + changes: + - infra/docker/Dockerfile.infra-runner + when: on_success + - when: manual + allow_failure: true + before_script: + - mkdir -p /kaniko/.docker + - | + echo "{\"auths\":{\"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}}}" > /kaniko/.docker/config.json + script: + - /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/infra/docker/Dockerfile.infra-runner" + --destination "${INFRA_RUNNER_IMAGE}" + # Base template for Rust jobs — pre-baked CI builder from Scaleway CR # Image pre-exists in SCR; rebuild via build-ci-builder when Dockerfile changes .rust-base: @@ -269,6 +299,99 @@ build-training: --destination "${REGISTRY}/training:${CI_COMMIT_SHA}" --destination "${REGISTRY}/training:latest" +# -------------------------------------------------------------------------- +# IaC: Terragrunt plan on MR (runs on gitlab pool) +# -------------------------------------------------------------------------- +infra-plan: + stage: check + image: ${INFRA_RUNNER_IMAGE} + tags: + - kapsule + needs: [] + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + - infra/** + before_script: + - export SCW_ACCESS_KEY=${SCW_ACCESS_KEY} + - export SCW_SECRET_KEY=${SCW_SECRET_KEY} + - export SCW_DEFAULT_PROJECT_ID=${SCW_DEFAULT_PROJECT_ID} + - export AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} + - export AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} + script: + - cd infra/live/production + - terragrunt run-all plan --terragrunt-non-interactive 2>&1 | tee /tmp/plan-output.txt + - echo "Plan completed successfully" + artifacts: + paths: + - /tmp/plan-output.txt + when: always + expire_in: 7 days + +# -------------------------------------------------------------------------- +# IaC: Terragrunt apply on merge to main (runs on gitlab pool) +# -------------------------------------------------------------------------- +infra-apply: + stage: deploy + image: ${INFRA_RUNNER_IMAGE} + tags: + - kapsule + needs: [] + rules: + - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push" + changes: + - infra/** + before_script: + - export SCW_ACCESS_KEY=${SCW_ACCESS_KEY} + - export SCW_SECRET_KEY=${SCW_SECRET_KEY} + - export SCW_DEFAULT_PROJECT_ID=${SCW_DEFAULT_PROJECT_ID} + - export AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} + - export AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} + script: + - cd infra/live/production + - terragrunt run-all apply --terragrunt-non-interactive -auto-approve + environment: + name: production/infrastructure + +# -------------------------------------------------------------------------- +# IaC: Weekly drift detection (scheduled pipeline, gitlab pool) +# -------------------------------------------------------------------------- +infra-drift-check: + stage: check + image: ${INFRA_RUNNER_IMAGE} + tags: + - kapsule + needs: [] + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" && $DRIFT_CHECK == "true" + before_script: + - export SCW_ACCESS_KEY=${SCW_ACCESS_KEY} + - export SCW_SECRET_KEY=${SCW_SECRET_KEY} + - export SCW_DEFAULT_PROJECT_ID=${SCW_DEFAULT_PROJECT_ID} + - export AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} + - export AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} + script: + - cd infra/live/production + - | + if ! terragrunt run-all plan -detailed-exitcode --terragrunt-non-interactive 2>&1 | tee /tmp/drift-output.txt; then + EXIT_CODE=${PIPESTATUS[0]} + if [ "$EXIT_CODE" -eq 2 ]; then + echo "DRIFT DETECTED — creating GitLab issue" + glab issue create \ + --title "IaC Drift Detected ($(date +%Y-%m-%d))" \ + --description "$(cat /tmp/drift-output.txt | tail -100)" \ + --label "infrastructure,drift" + exit 1 + fi + exit $EXIT_CODE + fi + echo "No drift detected" + artifacts: + paths: + - /tmp/drift-output.txt + when: always + expire_in: 7 days + # -------------------------------------------------------------------------- # Stage 4: Deploy to Kapsule (main only) # -------------------------------------------------------------------------- From 980489edc57c621dfc5a77aadaaa8811bb2c2a0f Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 25 Feb 2026 21:51:46 +0100 Subject: [PATCH 7/7] fix(ci): use relative artifact paths and avoid PIPESTATUS in drift check - Artifact paths changed from /tmp/ to ${CI_PROJECT_DIR}/ for GitLab k8s executor compatibility - Drift check uses $? instead of PIPESTATUS (Alpine sh doesn't support bash arrays) Co-Authored-By: Claude Opus 4.6 --- .gitlab-ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de22ad97c..3d0292f99 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -320,11 +320,11 @@ infra-plan: - export AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} script: - cd infra/live/production - - terragrunt run-all plan --terragrunt-non-interactive 2>&1 | tee /tmp/plan-output.txt + - terragrunt run-all plan --terragrunt-non-interactive 2>&1 | tee ${CI_PROJECT_DIR}/plan-output.txt - echo "Plan completed successfully" artifacts: paths: - - /tmp/plan-output.txt + - plan-output.txt when: always expire_in: 7 days @@ -373,22 +373,22 @@ infra-drift-check: script: - cd infra/live/production - | - if ! terragrunt run-all plan -detailed-exitcode --terragrunt-non-interactive 2>&1 | tee /tmp/drift-output.txt; then - EXIT_CODE=${PIPESTATUS[0]} - if [ "$EXIT_CODE" -eq 2 ]; then - echo "DRIFT DETECTED — creating GitLab issue" - glab issue create \ - --title "IaC Drift Detected ($(date +%Y-%m-%d))" \ - --description "$(cat /tmp/drift-output.txt | tail -100)" \ - --label "infrastructure,drift" - exit 1 - fi + terragrunt run-all plan -detailed-exitcode --terragrunt-non-interactive \ + 2>&1 | tee ${CI_PROJECT_DIR}/drift-output.txt; EXIT_CODE=$? + if [ "$EXIT_CODE" -eq 2 ]; then + echo "DRIFT DETECTED — creating GitLab issue" + glab issue create \ + --title "IaC Drift Detected ($(date +%Y-%m-%d))" \ + --description "$(tail -100 ${CI_PROJECT_DIR}/drift-output.txt)" \ + --label "infrastructure,drift" + exit 1 + elif [ "$EXIT_CODE" -ne 0 ]; then exit $EXIT_CODE fi echo "No drift detected" artifacts: paths: - - /tmp/drift-output.txt + - drift-output.txt when: always expire_in: 7 days