Guía completa para implementar Rust en pipelines de CI/CD, automatización de infraestructura y herramientas DevOps
Rust es un lenguaje ideal para DevOps por su combinación única de:
¿Por qué Rust para DevOps? Herramientas críticas como Docker, Kubernetes, Terraform y otros están adoptando Rust para componentes donde el rendimiento y la seguridad son cruciales.
Área | Aplicación | Ventaja de Rust |
---|---|---|
Herramientas CLI | Automation scripts, custom tools | Rápido, binarios autocontenidos |
Procesamiento de datos | Log analysis, metrics processing | Rendimiento, concurrencia segura |
Infraestructura | Custom controllers, plugins | Seguridad, interoperabilidad |
Contenedores | Runtime tools, WASM | Pequeño footprint, seguridad |
Networking | Proxies, service mesh | Bajo latency, sin GC pauses |
# Instalar Rust y herramientas básicas
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
# Configurar entorno (para todos los usuarios)
echo 'export PATH="$HOME/.cargo/bin:$PATH"' | sudo tee /etc/profile.d/rust.sh
source /etc/profile.d/rust.sh
# Verificar instalación
rustc --version
cargo --version
FROM rust:1.60 as builder
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
cmake \
libssl-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
# Build de release optimizado
RUN cargo build --release
# Runtime image minimal
FROM debian:buster-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/my-tool /usr/local/bin/my-tool
CMD ["my-tool"]
# ~/.cargo/config.toml
[build]
# Para builds más rápidos en CI (usa más memoria)
incremental = true
codegen-units = 1
[target.x86_64-unknown-linux-gnu]
# Linker optimizado
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[install]
# Instalar en /opt/rust/bin para sistemas
prefix = "/opt/rust"
use clap::{App, Arg, SubCommand};
use serde_json::Value;
use reqwest::blocking::Client;
use std::process;
fn main() {
let matches = App::new("InfraCLI")
.version("1.0")
.about("Herramienta DevOps para gestión de infraestructura")
.subcommand(SubCommand::with_name("deploy")
.about("Despliega una aplicación")
.arg(Arg::with_name("app")
.help("Nombre de la aplicación")
.required(true))
.subcommand(SubCommand::with_name("status")
.about("Verifica el estado de los servicios")
.arg(Arg::with_name("service")
.help("Nombre del servicio")))
.get_matches();
match matches.subcommand() {
("deploy", Some(sub_m)) => {
let app = sub_m.value_of("app").unwrap();
deploy_app(app);
},
("status", Some(sub_m)) => {
let service = sub_m.value_of("service");
check_status(service);
},
_ => {
eprintln!("Comando no reconocido. Use --help para ayuda");
process::exit(1);
}
}
}
fn deploy_app(app: &str) {
let client = Client::new();
let response = client.post("https://api.infra.example.com/deploy")
.json(&serde_json::json!({ "app": app }))
.send();
match response {
Ok(res) if res.status().is_success() => {
println!("✅ Aplicación {} desplegada con éxito", app);
},
Ok(res) => {
eprintln!("❌ Error al desplegar: {}", res.status());
process::exit(1);
},
Err(e) => {
eprintln!("❌ Error de red: {}", e);
process::exit(1);
}
}
}
Crate | Uso | Ejemplo |
---|---|---|
clap |
Parseo de argumentos | CLIs complejas con subcomandos |
structopt |
CLI declarativa | Derive-based argument parsing |
indicatif |
Progress bars | Feedback visual en operaciones largas |
console |
Styling terminal | Colores, estilos, emojis |
anyhow |
Manejo de errores | Errores fáciles de usar/debug |
reqwest |
HTTP client | APIs REST/HTTP |
use rusoto_core::{Region, HttpClient};
use rusoto_credential::{StaticProvider, ProvideAwsCredentials};
use rusoto_ec2::{Ec2, Ec2Client, DescribeInstancesRequest};
#[tokio::main]
async fn main() {
// Configurar credenciales (mejor usar variables de entorno en producción)
let creds = StaticProvider::new_minimal(
"AKIAIOSFODNN7EXAMPLE".to_string(),
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
);
let client = Ec2Client::new_with(
HttpClient::new().expect("Error creando HTTP client"),
creds,
Region::UsEast1,
);
// Listar instancias EC2
let request = DescribeInstancesRequest::default();
match client.describe_instances(request).await {
Ok(output) => {
if let Some(reservations) = output.reservations {
println!("Instancias EC2 encontradas:");
for res in reservations {
if let Some(instances) = res.instances {
for instance in instances {
println!(
"ID: {}, Estado: {:?}, Tipo: {}",
instance.instance_id.unwrap_or_default(),
instance.state.map(|s| s.name.unwrap_or_default()),
instance.instance_type.unwrap_or_default()
);
}
}
}
}
},
Err(e) => eprintln!("Error al listar instancias: {}", e),
}
}
use std::process::{Command, Stdio};
use std::io::{self, Write};
use std::path::Path;
fn run_terraform(path: &str, command: &str, args: &[&str]) -> io::Result<()> {
if !Path::new(path).join("terraform.tf").exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"No se encontró terraform.tf",
));
}
let status = Command::new("terraform")
.current_dir(path)
.arg(command)
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if !status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Terraform {} falló", command),
));
}
Ok(())
}
fn main() -> io::Result<()> {
println!("Inicializando Terraform...");
run_terraform("./infra", "init", &[])?;
println!("Aplicando cambios...");
run_terraform("./infra", "apply", &["-auto-approve"])?;
println!("Infraestructura desplegada con éxito");
Ok(())
}
name: Rust CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Cache cargo
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build
run: cargo build --release
- name: Run tests
run: cargo test --release
- name: Lint
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Upload artifact
uses: actions/upload-artifact@v2
if: success()
with:
name: release-binary
path: target/release/my-tool
- name: Deploy to staging
if: github.ref == 'refs/heads/main'
run: |
scp target/release/my-tool user@staging-server:/opt/my-tool
ssh user@staging-server "systemctl restart my-tool"
use reqwest::blocking::Client;
use std::time::Duration;
#[test]
fn test_api_endpoint() {
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()
.expect("Error creando cliente HTTP");
let response = client.get("http://staging.example.com/health")
.send()
.expect("Error en la petición HTTP");
assert!(response.status().is_success(), "El endpoint de health check falló");
let health: serde_json::Value = response.json()
.expect("Error parseando JSON");
assert_eq!(
health["status"].as_str(),
Some("ok"),
"El estado no es 'ok'"
);
}
#[test]
fn test_database_connection() {
let conn = postgres::Client::connect(
"host=staging-db.example.com user=admin dbname=test",
postgres::NoTls,
).expect("Error conectando a la base de datos");
let rows = conn.query("SELECT 1 + 1 as result", &[])
.expect("Error en la consulta");
let sum: i32 = rows[0].get("result");
assert_eq!(sum, 2, "La consulta básica falló");
}
use bollard::Docker;
use bollard::container::{ListContainersOptions, StopContainerOptions};
use futures_util::stream::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box> {
// Conectar al socket de Docker
let docker = Docker::connect_with_unix_defaults()?;
// Listar contenedores
let containers = docker.list_containers(Some(ListContainersOptions:: {
all: true,
..Default::default()
})).await?;
println!("Contenedores en ejecución:");
for container in containers {
println!(
"ID: {}, Imagen: {}, Estado: {}",
container.id.unwrap_or_default(),
container.image.unwrap_or_default(),
container.status.unwrap_or_default()
);
}
// Detener un contenedor por ID
let container_id = "mi_contenedor";
docker.stop_container(
container_id,
Some(StopContainerOptions { t: 10 }),
).await?;
println!("Contenedor {} detenido", container_id);
Ok(())
}
# Stage 1: Builder
FROM rust:1.60 as builder
WORKDIR /app
COPY . .
# Build estático con musl para imagen minimal
RUN apt-get update && apt-get install -y musl-tools && \
rustup target add x86_64-unknown-linux-musl && \
cargo build --target x86_64-unknown-linux-musl --release
# Stage 2: Runtime image
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-app /my-app
# Certificados CA para conexiones TLS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/my-app"]
Tamaño de imagen resultante: ~5MB (vs ~2GB para imagen completa)
use actix_web::{web, App, HttpServer, Responder};
use prometheus::{opts, IntCounter, Registry, TextEncoder};
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref REQUEST_COUNTER: IntCounter = IntCounter::new(
"http_requests_total",
"Total HTTP requests"
).unwrap();
}
async fn metrics(registry: web::Data>) -> impl Responder {
let encoder = TextEncoder::new();
let metric_families = registry.lock().unwrap().gather();
encoder.encode_to_string(&metric_families).unwrap()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let registry = Registry::new();
registry.register(Box::new(REQUEST_COUNTER.clone())).unwrap();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(Mutex::new(registry.clone())))
.route("/metrics", web::get().to(metrics))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
use log::{info, error, warn};
use serde_json::json;
use syslog::{Facility, Formatter3164};
fn setup_logging() {
let formatter = Formatter3164 {
facility: Facility::LOG_DAEMON,
hostname: None,
process: "my-devops-tool".into(),
pid: std::process::id(),
};
// Log a syslog
syslog::init(formatter, log::LevelFilter::Info, None)
.expect("Error inicializando syslog");
// También a archivo con JSON estructurado
let file = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open("/var/log/my-devops-tool.log")
.expect("Error abriendo archivo de log");
let logger = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}",
json!({
"timestamp": chrono::Local::now().to_rfc3339(),
"level": record.level().to_string(),
"message": message.to_string(),
"target": record.target(),
"module": record.module_path().unwrap_or_default(),
"line": record.line().unwrap_or_default(),
})
))
})
.chain(file)
.apply();
logger.expect("Error configurando logger");
}
fn main() {
setup_logging();
info!("Iniciando herramienta DevOps");
warn!("Configuración no óptima detectada");
error!("Error crítico en módulo X");
}
name: Security Audit
on:
schedule:
- cron: '0 0 * * *' # Diario a medianoche
pull_request:
branches: [ main ]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run security audit
run: cargo audit
Para ejecutar localmente:
cargo install cargo-audit
cargo audit
use secrecy::{Secret, ExposeSecret};
use dotenv::dotenv;
use std::env;
struct DatabaseConfig {
host: String,
username: String,
password: Secret,
}
impl DatabaseConfig {
fn from_env() -> Result {
dotenv().ok(); // Cargar .env
Ok(Self {
host: env::var("DB_HOST")?,
username: env::var("DB_USER")?,
password: Secret::new(env::var("DB_PASS")?),
})
}
}
fn connect_to_db(config: &DatabaseConfig) {
// Nunca imprimas el password directamente!
println!("Conectando a {} como {}", config.host, config.username);
// Acceso seguro al secreto
let password = config.password.expose_secret();
// ... lógica de conexión ...
// El secreto se borrará de memoria cuando salga del ámbito
}
fn main() {
let db_config = DatabaseConfig::from_env()
.expect("Error cargando configuración de DB");
connect_to_db(&db_config);
}
Archivo .env:
DB_HOST=db.example.com
DB_USER=admin
DB_PASS=supersecret123
Herramienta | Descripción | Enlace |
---|---|---|
exa | Reemplazo moderno para ls | GitHub |
bat | Clone de cat con syntax highlighting | GitHub |
starship | Prompt de terminal personalizable | GitHub |
bottom | Monitor de sistema alternativo a top | GitHub |
dust | Alternativa más intuitiva a du | GitHub |
use actix_web::{get, App, HttpServer, Responder};
use sysinfo::{System, SystemExt, CpuExt};
#[get("/metrics")]
async fn metrics() -> impl Responder {
let mut sys = System::new_all();
sys.refresh_all();
let cpu_usage = sys.global_cpu_info().cpu_usage();
let used_memory = sys.used_memory();
let total_memory = sys.total_memory();
format!(
"cpu_usage {}\nmemory_used {}\nmemory_total {}",
cpu_usage, used_memory, total_memory
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(metrics)
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Configuración de Prometheus para scrapear esta aplicación:
scrape_configs:
- job_name: 'rust_monitor'
static_configs:
- targets: ['localhost:8080']
Crate | Descripción |
---|---|
tokio |
Runtime async para aplicaciones de red |
reqwest |
Cliente HTTP async/sync |
serde |
Serialización/deserialización |
anyhow |
Manejo simple de errores |
thiserror |
Errores personalizados |
clap |
Parseo de argumentos CLI |
sysinfo |
Información del sistema |
bollard |
API Docker |
Consejo final: Comienza integrando Rust en pequeñas partes de tu workflow DevOps, como scripts de automatización o herramientas CLI. A medida que ganes confianza, podrás usarlo para componentes más críticos.