feat(infra): IaC pipeline, tfstate migration, and drift detection
- Migrate tfstate backend from nl-ams to fr-par - Add missing resources to TF: sccache bucket, foxhunt-ci registry, grafana + prometheus DNS records - Add infra-runner Dockerfile (terraform + terragrunt + scw + glab) - Add CI jobs: infra-plan (MR), infra-apply (merge), drift-check (weekly) - All IaC jobs run on gitlab pool (zero extra cost) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
123
.gitlab-ci.yml
123
.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)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
46
infra/docker/Dockerfile.infra-runner
Normal file
46
infra/docker/Dockerfile.infra-runner
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user