Seleccione su idioma

La migración de Portainer Agent a Edge Agent Standard en varios clústeres OpenShift/OKD reveló una cadena de problemas independientes: permisos del sistema de archivos, campos inmutables de servicios Kubernetes, resolución DNS, credenciales incompletas y automatización del despliegue. Esta guía documenta los síntomas, las causas confirmadas y las correcciones aplicadas.

Nota: caso técnico real anonimizado. Se han omitido nombres de organizaciones, clústeres, nodos, dominios, direcciones IP, identificadores y credenciales. Los ejemplos utilizan nombres genéricos y deben adaptarse al entorno antes de ejecutar comandos.

1. Contexto: del agente convencional a Edge Agent

La arquitectura inicial utilizaba Portainer Community Edition con un agente convencional en cada clúster. Cada agente se exponía mediante un servicio Kubernetes LoadBalancer, respaldado por MetalLB, y el servidor central accedía a su API por el puerto TCP 9001.

Portainer Server -- TCP 9001 -- LoadBalancer -- Portainer Agent -- Kubernetes API

La arquitectura objetivo empleaba Edge Agent Standard. En este modelo es el propio agente quien inicia las comunicaciones hacia Portainer: realiza consultas periódicas a la API a través de HTTPS y establece el túnel Edge por TCP 8000 cuando es necesario. La exposición individual de cada agente mediante un balanceador deja de ser necesaria.

OpenShift / Edge Agent -- HTTPS ----> Portainer API
                       -- TCP 8000 -> Portainer Edge Tunnel

2. Error inicial: permisos para generar certificados

Las primeras pruebas con el agente convencional produjeron un error de escritura:

unable to generate self-signed certificates
open cert.pem: permission denied

OpenShift aplica políticas de seguridad mediante Security Context Constraints (SCC), que pueden impedir ejecutar los contenedores como root y restringir la escritura sobre el sistema de archivos de la imagen. El agente intentaba crear un certificado en una ubicación no escribible para su usuario efectivo.

Diagnóstico: los permisos RBAC del ServiceAccount sobre la API de Kubernetes son independientes de los permisos del usuario del proceso dentro del contenedor. Un rol administrativo en Kubernetes no resuelve un permission denied del sistema de archivos.

Se optó por mantener la ejecución sin privilegios y adaptar el despliegue Edge, en lugar de ampliar los privilegios del contenedor como solución permanente.

3. Error de migración: ClusterIP inmutable

Al aplicar el manifiesto Edge sobre el servicio existente, Kubernetes rechazó el cambio:

spec.clusterIPs[0]: Invalid value: ["None"]:
may not change once set

El servicio anterior disponía de una ClusterIP asignada. El nuevo agente necesitaba un servicio headless con clusterIP: None. Ese campo no puede modificarse en un servicio ya creado.

Solución: añadir una migración controlada al procedimiento de despliegue. La automatización comprueba el servicio existente, identifica el antiguo LoadBalancer y, solo en ese caso, elimina y recrea ese servicio concreto. No se elimina el namespace ni el servidor Portainer.

apiVersion: v1
kind: Service
metadata:
  name: portainer-agent
spec:
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app: portainer-agent

La recreación del servicio puede producir una interrupción breve y debe tratarse como una operación de mantenimiento controlada. Los siguientes despliegues reutilizan el servicio headless.

4. Error DNS durante el arranque

Una vez sustituido el servicio, aparecieron fallos de resolución:

lookup portainer-agent on <dns-server>:53:
no such host

El agente utiliza el nombre del servicio para descubrir miembros. Un servicio headless puede no ofrecer los endpoints esperados mientras todavía no existen pods considerados Ready. Se producía una dependencia circular: el agente necesitaba resolver el servicio para inicializarse, pero los endpoints aún no estaban publicados.

Solución: establecer publishNotReadyAddresses: true para permitir el descubrimiento de los pods durante su inicialización. En este despliegue, el cambio eliminó el bloqueo de resolución DNS.

5. Credencial Edge vacía y error secundario en el puerto 80

Otro intento terminaba con un mensaje aparentemente relacionado con los puertos privilegiados:

Unable to start Edge server
listen tcp 0.0.0.0:80: bind: permission denied

La revisión del Secret Kubernetes reveló que EDGE_KEY tenía longitud cero. Por tanto, el agente no estaba recibiendo correctamente la clave de asociación generada por Portainer. En esa configuración se observó un intento de iniciar un servidor en el puerto 80; el mensaje de permisos era un síntoma secundario de la inicialización incorrecta.

La corrección consistió en proporcionar una clave válida y verificar que el agente iniciaba el flujo Edge esperado. No fue necesario conceder capacidades para abrir puertos privilegiados.

Las credenciales se centralizaron en HashiCorp Vault, con un registro por entorno y dos campos:

Campo Uso
id Identificador del entorno Edge
key Clave de asociación con Portainer

La automatización recupera los valores desde Vault, construye un ConfigMap con los parámetros no sensibles y genera un Secret Kubernetes con la clave. El Deployment la consume mediante:

env:
  - name: EDGE_KEY
    valueFrom:
      secretKeyRef:
        name: portainer-agent-edge-key
        key: edge.key

El despliegue falla de forma explícita si falta alguno de los valores. Las claves no se imprimen, no se incorporan a los manifiestos versionados y no se muestran en los logs. Para comprobar la carga basta con validar su existencia y longitud, sin revelar el contenido.

6. Directorio /tmp sin permisos de escritura

Con una clave válida, el agente ya podía iniciar su API y establecer parte de las comunicaciones. Sin embargo, persistían errores en componentes internos:

failed to create healthy file:
open /tmp/.portainer-healthy: permission denied
failed to create alert rule evaluator:
open TSDB: mkdir /tmp/portainer-eval-tsdb-...:
permission denied

El contenedor se ejecutaba como usuario no privilegiado. El volumen de /data era escribible, pero el agente también necesitaba crear archivos de salud, métricas y datos temporales en /tmp, que no era escribible bajo el contexto de seguridad aplicado.

Solución: montar volúmenes emptyDir independientes en /data y /tmp y mantener un contexto de seguridad sin privilegios:

spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
        - name: portainer-agent
          volumeMounts:
            - name: agent-data
              mountPath: /data
            - name: agent-tmp
              mountPath: /tmp
      volumes:
        - name: agent-data
          emptyDir: {}
        - name: agent-tmp
          emptyDir: {}

Los volúmenes emptyDir son efímeros: sus datos se pierden al eliminar el pod. Debe tenerse en cuenta si se habilitan funciones que requieran persistencia. En el entorno analizado, esta configuración eliminó los errores de escritura sin ejecutar el contenedor como root.

7. Readiness: HTTP interno no equivale a la conexión Edge

El Deployment comprobaba la disponibilidad de la API interna del agente a través de /ping en el puerto 9001:

readinessProbe:
  httpGet:
    path: /ping
    port: 9001
    scheme: HTTP
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

En esta configuración, la API interna escuchaba mediante HTTP. Esto no contradice el uso de HTTPS y del túnel TCP 8000 en las comunicaciones externas de Edge Agent.

Hay que distinguir tres estados: Running indica que el proceso está ejecutándose; Ready indica que la comprobación local pasa; y Connected implica que Portainer recibe las comunicaciones necesarias y puede administrar el entorno. Ni Running ni Ready prueban por sí solos la conectividad Edge.

8. Automatización: definiciones duplicadas en el Makefile

Durante la implantación también aparecieron advertencias de GNU Make:

warning: overriding recipe for target 'configure'
warning: ignoring old recipe for target 'configure'

El Makefile contenía varias definiciones para objetivos como configure y deploy. GNU Make conserva la última receta para cada objetivo y descarta las anteriores. El resultado era un despliegue que podía ejecutar una secuencia antigua y omitir la carga de credenciales desde Vault.

Se consolidó un único objetivo por operación. La secuencia resultante prepara el namespace, obtiene credenciales, verifica ConfigMap y Secret, migra el servicio cuando hace falta, aplica los manifiestos y valida el rollout.

deploy: configure check-config

La automatización también se corrigió para propagar errores de lectura y de aplicación, en lugar de ocultarlos entre tuberías o comandos encadenados. Como la configuración común se distribuye mediante submódulos Git, se verificó que cada clúster apuntara a la revisión correcta: un cambio en el repositorio compartido no actualiza automáticamente todos los submódulos.

9. Verificación de la resolución

La validación se realizó a nivel Kubernetes, aplicación y red. Primero se comprobó el Deployment y los pods:

oc -n portainer get deployment portainer-agent
oc -n portainer get pods -l app=portainer-agent -o wide
oc -n portainer logs deployment/portainer-agent --tail=100

Entre los registros de una inicialización correcta aparecieron mensajes equivalentes a:

edge key loaded from options
Agent info updated successfully
evaluator successfully initialized

Se verificó la ausencia de errores persistentes de escritura en /tmp y de bucles de reinicio. También se comprobó la conectividad saliente desde una red representativa de los pods hacia el endpoint HTTPS de Portainer y el puerto TCP 8000 del túnel.

Durante un rollout pueden aparecer avisos transitorios de descubrimiento de miembros referidos a pods antiguos en terminación. Es necesario distinguir estos eventos de un fallo persistente del nuevo pod; tampoco debe atribuirse una causa de red sin evidencias reproducibles.

La validación decisiva se realizó en la interfaz de Portainer: entornos disponibles, actualización de estado y acceso funcional a los recursos Kubernetes. Un Deployment saludable no sustituye a una prueba funcional desde Portainer.

10. Resultado y conclusiones

Aspecto Configuración final
Modalidad Edge Agent Standard
Comunicación Iniciada desde el agente, HTTPS y TCP 8000
Servicio local Headless, sin LoadBalancer individual
Identidad del proceso Usuario no privilegiado
Directorios escribibles Volúmenes emptyDir
Gestión de credenciales HashiCorp Vault y Secret Kubernetes
Despliegue Manifiestos versionados y Makefile consolidado
Validación Estado Kubernetes y conexión funcional en Portainer

La migración no falló por una única causa. Coexistían errores de permisos, diseño de servicios, descubrimiento DNS, gestión de secretos y automatización. La resolución exigió separar los síntomas de sus causas, corregir cada componente y añadir comprobaciones reproducibles.

Lección principal: reiniciar repetidamente un Deployment no corrige un Secret vacío, un campo inmutable, un directorio sin permisos ni una receta de despliegue equivocada. El diagnóstico debe terminar con una comprobación funcional, no únicamente con un pod en estado Running.

Referencias