diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84e1bda32..3d0292f99 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 ${CI_PROJECT_DIR}/plan-output.txt + - echo "Plan completed successfully" + artifacts: + paths: + - 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 + - | + 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: + - drift-output.txt + when: always + expire_in: 7 days + # -------------------------------------------------------------------------- # Stage 4: Deploy to Kapsule (main only) # -------------------------------------------------------------------------- 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 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 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}" +} 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 +} 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 +}