Del Caos Al Control: Gestionando Variables De Entorno De Microservicios en AWS

Introducción
En este artículo te comparto la experiencia que tuve en una de las tareas más abarcadoras que he realizado desde el punto de vista infraestructura en AWS, y fue centralizar las variables de entorno que se compartían entre múltiples microservicios. Apredenderás sobre buenas prácticas relacionadas a variables de entorno y que deberías tener en cuenta cuando vayas a diseñar un sistema desde cero.
El problema
Ante todo, para que tengas contexto general, es importante entender que estamos hablando de un sistema con alrededor de 40 microservicios, esto lo dejo claro porque tiene bastante importancia tanto para entender el problema como la solución propuesta. Teniendo esto en cuenta, imagina que en cada uno de esos microservicios tienes variabes de entorno que, para empezar, están versionadas en el código, o sea, cualquiera tiene acceso a esas variables, donde pueden haber lo mismo datos sensibles, que URLs de conexión a bases de datos, conclusión, datos que por un motivo u otro no deberían estar en el código, y mucho menos ser accesibles para cualquiera que tenga acceso al repositorio.
Este sistema tenía la peculiaridad de que algunos microservicios estaban desplegados en AWS ECS usando EC2, otros en AWS Lambda, y otros en AWS Batch. O sea, que hay ciertas cosas específicas a cada uno de estos servicios que hay que tener en cuenta y las veremos más adelante. Entonces la idea para mejorar operativamente el sistema fue la siguiente:
Centralizar las variables de entorno. De esta forma todos los microservicios obtienen las variables de entorno de la misma forma, y si hay que cambiar alguna de estas variables, solo se cambia una vez. Esto es una gran mejora, ya que anteriormente, por ejemplo, si se cambiaba un API KEY, y esta se usaba en 15 microservicios, había que desplegar los 15 microservicios para poder efectuar el cambio, ahora esto ya no es necesario
Implementar un mecanismo para que cuando una de las variables de entorno se cambie, todos los microservicios que la usan actualicen el valor.
La solución
Para implementar esto, se pensó en dos servicios de AWS, Secrets Manager y Parameter Store, a continuación un resumen de las diferencias entre ambos, teniendo en cuenta que ambos podían resolver el problema:
| Secrets Manager | Parameter Store | |
|---|---|---|
| Rotación automática | Puede configurarse para que las keys se roten automáticamente | No tiene esta funcionalidad integrada aunque se puede lograr usando otros servicios como AWS Event Bride |
| Precio | Cada variable de entorno tiene un precio de $0.40 por mes, y por cada 10000 llamadas de APIs se cobra $0.05 | Hay que tener en cuenta que posee dos tipos de parámetros, los estándar y los avanzados, para esta aplicación los estándar fueron suficientes. En este caso, se permiten hasta 10000 parámetros que son gratis, a partir de esta cantidad, se cobra $0.05 por cada parámetro extra y las llamadas al API en el rendimiento estándar son gratuitas |
Implementación
A continuación mostraré un ejemplo de implementación para que te hagas una idea práctica de esta solución.
Variables de entorno
sentryDSN: valor hardcodeado del sentry DSN
sentryDSNParameter: /SENTRY/DSN/API_COMPA_COMPILALa propiedad sentryDSN refleja como estaba hardcodeada cada una de las variables de entorno antes de esta solución, y la propiedad sentryDSNParameter refleja el nuevo cambio, en el cual la variable de entorno solo hace referencia al camino donde se guardó el valor en Parameter Store.
Parameter Store
Evidentemente se tiene que crear cada parámetro en Parameter Store para poder ser luego usado por cada uno de los microservicios, siendo consecuentes con el ejemplo de variables de entorno mostrado anteriormente, quedaría algo como lo que se muestra a continuación:

Aquí tener en cuenta que el tipo de parámetro es: SecureString, esto implica que el valor se guarda encriptado, entonces cuando se obtiene el valor usando el cliente SSM del SDK de AWS, si no se le especifica que lo desencripte, entonces obtenemos el valor encriptado.
Tener en cuenta en caso que se guarden los valores encriptados y se quieran obtener desencriptados, que el role del servicio que ejecute el código (AWS Lambda, AWS ECS o AWS Batch) debe tener el permiso
kms:Decryptcon el recurso que corresponde a la llave de KMS que se usó para encriptar el valor, esto se selecciona en el momento de crear el parámetro
Obtención de los parámetros
A continuación te muestro un ejemplo del servicio escrito en Golang para obtener los parámetros de Parameter Store:
Modelo
Para este servicio se crean dos nuevas estructuras:
package model
type RetrievedParameters struct {
SentryDSN string
DatabaseUrl string
}
type ParametersInput struct {
SentryDSNParameter string
DatabaseUrlParameter string
}Servicio
package parameters
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/elC0mpa/api-compa-compila/model"
)
type ParametersService interface {
GetAllParameters(ctx context.Context) (*model.RetrievedParameters, error)
}
type secrets struct {
sentryDsnName string
databaseUrlName string
svc *ssm.Client
}
func NewParametersService(awsConfig aws.Config, parameters model.ParametersInput) ParametersService {
svc := ssm.NewFromConfig(awsConfig)
return &secrets{
sentryDsnName: parameters.SentryDSNParameter,
databaseUrlName: parameters.DatabaseUrlParameter,
svc: svc,
}
}
func (s *secrets) GetAllParameters(ctx context.Context) (*model.RetrievedParameters, error) {
parameterNames := []string{s.sentryDsnName, s.databaseUrlName}
input := ssm.GetParametersInput{Names: parameterNames, WithDecryption: aws.Bool(true)}
result, err := s.svc.GetParameters(ctx, &input
if err != nil {
return nil, fmt.Errorf("error retrieving parameters: %w", err)
}
if len(result.InvalidParameters) > 0 {
return nil, fmt.Errorf("error retrieving parameter: %s", result.InvalidParameters[0])
}
return s.mapParameters(result.Parameters), nil
}
func (s *secrets) mapParameters(params []types.Parameter) *model.RetrievedParameters {
parameters := model.RetrievedParameters{}
for _, parameter := range params {
if *parameter.Name == s.sentryDsnName {
parameters.SentryDSN = *parameter.Value
continue
}
parameters.DatabaseUrl = *parameter.Value
}
return ¶meters
}Este servicio es el encargado de recibir a través de la estructura ParametersInput los nombres de los parámetros que necesita este microservicio. Una vez que recibe estos nombres de los parámetros, los usa para obtener los valores de Parameter Store, y luego los devuelve en la estructura RetrievedParameters.
Hay que tener en cuenta que por la manera en que esto se diseñó, independientemente de si el microservicio es una lambda, un contenedor ejecutándose en ECS o un Job de Batch, esto se llama al inicio, o sea, una vez que se levanta el microservicio, lo que significa que no está continuamente leyendo de Parameter Store para verificar si alguna de las variables cambió. Por este motivo en un próximo artículo estaremos hablando del mecanismo que se implementó para que cuando un parámetro se cambie usando Parameter Store, los microservicios que lo usan se actualicen con su valor automáticamente.
Beneficios
Seguridad
El primer y diría yo que mayor beneficio que introdujo este cambio fue el relacionado a la seguridad, y aunque se explica solo te mostraré porque. Y es que siempre hay valores muy sensibles que no tienen porque estar al alcance de todas las personas que tengan acceso a un repositorio, y mucho menos para el entorno de Producción. Con este cambio los valores solo están en Parameter Store, y cada desarrollador solo tendrá acceso a los parámetros cuyos permisos IAM lo permitan. Por lo tanto, este es el beneficio principal de este cambio.
Operatividad
La otra ventaja es relacionada a la facilidad de operación del sistema en su totalidad. El punto es el siguiente, supongamos que 15 de los 40 microservicios se conectan a una misma base de datos SQL, y de repente por algún motivo el host de esta base de datos cambia, en ese caso, anteriormente había que volver a desplegar cada uno de los 15 microservicios actualizando el valor del host de la base de datos. Sin embargo, con esta nueva mejora, solo hay que cambiar el valor en el parámetro declarado en Parameter Store.
Conclusiones
Esto es todo por este artículo, espero poder haberte aportado algo, este es uno de los cambios de mayor impacto que he realizado como arquitecto de software. Nos vemos en el próximo artículo.
Contenido relacionado
- El Pequeño Cambio Que Generó Un Gran Impacto
- Aplicación Práctica De Las Funciones Cloudfront
- Desplegando Nuestro Blog Sin Usar Servidores
- Selección De La Herramienta Para El Desarrollo De Este Blog
- Tres Certificaciones AWS Associate en Seis Meses: Mi Experiencia SysOps Admin Y Más Allá