Construcción de CI/CD Auto-hospedado con Rust y Kubernetes
Una guía completa para configurar tu propia infraestructura de CI/CD
![]()
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:
- Servidor Git: Gitea para hospedaje de repositorios
- Motor CI/CD: Tekton Pipelines en Kubernetes
- Almacenamiento de Artefactos: Registro de contenedores Harbor
- Monitoreo: Prometheus y Grafana
- 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:
- Empezar Simple: Comienza con pipelines básicos y agrega complejidad gradualmente
- Monitorear Todo: El monitoreo integral es crucial para la resolución de problemas
- Seguridad Primero: Implementa escaneo de seguridad y gestión de secretos desde el inicio
- 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.