DevOps desde Cero: Secretos, Configuracion y Manejo de Entornos

2026-05-15 | Gabriel Garrido | 20 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introduccion

Bienvenido al articulo nueve de la serie DevOps desde Cero. En el articulo anterior deployeamos nuestra API TypeScript a ECS con Fargate, y todo esta corriendo en la nube. Pero nos salteamos algo importante: como obtiene tu aplicacion su URL de base de datos, API keys, y otros valores de configuracion? Si los dejaste hard-codeados en tu codigo fuente, tenes un problema.


El manejo de configuracion y secretos es uno de esos temas que parece simple hasta que lo haces mal. Una API key filtrada te puede costar miles de dolares. Una URL de base de datos mal configurada puede apuntar tu app de produccion a la base de datos de staging. Un archivo .env commiteado puede exponer credenciales a cualquiera que clone tu repositorio. Estos no son escenarios hipoteticos, pasan todo el tiempo.


En este articulo vamos a cubrir las practicas fundamentales para manejar configuracion y secretos: la metodologia 12-factor, variables de entorno, archivos .env, escaneo de secretos, AWS Secrets Manager, AWS Systems Manager Parameter Store, y como estructurar la configuracion entre entornos de dev, staging y produccion. Al final vas a tener un enfoque claro y practico para mantener tu config limpia y tus secretos seguros.


Vamos a meternos de lleno.


La 12-factor app: la config pertenece al entorno

La Twelve-Factor App es una metodologia para construir aplicaciones modernas que fue publicada por el equipo de Heroku alla por 2012. Describe doce principios para construir software que sea facil de deployear, escalar y mantener. El factor numero tres es sobre configuracion, y dice algo muy claro: guarda la config en el entorno.


Que significa “config” aca? Es cualquier cosa que probablemente cambie entre entornos (dev, staging, produccion). URLs de base de datos, API keys, feature flags, endpoints de servicios externos, niveles de log. Estos valores no deberian vivir en tu codigo fuente. No deberian estar horneados en tu imagen Docker. Deberian venir del entorno donde tu aplicacion esta corriendo.


El razonamiento es simple:


  • Seguridad: Los secretos en el codigo fuente terminan en control de versiones, en logs de CI, en capas de Docker, y en las manos de cualquiera que tenga acceso a tu repositorio.
  • Portabilidad: Si tu URL de base de datos esta hard-codeada, no podes correr el mismo codigo contra una base de datos de staging sin cambiar el codigo. Si viene del entorno, simplemente cambias la variable de entorno.
  • Simplicidad: Un solo artefacto de build (tu imagen Docker) funciona en cada entorno. Lo unico que cambia es la configuracion inyectada en runtime.

Aca esta el anti-patron versus el enfoque correcto:


// MAL: config hard-codeada
const dbUrl = "postgresql://admin:[email protected]:5432/myapp";

// BIEN: leer del entorno
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
  throw new Error("La variable de entorno DATABASE_URL es requerida");
}

El segundo ejemplo sigue el principio 12-factor. La aplicacion no sabe ni le importa en que entorno esta corriendo. Simplemente lee el valor del entorno y lo usa.


Variables de entorno: como funcionan

Las variables de entorno son pares clave-valor que existen en el entorno de proceso del sistema operativo. Cada proceso hereda el entorno de su proceso padre, y podes setear variables adicionales cuando lanzas un proceso.


Seteando y leyendo variables de entorno en la shell:


# Setear una variable para la sesion actual de la shell
export DATABASE_URL="postgresql://localhost:5432/myapp"

# Leerla
echo $DATABASE_URL

# Setear una variable solo para un unico comando
DATABASE_URL="postgresql://localhost:5432/myapp" node app.js

# Listar todas las variables de entorno
env

# Borrar una variable
unset DATABASE_URL

En Node.js/TypeScript, las accedes a traves de process.env:


// Leer una variable de entorno
const port = process.env.PORT || "3000";
const dbUrl = process.env.DATABASE_URL;
const logLevel = process.env.LOG_LEVEL || "info";

// Verificar variables requeridas al inicio
const required = ["DATABASE_URL", "API_KEY", "JWT_SECRET"];
for (const key of required) {
  if (!process.env[key]) {
    console.error(`Falta la variable de entorno requerida: ${key}`);
    process.exit(1);
  }
}

Este patron de verificar variables requeridas al inicio es importante. Queres que tu aplicacion falle rapido y ruidosamente si le falta configuracion, no que se rompa silenciosamente en algun punto aleatorio despues.


Archivos dotenv: conveniencia para desarrollo local

Tipear export DATABASE_URL=... cada vez que abris una terminal se vuelve tedioso rapido. Para eso estan los archivos .env. Un archivo .env es un archivo de texto simple que lista variables de entorno, una por linea:


# .env
DATABASE_URL=postgresql://localhost:5432/myapp_dev
API_KEY=dev-api-key-not-real
JWT_SECRET=local-dev-secret
LOG_LEVEL=debug
PORT=3000

Librerias como dotenv para Node.js leen automaticamente este archivo y cargan las variables en process.env cuando tu aplicacion inicia:


// Cargar el archivo .env al principio de tu entry point
import "dotenv/config";

// Ahora process.env.DATABASE_URL esta disponible
console.log(process.env.DATABASE_URL);

La regla critica con los archivos .env es: nunca los commitees a Git. Contienen secretos, y tu repositorio Git no es un lugar seguro para guardar secretos. Agrega .env a tu .gitignore inmediatamente:


# .gitignore

# Archivos de entorno con secretos
.env
.env.local
.env.*.local

# Mantener el archivo de ejemplo (sin secretos reales)
!.env.example

En lugar de commitear tu archivo .env real, commitea un archivo .env.example con valores de placeholder. Esto le dice a tus companeros de equipo que variables necesitan sin exponer secretos reales:


# .env.example
DATABASE_URL=postgresql://localhost:5432/myapp_dev
API_KEY=tu-api-key-aca
JWT_SECRET=genera-un-string-aleatorio
LOG_LEVEL=debug
PORT=3000

Cuando un nuevo desarrollador se suma al equipo, copia .env.example a .env y completa con sus propios valores. Simple, seguro, efectivo.


Por que nunca deberias commitear secretos a Git

Esto merece su propia seccion porque es asi de importante. Cuando commiteas un secreto a Git, no existe solo en la version actual del archivo. Existe en el historial de Git para siempre. Incluso si borras el archivo o sobreescribis el valor en un commit posterior, cualquiera que clone el repositorio puede encontrarlo mirando el historial de commits.


# Ups, commitee mi archivo .env
git log --all --full-history -- .env

# Cualquiera puede ver el contenido de ese archivo en ese commit
git show abc123:.env

Si esto pasa, el secreto esta comprometido. Necesitas rotarlo inmediatamente, o sea generar una clave nueva y revocar la vieja. Reescribir el historial de Git con git filter-branch o BFG Repo-Cleaner es posible pero doloroso, especialmente en un repositorio compartido.


El mejor enfoque es la prevencion. Usa herramientas que escaneen tu repositorio buscando secretos antes de que se commiteen:


  • git-secrets: Una herramienta de AWS que instala hooks de Git para prevenir commitear secretos. Escanea buscando access keys de AWS, secret keys, y patrones personalizados que vos definas.
  • gitleaks: Un scanner mas rapido y completo que detecta un amplio rango de patrones de secretos (API keys, tokens, passwords) en todo el historial de tu repositorio.
  • pre-commit: Un framework para gestionar hooks de pre-commit de Git. Podes agregar gitleaks o git-secrets como un hook que corre automaticamente en cada commit.

Asi se configura gitleaks como un hook de pre-commit:


# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

# Instalar pre-commit y configurar los hooks
pip install pre-commit
pre-commit install

# Ahora cada commit va a ser escaneado buscando secretos automaticamente
git commit -m "agregar nueva funcionalidad"
# gitleaks corre y bloquea el commit si encuentra un secreto

Tambien deberias correr gitleaks en tu pipeline de CI como red de seguridad. Cubrimos pipelines de CI en el articulo cinco, asi que agregar un paso de gitleaks es directo:


# En tu workflow de GitHub Actions
- name: Escanear secretos
  uses: gitleaks/gitleaks-action@v2
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Jerarquia de configuracion: como se resuelven los valores

En una aplicacion real, la configuracion puede venir de multiples fuentes. Cuando la misma clave esta definida en mas de un lugar, necesitas un orden de precedencia claro. La jerarquia estandar, de menor a mayor prioridad, se ve asi:


1. Defaults de la aplicacion (fallbacks hard-codeados en tu codigo)
2. Archivos de config (archivos JSON, YAML, TOML cargados al inicio)
3. Variables de entorno (seteadas por el SO, container runtime, o archivo .env)
4. Flags de CLI (pasados al iniciar la aplicacion)
5. Config remota (obtenida de Secrets Manager, Parameter Store, etc.)

Cada nivel sobreescribe al de abajo. Asi que si tu codigo tiene un default LOG_LEVEL=info, tu archivo de config lo setea a warn, y tu variable de entorno lo setea a debug, la variable de entorno gana. Si ademas pasas --log-level=error como flag de CLI, eso gana sobre todo lo demas.


Aca hay un ejemplo practico mostrando esta jerarquia en TypeScript:


import { readFileSync, existsSync } from "fs";

interface AppConfig {
  port: number;
  logLevel: string;
  dbUrl: string;
}

function loadConfig(): AppConfig {
  // Nivel 1: Defaults de la aplicacion
  let config: AppConfig = {
    port: 3000,
    logLevel: "info",
    dbUrl: "postgresql://localhost:5432/myapp",
  };

  // Nivel 2: Archivo de config (si existe)
  const configPath = "./config.json";
  if (existsSync(configPath)) {
    const fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
    config = { ...config, ...fileConfig };
  }

  // Nivel 3: Variables de entorno (sobreescriben la config del archivo)
  if (process.env.PORT) config.port = parseInt(process.env.PORT, 10);
  if (process.env.LOG_LEVEL) config.logLevel = process.env.LOG_LEVEL;
  if (process.env.DATABASE_URL) config.dbUrl = process.env.DATABASE_URL;

  return config;
}

const config = loadConfig();
console.log("Config cargada:", config);

Este patron te da flexibilidad. Los desarrolladores pueden usar un archivo de config localmente, el entorno de CI puede setear variables de entorno, y produccion puede obtener secretos de AWS Secrets Manager (que vamos a cubrir a continuacion).


AWS Secrets Manager: almacenando y obteniendo secretos

AWS Secrets Manager es un servicio gestionado para almacenar, obtener y rotar secretos. A diferencia de las variables de entorno, que son visibles en las definiciones de task de ECS, templates de CloudFormation, y potencialmente en logs, Secrets Manager almacena valores encriptados en reposo y provee control de acceso granular a traves de politicas de IAM.


Cuando deberias usar Secrets Manager en lugar de variables de entorno planas?


  • Credenciales de base de datos: Secrets Manager puede rotar automaticamente passwords de base de datos en un cronograma, actualizando tanto el valor del secreto como la base de datos misma.
  • API keys de servicios terceros: Stripe, Twilio, SendGrid, cualquier cosa donde una key filtrada significa plata real.
  • Certificados TLS y claves privadas: Cualquier cosa criptografica que nunca deberia aparecer en texto plano.
  • Secretos compartidos entre servicios: Cuando multiples servicios necesitan las mismas credenciales, Secrets Manager es una unica fuente de verdad.

Creando un secreto con la CLI de AWS:


# Crear un secreto de string simple
aws secretsmanager create-secret \
  --name "prod/task-api/database-url" \
  --description "String de conexion a base de datos de produccion" \
  --secret-string "postgresql://admin:s3cur3P@[email protected]:5432/myapp"

# Crear un secreto JSON (multiples pares clave-valor en un secreto)
aws secretsmanager create-secret \
  --name "prod/task-api/credentials" \
  --description "Credenciales de API de produccion" \
  --secret-string '{
    "DB_URL": "postgresql://admin:s3cur3P@[email protected]:5432/myapp",
    "API_KEY": "sk_live_abc123",
    "JWT_SECRET": "un-string-aleatorio-muy-largo"
  }'

Nota la convencion de nombres: entorno/servicio/nombre-del-secreto. Este naming jerarquico hace que sea facil organizar secretos y escribir politicas de IAM que restrinjan acceso por entorno o servicio.


Obteniendo un secreto:


# Obtener el valor del secreto
aws secretsmanager get-secret-value \
  --secret-id "prod/task-api/database-url" \
  --query SecretString \
  --output text

Secrets Manager: basicos de rotacion

Una de las funcionalidades mas poderosas de Secrets Manager es la rotacion automatica. En lugar de usar el mismo password de base de datos para siempre (y rezar que nadie lo filtre), podes configurar Secrets Manager para rotar el password en un cronograma, por ejemplo cada 30 dias.


Para bases de datos Amazon RDS, AWS provee funciones Lambda de rotacion incorporadas. El proceso de rotacion funciona asi:


1. Secrets Manager invoca una funcion Lambda en un cronograma
2. La Lambda genera un nuevo password
3. Actualiza el password en la base de datos RDS
4. Almacena el nuevo password en Secrets Manager
5. Tu aplicacion obtiene el nuevo valor la proxima vez que lee el secreto

Configurando la rotacion con la CLI:


# Habilitar rotacion para un secreto de RDS
aws secretsmanager rotate-secret \
  --secret-id "prod/task-api/database-url" \
  --rotation-lambda-arn "arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSRotation" \
  --rotation-rules '{"AutomaticallyAfterDays": 30}'

Lo importante que tenes que entender sobre la rotacion es que tu aplicacion necesita manejarla de forma elegante. Si tu app cachea el string de conexion a la base de datos al inicio y nunca lo relee, un password rotado va a romper tu conexion. La solucion es o re-obtener el secreto periodicamente o usar una libreria de conexion que pueda manejar el refresco de credenciales.


Secrets Manager: politicas de acceso IAM

Controlas quien y que puede acceder a tus secretos a traves de politicas IAM. Aca hay una politica que permite a un rol de task de ECS leer solo los secretos de un entorno y servicio especificos:


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/task-api/*"
    }
  ]
}

Esta politica sigue el principio de menor privilegio. El task de ECS solo puede leer secretos bajo el prefijo prod/task-api/. No puede listar todos los secretos de la cuenta, no puede leer secretos de otros servicios, y no puede modificar ni borrar ningun secreto. Si alguien compromete tu servicio task-api, igual no puede acceder a los secretos pertenecientes a tu user-service o payment-service.


Adjuntas esta politica al rol de ejecucion del task de ECS que configuramos en el articulo anterior:


# Crear la politica
aws iam create-policy \
  --policy-name task-api-secrets-read \
  --policy-document file://secrets-policy.json

# Adjuntarla al rol del task de ECS
aws iam attach-role-policy \
  --role-name task-api-task-role \
  --policy-arn "arn:aws:iam::123456789012:policy/task-api-secrets-read"

AWS Systems Manager Parameter Store

Parameter Store es otro servicio de AWS para almacenar configuracion, y sirve un proposito diferente al de Secrets Manager. Pensalo de esta manera:


  • Secrets Manager: Para valores sensibles que necesitan encriptacion, rotacion, y control de acceso granular. Cuesta $0.40 por secreto por mes.
  • Parameter Store: Para valores de configuracion no sensibles o menos sensibles. El tier estandar es gratis para hasta 10,000 parametros.

Parameter Store soporta tres tipos de parametros:


  • String: Un valor de texto plano. Bueno para configuracion como niveles de log, feature flags, o URLs de endpoints.
  • StringList: Una lista de valores separados por coma.
  • SecureString: Un valor encriptado usando AWS KMS. Esto provee encriptacion similar a Secrets Manager pero sin las funcionalidades de rotacion.

Creando parametros con la CLI:


# Parametro de string plano
aws ssm put-parameter \
  --name "/prod/task-api/log-level" \
  --type "String" \
  --value "info"

# Parametro encriptado
aws ssm put-parameter \
  --name "/prod/task-api/api-key" \
  --type "SecureString" \
  --value "sk_live_abc123"

# Obtener un parametro
aws ssm get-parameter \
  --name "/prod/task-api/log-level" \
  --query "Parameter.Value" \
  --output text

# Obtener un parametro encriptado (desencriptarlo)
aws ssm get-parameter \
  --name "/prod/task-api/api-key" \
  --with-decryption \
  --query "Parameter.Value" \
  --output text

# Obtener todos los parametros bajo un path
aws ssm get-parameters-by-path \
  --path "/prod/task-api/" \
  --with-decryption

El naming jerarquico por path (/entorno/servicio/parametro) es la misma convencion que usamos con Secrets Manager, y hace que las politicas de IAM sean directas:


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/task-api/*"
    }
  ]
}

Un patron comun es usar Parameter Store para config no sensible (nivel de log, feature flags, URLs de servicios) y Secrets Manager para valores verdaderamente sensibles (passwords de base de datos, API keys). Esto mantiene los costos bajos y te da lo mejor de ambos servicios.


Ejemplo practico: cargando config de env vars y Secrets Manager

Juntemos todo. Aca hay un ejemplo realista de carga de configuracion en una aplicacion TypeScript que lee de variables de entorno primero, y despues recurre a AWS Secrets Manager para valores sensibles.


Primero, instala el SDK de AWS:


npm install @aws-sdk/client-secrets-manager @aws-sdk/client-ssm

Ahora el cargador de configuracion:


// src/config.ts
import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";

const smClient = new SecretsManagerClient({ region: "us-east-1" });
const ssmClient = new SSMClient({ region: "us-east-1" });

interface AppConfig {
  port: number;
  logLevel: string;
  dbUrl: string;
  apiKey: string;
  jwtSecret: string;
}

async function getSecret(secretId: string): Promise<string> {
  const command = new GetSecretValueCommand({ SecretId: secretId });
  const response = await smClient.send(command);
  if (!response.SecretString) {
    throw new Error(`El secreto ${secretId} no tiene valor string`);
  }
  return response.SecretString;
}

async function getParameter(name: string): Promise<string> {
  const command = new GetParameterCommand({
    Name: name,
    WithDecryption: true,
  });
  const response = await ssmClient.send(command);
  if (!response.Parameter?.Value) {
    throw new Error(`Parametro ${name} no encontrado`);
  }
  return response.Parameter.Value;
}

export async function loadConfig(): Promise<AppConfig> {
  const env = process.env.APP_ENV || "dev";

  // Config no sensible: preferir env vars, recurrir a Parameter Store
  const port = process.env.PORT
    ? parseInt(process.env.PORT, 10)
    : 3000;

  const logLevel = process.env.LOG_LEVEL
    || await getParameter(`/${env}/task-api/log-level`).catch(() => "info");

  // Config sensible: preferir env vars (para dev local), recurrir a Secrets Manager
  let dbUrl = process.env.DATABASE_URL;
  let apiKey = process.env.API_KEY;
  let jwtSecret = process.env.JWT_SECRET;

  if (!dbUrl || !apiKey || !jwtSecret) {
    console.log(`Obteniendo secretos de AWS Secrets Manager para env: ${env}`);
    const secretString = await getSecret(`${env}/task-api/credentials`);
    const secrets = JSON.parse(secretString);

    dbUrl = dbUrl || secrets.DB_URL;
    apiKey = apiKey || secrets.API_KEY;
    jwtSecret = jwtSecret || secrets.JWT_SECRET;
  }

  if (!dbUrl || !apiKey || !jwtSecret) {
    throw new Error("Falta configuracion requerida. Revisa env vars o Secrets Manager.");
  }

  return { port, logLevel, dbUrl, apiKey, jwtSecret };
}

Y aca esta como lo usas en el entry point de tu aplicacion:


// src/index.ts
import "dotenv/config";
import { loadConfig } from "./config";
import { createApp } from "./app";

async function main() {
  const config = await loadConfig();
  console.log(`Iniciando servidor en puerto ${config.port} (log level: ${config.logLevel})`);

  const app = createApp(config);
  app.listen(config.port, () => {
    console.log(`Servidor corriendo en http://localhost:${config.port}`);
  });
}

main().catch((err) => {
  console.error("Error al iniciar:", err);
  process.exit(1);
});

Este setup funciona tanto para desarrollo local como para produccion:


  • Desarrollo local: Los desarrolladores setean valores en su archivo .env. La app lee de process.env y nunca le pega a AWS.
  • Produccion: El archivo .env no existe. La app detecta las env vars faltantes y las busca en Secrets Manager. El rol del task de ECS provee los permisos IAM necesarios.

Promocion de entornos: dev, staging y produccion

Cuando tenes multiples entornos, necesitas una estrategia clara sobre que cambia entre ellos y que se mantiene igual. El principio general es: tu codigo e imagen Docker deberian ser identicos en todos los entornos. Solo la configuracion deberia diferir.


Cosas que deberian diferir entre entornos:


  • Strings de conexion a base de datos: Cada entorno tiene su propia base de datos.
  • API keys y secretos: Claves separadas para cada entorno, asi una clave de dev comprometida no afecta produccion.
  • Niveles de log: Usualmente debug en dev, info en staging, warn o error en produccion.
  • Feature flags: Probar funcionalidades nuevas en staging antes de habilitarlas en produccion.
  • Parametros de escalado: Dev corre una instancia, produccion corre tres o mas.
  • Endpoints de servicios externos: Dev podria apuntar a APIs sandbox, produccion a las live.

Cosas que NO deberian diferir entre entornos:


  • Codigo de la aplicacion: La misma imagen Docker corre en todos lados. Sin code paths especificos por entorno.
  • Logica de negocio: Si tu app se comporta diferente en staging y produccion, la vas a pasar mal.
  • Estructura de configuracion: Las mismas claves existen en todos los entornos, solo con valores diferentes.

Aca hay una estructura practica usando Parameter Store y Secrets Manager:


Parameter Store:
  /dev/task-api/log-level       = "debug"
  /staging/task-api/log-level   = "info"
  /prod/task-api/log-level      = "warn"

  /dev/task-api/feature-new-ui  = "true"
  /staging/task-api/feature-new-ui = "true"
  /prod/task-api/feature-new-ui = "false"

Secrets Manager:
  dev/task-api/credentials      = { DB_URL: "...", API_KEY: "...", JWT_SECRET: "..." }
  staging/task-api/credentials  = { DB_URL: "...", API_KEY: "...", JWT_SECRET: "..." }
  prod/task-api/credentials     = { DB_URL: "...", API_KEY: "...", JWT_SECRET: "..." }

Tu task definition de ECS setea una unica variable de entorno, APP_ENV, para decirle a la aplicacion en que entorno esta corriendo. El cargador de config (como el que construimos arriba) usa ese valor para buscar los secretos correctos:


{
  "containerDefinitions": [
    {
      "name": "task-api",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:v1.2.3",
      "environment": [
        { "name": "APP_ENV", "value": "prod" },
        { "name": "PORT", "value": "3000" }
      ]
    }
  ]
}

Nota que los unicos valores en el task definition son no sensibles. La URL de base de datos y API keys se obtienen de Secrets Manager en runtime, asi que nunca aparecen en tu codigo Terraform, templates de CloudFormation, ni en la consola de ECS.


Integracion de ECS con Secrets Manager

ECS tambien tiene integracion nativa con Secrets Manager, donde puede inyectar valores de secretos directamente como variables de entorno al iniciar un container. Esto significa que tu aplicacion no necesita llamar a la API de Secrets Manager para nada:


{
  "containerDefinitions": [
    {
      "name": "task-api",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:v1.2.3",
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/task-api/credentials:DB_URL::"
        },
        {
          "name": "API_KEY",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/task-api/credentials:API_KEY::"
        }
      ],
      "environment": [
        { "name": "APP_ENV", "value": "prod" },
        { "name": "PORT", "value": "3000" }
      ]
    }
  ]
}

El campo valueFrom usa el formato secret-arn:json-key:version-stage:version-id. El doble dos puntos al final significa “usar la ultima version”. Este enfoque es mas simple porque tu aplicacion simplemente lee process.env.DATABASE_URL como siempre, y ECS maneja la integracion con Secrets Manager.


El trade-off es que los valores del secreto solo se obtienen cuando el container inicia. Si un secreto se rota, necesitas reiniciar el container para obtener el nuevo valor. El enfoque basado en SDK de la seccion anterior te permite re-obtener secretos sin reiniciar.


Herramientas avanzadas: Vault, SOPS y Sealed Secrets

Todo lo que cubrimos hasta ahora maneja los escenarios mas comunes bien. Pero a medida que tu infraestructura crece, podrias necesitar herramientas mas especializadas. Las cubri en profundidad en el articulo SRE: Secrets Management in Kubernetes, asi que aca va un resumen rapido con links:


  • HashiCorp Vault: Una plataforma de gestion de secretos completa. Soporta secretos dinamicos (generar una credencial de base de datos fresca para cada request), encriptacion como servicio, y logging de auditoria. Ideal para organizaciones grandes con requerimientos de compliance complejos.
  • SOPS: La herramienta de Mozilla para encriptar secretos en archivos. Podes guardar archivos YAML, JSON, o .env encriptados directamente en Git. SOPS encripta solo los valores, no las claves, asi que los diffs siguen siendo legibles. Genial para workflows de GitOps.
  • Sealed Secrets: Una solucion especifica de Kubernetes. Encriptas secretos localmente con una clave publica, commiteas la version encriptada a Git, y el controller de Sealed Secrets en tu cluster los desencripta. Perfecto para GitOps con Kubernetes.

Para el alcance de esta serie, AWS Secrets Manager y Parameter Store van a cubrir todo lo que necesitas. Si estas trabajando con Kubernetes y queres el deep dive en estas herramientas, revisa el articulo de SRE linkeado arriba.


Referencia rapida: eligiendo el enfoque correcto

Aca hay una guia de decision simple:


Es sensible (password, API key, token)?
  SI --> Usar AWS Secrets Manager
    - Necesita rotacion? --> Habilitar rotacion de Secrets Manager
    - Multiples servicios lo necesitan? --> Usar resource-based policy
  NO --> Es config especifica por entorno?
    SI --> Usar Parameter Store (tier gratis)
    NO --> Hard-codearlo como default de la aplicacion

Y aca hay una tabla comparativa:


Funcionalidad            | Env Vars      | Parameter Store | Secrets Manager
-------------------------|---------------|-----------------|----------------
Costo                    | Gratis        | Gratis (std)    | $0.40/secreto/mes
Encriptacion             | No            | Opcional (KMS)  | Siempre (KMS)
Rotacion                 | Manual        | Manual          | Automatica
Logging de auditoria     | No            | CloudTrail      | CloudTrail
Historial de versiones   | No            | Si              | Si
Acceso cross-account     | No            | Si              | Si
Mejor para               | Dev local     | No sensible     | Datos sensibles

Notas de cierre

Ahora tenes un entendimiento solido de como manejar configuracion y secretos en una aplicacion real. Los puntos clave son: segui la metodologia 12-factor y mantene la config fuera de tu codigo, usa archivos .env para desarrollo local pero nunca los commitees, escanea tus repositorios buscando secretos filtrados con herramientas como gitleaks, usa AWS Secrets Manager para valores sensibles y Parameter Store para todo lo demas, y estructura tu configuracion para que la misma imagen Docker funcione en cada entorno.


Estos son los fundamentos que te van a servir bien sin importar que proveedor de cloud o plataforma de orquestacion termines usando. En el proximo articulo, vamos a abordar DNS, TLS, y hacer que tu aplicacion sea alcanzable desde internet con un nombre de dominio propio y HTTPS. Nos vemos ahi.


Espero que te haya resultado util y que lo hayas disfrutado, hasta la proxima!


Errata

Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.

Tambien, podes revisar el codigo fuente y los cambios en los fuentes aca



$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-05-15 | Gabriel Garrido