Construcción de CI/CD Auto-hospedado con Rust y Kubernetes

Una guía completa para configurar tu propia infraestructura de CI/CD

Jesús Pérez Lorenzo 12 min read
Una guía completa para configurar tu propia infraestructura de CI/CD usando herramientas basadas en Rust, Kubernetes y alternativas de código abierto a servicios en la nube. Aprende cómo mantener control sobre tu pipeline de desarrollo mientras reduces dependencias en servicios externos.

Construcción de CI/CD Auto-hospedado con Rust y Kubernetes

Construcción de CI/CD Auto-hospedado con Rust y Kubernetes

En el mundo actual que prioriza la nube, muchos equipos de desarrollo dependen en gran medida de servicios de CI/CD gestionados. Aunque estos servicios ofrecen conveniencia, también introducen dependencias en proveedores externos y pueden volverse costosos a medida que los equipos escalan. Esta guía explora cómo construir un pipeline de CI/CD robusto y auto-hospedado usando herramientas basadas en Rust y Kubernetes.

¿Por qué CI/CD Auto-hospedado?

El auto-hospedaje de tu infraestructura de CI/CD ofrece varias ventajas:

  • Control: Control total sobre tu entorno de construcción y datos
  • Costo: Costos predecibles que escalan con tu infraestructura, no con tu uso
  • Seguridad: Tu código y artefactos nunca salen de tu infraestructura
  • Personalización: Flexibilidad completa para personalizar tu pipeline
  • Cumplimiento: Más fácil cumplir con requisitos regulatorios

Visión General de la Arquitectura

Nuestra solución de CI/CD auto-hospedada consiste en:

  1. Servidor Git: Gitea para hospedaje de repositorios
  2. Motor CI/CD: Tekton Pipelines en Kubernetes
  3. Almacenamiento de Artefactos: Registro de contenedores Harbor
  4. Monitoreo: Prometheus y Grafana
  5. Escaneo de Seguridad: Trivy para escaneo de vulnerabilidades

Configurando la Base

Configuración del Clúster Kubernetes

Primero, asegúrate de tener un clúster Kubernetes. Para esta guía, asumiremos que tienes un clúster con al menos 3 nodos y recursos suficientes.

# cluster-config.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ci-cd
---
apiVersion: v1
kind: Namespace
metadata:
  name: monitoring

Instalando Tekton Pipelines

Tekton proporciona capacidades de CI/CD nativas de la nube:

# Instalar Tekton Pipelines
kubectl apply --filename https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml

# Instalar Tekton Dashboard
kubectl apply --filename https://storage.googleapis.com/tekton-releases/dashboard/latest/tekton-dashboard-release.yaml

Herramientas de Construcción Basadas en Rust

Controlador de Pipeline Personalizado

Crearemos un controlador basado en Rust para gestionar nuestros pipelines:

use k8s_openapi::api::core::v1::Pod;
use kube::{Api, Client, ResourceExt};
use serde::{Deserialize, Serialize};
use tokio;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BuildRequest {
    pub repository: String,
    pub branch: String,
    pub commit_sha: String,
}

pub struct PipelineController {
    client: Client,
}

impl PipelineController {
    pub fn new(client: Client) -> Self {
        Self { client }
    }

    pub async fn trigger_build(&self, request: BuildRequest) -> Result<String, Box<dyn std::error::Error>> {
        // Crear TaskRun de Tekton desde solicitud de construcción
        let task_run = self.create_task_run(&request).await?;

        // Enviar a Kubernetes
        let api: Api<Pod> = Api::namespaced(self.client.clone(), "ci-cd");
        let result = api.create(&Default::default(), &task_run).await?;

        Ok(result.name_any())
    }

    async fn create_task_run(&self, request: &BuildRequest) -> Result<Pod, Box<dyn std::error::Error>> {
        // Implementación para crear TaskRun de Tekton
        todo!("Implementar creación de TaskRun")
    }
}

Agente de Construcción

Un agente ligero en Rust para ejecutar pasos de construcción:

use std::process::Command;
use tokio::fs;
use tracing::{info, error};

pub struct BuildAgent {
    workspace: String,
}

impl BuildAgent {
    pub fn new(workspace: String) -> Self {
        Self { workspace }
    }

    pub async fn clone_repository(&self, repo_url: &str, branch: &str) -> Result<(), Box<dyn std::error::Error>> {
        info!("Clonando repositorio: {} rama: {}", repo_url, branch);

        let output = Command::new("git")
            .args(&["clone", "--branch", branch, repo_url, &self.workspace])
            .output()?;

        if !output.status.success() {
            error!("Falló al clonar repositorio: {}", String::from_utf8_lossy(&output.stderr));
            return Err("Clonado git falló".into());
        }

        Ok(())
    }

    pub async fn run_tests(&self) -> Result<(), Box<dyn std::error::Error>> {
        info!("Ejecutando pruebas");

        let output = Command::new("cargo")
            .args(&["test", "--release"])
            .current_dir(&self.workspace)
            .output()?;

        if !output.status.success() {
            error!("Las pruebas fallaron: {}", String::from_utf8_lossy(&output.stderr));
            return Err("Las pruebas fallaron".into());
        }

        Ok(())
    }

    pub async fn build_container(&self, image_name: &str) -> Result<(), Box<dyn std::error::Error>> {
        info!("Construyendo imagen de contenedor: {}", image_name);

        let output = Command::new("docker")
            .args(&["build", "-t", image_name, "."])
            .current_dir(&self.workspace)
            .output()?;

        if !output.status.success() {
            error!("La construcción del contenedor falló: {}", String::from_utf8_lossy(&output.stderr));
            return Err("La construcción del contenedor falló".into());
        }

        Ok(())
    }
}

Definiciones de Pipeline

Pipeline Básico para Aplicación Rust

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: rust-app-pipeline
  namespace: ci-cd
spec:
  params:
    - name: repo-url
      type: string
    - name: branch
      type: string
      default: main
    - name: image-name
      type: string
  workspaces:
    - name: shared-data
  tasks:
    - name: fetch-source
      taskRef:
        name: git-clone
      workspaces:
        - name: output
          workspace: shared-data
      params:
        - name: url
          value: $(params.repo-url)
        - name: revision
          value: $(params.branch)

    - name: test
      runAfter: ["fetch-source"]
      taskRef:
        name: rust-test
      workspaces:
        - name: source
          workspace: shared-data

    - name: build
      runAfter: ["test"]
      taskRef:
        name: rust-build
      workspaces:
        - name: source
          workspace: shared-data
      params:
        - name: image
          value: $(params.image-name)

    - name: security-scan
      runAfter: ["build"]
      taskRef:
        name: trivy-scan
      params:
        - name: image
          value: $(params.image-name)

    - name: deploy-staging
      runAfter: ["security-scan"]
      taskRef:
        name: kubectl-deploy
      params:
        - name: image
          value: $(params.image-name)
        - name: environment
          value: staging

Tarea de Construcción Rust Personalizada

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: rust-build
  namespace: ci-cd
spec:
  params:
    - name: image
      type: string
  workspaces:
    - name: source
  steps:
    - name: cargo-build
      image: rust:1.75
      workingDir: $(workspaces.source.path)
      script: |
        #!/bin/bash
        set -e

        # Cachear dependencias
        cargo fetch

        # Ejecutar clippy para linting
        cargo clippy -- -D warnings

        # Construir binario de release
        cargo build --release

        # Ejecutar auditoría de seguridad
        cargo audit

    - name: container-build
      image: gcr.io/kaniko-project/executor:latest
      workingDir: $(workspaces.source.path)
      env:
        - name: DOCKER_CONFIG
          value: /kaniko/.docker
      command:
        - /kaniko/executor
        - --dockerfile=./Dockerfile
        - --destination=$(params.image)
        - --cache=true

Monitoreo y Observabilidad

Recolector de Métricas en Rust

use prometheus::{Counter, Histogram, IntGauge, Registry, Encoder, TextEncoder};
use std::collections::HashMap;
use warp::Filter;

pub struct MetricsCollector {
    registry: Registry,
    build_counter: Counter,
    build_duration: Histogram,
    active_builds: IntGauge,
}

impl MetricsCollector {
    pub fn new() -> Self {
        let registry = Registry::new();

        let build_counter = Counter::new("ci_builds_total", "Número total de construcciones")
            .expect("Falló al crear contador");

        let build_duration = Histogram::new("ci_build_duration_seconds", "Duración de construcción en segundos")
            .expect("Falló al crear histograma");

        let active_builds = IntGauge::new("ci_active_builds", "Número de construcciones activas")
            .expect("Falló al crear gauge");

        registry.register(Box::new(build_counter.clone())).unwrap();
        registry.register(Box::new(build_duration.clone())).unwrap();
        registry.register(Box::new(active_builds.clone())).unwrap();

        Self {
            registry,
            build_counter,
            build_duration,
            active_builds,
        }
    }

    pub fn increment_builds(&self) {
        self.build_counter.inc();
    }

    pub fn observe_build_duration(&self, duration: f64) {
        self.build_duration.observe(duration);
    }

    pub fn set_active_builds(&self, count: i64) {
        self.active_builds.set(count);
    }

    pub async fn metrics_handler(&self) -> Result<impl warp::Reply, warp::Rejection> {
        let encoder = TextEncoder::new();
        let metric_families = self.registry.gather();
        let mut buffer = Vec::new();
        encoder.encode(&metric_families, &mut buffer).unwrap();

        Ok(warp::reply::with_header(
            String::from_utf8(buffer).unwrap(),
            "content-type",
            "text/plain; version=0.0.4",
        ))
    }
}

Consideraciones de Seguridad

Gestión de Secretos

use k8s_openapi::api::core::v1::Secret;
use kube::{Api, Client};
use base64::{Engine as _, engine::general_purpose};

pub struct SecretManager {
    client: Client,
    namespace: String,
}

impl SecretManager {
    pub fn new(client: Client, namespace: String) -> Self {
        Self { client, namespace }
    }

    pub async fn get_secret(&self, name: &str, key: &str) -> Result<String, Box<dyn std::error::Error>> {
        let api: Api<Secret> = Api::namespaced(self.client.clone(), &self.namespace);
        let secret = api.get(name).await?;

        if let Some(data) = secret.data {
            if let Some(value) = data.get(key) {
                let decoded = general_purpose::STANDARD.decode(&value.0)?;
                return Ok(String::from_utf8(decoded)?);
            }
        }

        Err(format!("Clave de secreto {} no encontrada en {}", key, name).into())
    }
}

Despliegue y Escalado

Auto-escalado de Agentes de Construcción

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: build-agent-hpa
  namespace: ci-cd
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: build-agent
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Pods
      pods:
        metric:
          name: pending_builds
        target:
          type: AverageValue
          averageValue: "2"

Conclusión

Construir un pipeline de CI/CD auto-hospedado con Rust y Kubernetes proporciona excelente control, rendimiento y beneficios de costo. El ecosistema de Rust ofrece herramientas poderosas para construir componentes de CI/CD personalizados que son tanto rápidos como confiables.

Puntos clave:

  1. Empezar Simple: Comienza con pipelines básicos y agrega complejidad gradualmente
  2. Monitorear Todo: El monitoreo integral es crucial para la resolución de problemas
  3. Seguridad Primero: Implementa escaneo de seguridad y gestión de secretos desde el inicio
  4. Escalar Gradualmente: Usa auto-escalado para manejar cargas de construcción variables de manera eficiente

La combinación de las garantías de rendimiento y seguridad de Rust con las capacidades de orquestación de Kubernetes crea una base poderosa para las necesidades de CI/CD de cualquier equipo de desarrollo.


¿Preguntas o comentarios? No dudes en contactarme en LinkedIn o GitHub.

Usamos cookies para que este sitio funcione, entender el uso del servicio y apoyar acciones de marketing. Política de cookies para más información.