Plugins Personalizados¶
Ejemplo 1: Plugin que loguea todos los valores resueltos¶
Registra el estado completo de cada servicio despues de que los resolvers procesaron el YAML.
plugins/logger.js¶
module.exports = async function logger(parsed) {
console.log(`\n[logger] Stack: ${parsed.name}`);
console.log(`[logger] Services: ${Object.keys(parsed.services).join(', ')}`);
for (const [name, service] of Object.entries(parsed.services)) {
console.log(`\n[logger] --- ${name} ---`);
console.log(`[logger] image: ${service.image}`);
console.log(`[logger] provider: ${service.provider.name}`);
console.log(`[logger] replica: ${JSON.stringify(service.scale.replica)}`);
if (service.env) {
for (const [key, value] of Object.entries(service.env)) {
// Enmascara valores que parecen secretos
const masked = key.match(/SECRET|PASSWORD|TOKEN|KEY/i)
? value.slice(0, 4) + '****'
: value;
console.log(`[logger] env.${key}: ${masked}`);
}
}
if (service.ports) console.log(`[logger] ports: ${service.ports.join(', ')}`);
if (service.health) console.log(`[logger] health: ${service.health.command} (every ${service.health.interval}s)`);
}
console.log('');
};
Salida¶
[logger] Stack: my-app
[logger] Services: api, worker
[logger] --- api ---
[logger] image: ./Dockerfile
[logger] provider: aws
[logger] replica: [2,10]
[logger] env.NODE_ENV: production
[logger] env.DATABASE_URL: post****
[logger] env.JWT_SECRET: s3cr****
[logger] ports: 3000
[logger] health: curl -f http://localhost:3000/health (every 30s)
[logger] --- worker ---
[logger] image: ./Dockerfile
[logger] provider: aws
[logger] replica: 3
[logger] env.NODE_ENV: production
[logger] env.QUEUE_URL: https://sqs.us-east-1.amazonaws.com/...
Ejemplo 2: Plugin que valida la existencia de variables de entorno¶
Verifica que variables criticas no esten vacias ni sean null antes de desplegar.
plugins/require-env.js¶
module.exports = async function requireEnv(parsed) {
const required = {
api: ['DATABASE_URL', 'JWT_SECRET'],
worker: ['DATABASE_URL', 'QUEUE_URL'],
};
const errors = [];
for (const [name, keys] of Object.entries(required)) {
const service = parsed.services[name];
if (!service) continue;
for (const key of keys) {
const value = service.env?.[key];
if (value === undefined || value === null || value === '') {
errors.push(`Service "${name}": env.${key} is empty or missing`);
}
}
}
if (errors.length) {
throw new Error(`[require-env] Validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
}
console.log(`[require-env] All required env vars present`);
};
Error de ejemplo¶
Error: [require-env] Validation failed:
- Service "api": env.JWT_SECRET is empty or missing
- Service "worker": env.QUEUE_URL is empty or missing
Ejemplo 3: Plugin que agrega labels por defecto¶
Inyecta variables de entorno con metadata del despliegue a todos los servicios.
plugins/default-labels.js¶
module.exports = async function defaultLabels(parsed) {
const metadata = {
PCTL_STACK: parsed.name,
PCTL_DEPLOY_TIME: new Date().toISOString(),
PCTL_GIT_SHA: (() => {
try {
const { execSync } = require('child_process');
return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
} catch {
return 'unknown';
}
})(),
};
for (const [name, service] of Object.entries(parsed.services)) {
service.env = {
...metadata,
PCTL_SERVICE: name,
...(service.env ?? {}),
};
}
console.log(`[default-labels] Injected metadata (git: ${metadata.PCTL_GIT_SHA})`);
};
Resultado: cada contenedor tendra PCTL_STACK, PCTL_DEPLOY_TIME, PCTL_GIT_SHA y PCTL_SERVICE en sus variables de entorno. Las variables del usuario sobreescriben las del plugin porque se aplican despues en el spread.
Ejemplo 4: Plugin que modifica el escalado por horario¶
Reduce las replicas durante la noche para ahorrar costos.
plugins/time-scaler.js¶
module.exports = async function timeScaler(parsed) {
const hour = new Date().getHours();
const isNight = hour >= 22 || hour < 6;
const isWeekend = [0, 6].includes(new Date().getDay());
for (const [name, service] of Object.entries(parsed.services)) {
if (!Array.isArray(service.scale.replica)) continue;
const [min, max] = service.scale.replica;
if (isNight) {
service.scale.replica = [Math.max(min, 1), Math.ceil(max / 3)];
console.log(`[time-scaler] "${name}" night mode: [${service.scale.replica}]`);
} else if (isWeekend) {
service.scale.replica = [min, Math.ceil(max / 2)];
console.log(`[time-scaler] "${name}" weekend mode: [${service.scale.replica}]`);
}
}
if (!isNight && !isWeekend) {
console.log(`[time-scaler] Business hours, no changes`);
}
};
Comportamiento¶
| Horario | Replica original | Replica ajustada |
|---|---|---|
| Dia laboral | [2, 10] | [2, 10] (sin cambio) |
| Noche (22-06) | [2, 10] | [2, 4] (max / 3) |
| Fin de semana | [2, 10] | [2, 5] (max / 2) |
Solo afecta servicios con auto-scaling (replica como tupla). Los servicios con replicas fijas no se modifican.
YAML completo con todos los plugins¶
name: my-app
plugin:
- ./plugins/default-labels.js
- ./plugins/require-env.js
- ./plugins/time-scaler.js
- ./plugins/logger.js
custom:
cluster: prod-eks
namespace: app
ecr: 507738123456.dkr.ecr.us-east-1.amazonaws.com
services:
api:
image: ./services/api/Dockerfile
registry: ${self:custom.ecr}/api
env:
NODE_ENV: production
DATABASE_URL: ${ssm:/myapp/db-url}
JWT_SECRET: ${ssm:/myapp/jwt-secret}
scale:
replica: [2, 10]
cpu: 256m
memory: 512Mi
ports:
- 3000
health:
interval: 30
command: "curl -f http://localhost:3000/health"
provider:
name: aws
options:
cluster: ${self:custom.cluster}
namespace: ${self:custom.namespace}
worker:
image: ./services/worker/Dockerfile
registry: ${self:custom.ecr}/worker
env:
NODE_ENV: production
DATABASE_URL: ${ssm:/myapp/db-url}
QUEUE_URL: ${ssm:/myapp/queue-url}
scale:
replica: 3
cpu: 512m
memory: 1Gi
provider:
name: aws
options:
cluster: ${self:custom.cluster}
namespace: ${self:custom.namespace}
Orden de ejecucion¶
- resolve - Resuelve
${self:...},${ssm:...},${env:...} - validate - Verifica el schema Zod
- default-labels - Inyecta
PCTL_STACK,PCTL_DEPLOY_TIME,PCTL_GIT_SHA,PCTL_SERVICE - require-env - Verifica que
DATABASE_URLyJWT_SECRETexistan enapi, yDATABASE_URLyQUEUE_URLenworker - time-scaler - Ajusta replicas de
apisi es noche o fin de semana - logger - Imprime todo el estado resuelto y modificado
- aws - Despliega
apiyworkera EKS - docker - No hace nada (no hay servicios Docker)
- gcp - No hace nada (no hay servicios GCP)