<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ricardo]]></title><description><![CDATA[Ricardo]]></description><link>https://ricardo.sh</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Ricardo</title><link>https://ricardo.sh</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 26 Apr 2026 03:04:06 GMT</lastBuildDate><atom:link href="https://ricardo.sh/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[ShiftClaw: corriendo OpenClaw en OpenShift como una carga de trabajo seria]]></title><description><![CDATA[Un experimento para llevar un agente de IA al terreno donde me siento cómodo: contenedores, OpenShift, seguridad por defecto y operación reproducible.

Hace algún tiempo empecé a jugar con agentes de ]]></description><link>https://ricardo.sh/shiftclaw-corriendo-openclaw-en-openshift-como-una-carga-de-trabajo-seria</link><guid isPermaLink="true">https://ricardo.sh/shiftclaw-corriendo-openclaw-en-openshift-como-una-carga-de-trabajo-seria</guid><category><![CDATA[openshift]]></category><category><![CDATA[openclaw]]></category><dc:creator><![CDATA[ricardo]]></dc:creator><pubDate>Sat, 25 Apr 2026 23:50:05 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>Un experimento para llevar un agente de IA al terreno donde me siento cómodo: contenedores, OpenShift, seguridad por defecto y operación reproducible.</p>
</blockquote>
<p>Hace algún tiempo empecé a jugar con agentes de IA. No me refiero solamente a usar un chatbot desde el navegador, sino a ejecutar un agente real, persistente, con estado, credenciales, canales de entrada y salida, y la posibilidad de dejarlo en ejecución como un servicio.</p>
<p>Ahí apareció OpenClaw.</p>
<p>OpenClaw me pareció interesante porque no lo vi solo como otra herramienta para probar prompts. Lo vi como una pieza que podía convertirse en algo más parecido a una aplicación de plataforma: un agente que corre todo el tiempo, recibe mensajes, mantiene la configuración, se conecta a modelos externos y puede integrarse con canales como Telegram.</p>
<p>Pero apenas uno sale del modo demo, aparece la pregunta incómoda: ¿cómo se opera esto de verdad?</p>
<p>Muchos proyectos de este tipo nacen en el mundo del <code>npm install</code>, del <code>docker run</code>, del servidor personal o del tutorial que funciona perfecto mientras todo vive en la laptop del autor. Eso está bien para empezar. De hecho, casi todo lo interesante empieza así. Pero no es la forma en que normalmente pienso sobre la operación de software.</p>
<p>Trabajo todos los días con Linux, contenedores, OpenShift, seguridad, automatización, GitOps y plataformas empresariales. Entonces mi pregunta fue bastante natural: ¿qué pasaría si tratara a OpenClaw como trataría a cualquier otra aplicación que quiero operar en OpenShift?</p>
<p>De ahí nació <strong>ShiftClaw</strong>.</p>
<p>El repositorio está aquí:</p>
<pre><code class="language-text">https://github.com/rarguello/shiftclaw
</code></pre>
<h2>La idea del proyecto</h2>
<p>ShiftClaw es mi empaquetado de OpenClaw para correrlo en OpenShift u OKD. No es un fork conceptual de OpenClaw ni pretende reinventar el agente. La idea es construir, alrededor de OpenClaw, las piezas que faltan para operarlo de forma más parecida a la producción.</p>
<p>Eso significa tener una imagen basada en Red Hat UBI 10, construirla con GitHub Actions, publicarla en GHCR, desplegarla con manifiestos de Kubernetes/OpenShift, darle almacenamiento persistente, manejar credenciales con Secrets, sembrar la configuración inicial con ConfigMaps, endurecer el security context y limitar la red con NetworkPolicy.</p>
<p>Dicho de otra forma: ShiftClaw no es solo “metí OpenClaw en un contenedor”. Es el ejercicio de preguntarme cómo debería verse un agente de IA cuando deja de ser un experimento local y empieza a comportarse como una carga de trabajo seria.</p>
<h2>Por qué OpenShift</h2>
<p>Podría haber dejado todo en un <code>docker run</code>, y de hecho el proyecto también permite correrlo localmente con Podman porque eso es útil para desarrollo y pruebas. Pero mi principal interés era OpenShift.</p>
<p>Un agente persistente se parece más a una aplicación de plataforma que a un script. Tiene estado. Tiene credenciales. Tiene conectividad externa. Tiene lifecycle. Necesita reiniciarse, actualizarse, leer la configuración, escribir datos, reportar logs y sobrevivir a cambios en la infraestructura. En ese punto, las primitivas de Kubernetes y OpenShift dejan de ser “overkill” y pasan a ser el lenguaje natural para el problema.</p>
<p>OpenShift ya tiene muchas de las piezas que necesito: scheduling, restart automático, Secrets, ConfigMaps, PVCs, NetworkPolicy, security contexts, probes, service accounts y límites de recursos. La parte interesante no es usar OpenShift porque sí, sino usarlo para responder una pregunta que cada vez me parece más importante: ¿cómo se opera un agente de IA en infraestructura real?</p>
<p>No en una demo bonita. No en un notebook. No en la laptop. En infraestructura real.</p>
<h2>La imagen: UBI 10 y Node.js 24</h2>
<p>La primera decisión fue construir la imagen sobre Red Hat UBI 10. No porque tenga algo contra Debian o Ubuntu, sino porque quería que el proyecto encajara naturalmente con OpenShift y con el ecosistema de Red Hat.</p>
<p>El <code>Containerfile</code> usa un build multi-stage. La imagen de builder instala OpenClaw usando Node.js 24, y la imagen final usa una base minimal para ejecutar el agente con menos superficie innecesaria.</p>
<pre><code class="language-Dockerfile">ARG OPENCLAW_VERSION=2026.4.23

FROM registry.access.redhat.com/ubi10/nodejs-24:10.1 AS builder

ARG OPENCLAW_VERSION
WORKDIR /opt/app-root/src

USER 0
RUN npm install "openclaw@${OPENCLAW_VERSION}" \
  --omit=dev \
  --no-audit \
  --no-fund \
  &amp;&amp; npm cache clean --force

FROM registry.access.redhat.com/ubi10/nodejs-24-minimal:10.1 AS runtime

ARG OPENCLAW_VERSION
WORKDIR /opt/app-root/src

COPY --from=builder --chown=1001:0 /opt/app-root/src/node_modules ./node_modules

USER 0
RUN mkdir -p /var/lib/openclaw \
  &amp;&amp; chown 1001:0 /var/lib/openclaw \
  &amp;&amp; chmod g=u /var/lib/openclaw

USER 1001

ENV OPENCLAW_VERSION=${OPENCLAW_VERSION} \
  XDG_CONFIG_HOME=/var/lib/openclaw \
  OPENCLAW_CONFIG_PATH=/var/lib/openclaw/openclaw.json \
  NODE_ENV=production \
  NPM_CONFIG_CACHE=/tmp/.npm \
  HOME=/var/lib/openclaw \
  PATH="/opt/app-root/src/node_modules/.bin:$PATH"

EXPOSE 18789

ENTRYPOINT ["openclaw", "gateway", "--allow-unconfigured"]
</code></pre>
<p>Hay un detalle importante para OpenShift: la imagen usa el usuario <code>1001</code>, pero OpenShift puede asignar un UID arbitrario en runtime cuando se usa la SCC restringida. Por eso el directorio persistente queda preparado con grupo <code>0</code> y permisos compatibles con ese modelo. Parece un detalle menor hasta que un Pod no puede escribir en el volumen y uno pierde una tarde mirando los logs.</p>
<h2>El estado del agente</h2>
<p>Un agente sin estado suele ser una demo. Si quiero que OpenClaw funcione como servicio, necesito persistir la configuración y los datos entre reinicios.</p>
<p>En ShiftClaw decidí que el estado viva en <code>/var/lib/openclaw</code>. En OpenShift, ese directorio se monta a partir de un PVC. Ahí queda la configuración viva del agente, incluyendo cosas que no quiero perder cada vez que se recrea el Pod.</p>
<p>Esto también me obligó a tomar una decisión sobre la configuración inicial. Un ConfigMap es muy cómodo para versionar un <code>openclaw.json</code>, pero OpenClaw necesita modificar su configuración en runtime. Si monto el archivo directamente desde el ConfigMap, termino con un archivo de solo lectura y una aplicación peleando contra Kubernetes.</p>
<p>La solución fue usar el ConfigMap como semilla. Un init container copia el archivo inicial al PVC solo si el archivo aún no existe.</p>
<pre><code class="language-yaml">command:
  - sh
  - -c
  - '[ -f /var/lib/openclaw/openclaw.json ] || cp /run/config/openclaw.json /var/lib/openclaw/openclaw.json'
</code></pre>
<p>Me gusta esta solución porque es simple y explícita. El ConfigMap define el punto de partida, pero después, el estado en vivo pertenece al PVC. Si algún día quiero forzar una nueva configuración desde cero, borro el archivo del PVC y reinicio el StatefulSet. No hay magia, pero hay control.</p>
<h2>Por qué usé StatefulSet</h2>
<p>Podría haber usado un Deployment, pero un StatefulSet se siente más natural en este caso. No necesito escalar ShiftClaw horizontalmente; por ahora una sola réplica es suficiente. Lo que sí necesito es identidad estable y almacenamiento persistente asociados al Pod.</p>
<p>Un agente con pairing de Telegram, configuración en vivo y estado local no se siente como una aplicación stateless cualquiera. Se parece más a un pequeño servicio persistente. Por eso preferí un StatefulSet con un PVC de 5 Gi. No es una arquitectura sofisticada, pero sí la adecuada para este primer objetivo.</p>
<h2>Telegram como interfaz y no exponer una Route</h2>
<p>Una de las decisiones que más me gustaron fue no exponer ShiftClaw a través de una Route desde el inicio.</p>
<p>El agente se maneja por Telegram, así que no necesito publicar una interfaz web en Internet, ni abrir una Route en OpenShift, ni resolver de inmediato el TLS público, la autenticación web o la exposición accidental. El gateway interno escucha en el puerto <code>18789</code>, pero para la operación normal no necesito exponerlo fuera del clúster.</p>
<p>Cuando necesito acceso local, uso <code>port-forward</code>.</p>
<pre><code class="language-bash">oc port-forward pod/shiftclaw-0 18789:18789 -n shiftclaw
</code></pre>
<p>Esta decisión reduce considerablemente la superficie de ataque inicial. No significa que Telegram sea perfecto ni que sea la única interfaz posible. Significa que, para este experimento, me permite operar el agente sin abrir más puertas de las necesarias.</p>
<h2>Seguridad desde el primer manifiesto</h2>
<p>Si voy a correr un agente de IA con tokens y acceso a servicios externos, no quiero que el YAML sea el típico manifiesto de demo que corre como root, con filesystem writable, capacidades por defecto y un service account token montado sin razón.</p>
<p>ShiftClaw corre como non-root, usa <code>RuntimeDefault</code> como seccomp profile, no permite privilege escalation, elimina capabilities y usa root filesystem de solo lectura.</p>
<pre><code class="language-yaml">securityContext:
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault
</code></pre>
<p>Y dentro del contenedor:</p>
<pre><code class="language-yaml">securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL
</code></pre>
<p>También deshabilité el montaje automático del token del ServiceAccount.</p>
<pre><code class="language-yaml">automountServiceAccountToken: false
</code></pre>
<p>Ese último punto me parece especialmente importante. Si el agente no necesita comunicarse con la API de Kubernetes, no hay razón para entregarle un token del clúster. Es una de esas pequeñas decisiones que reducen el riesgo sin complicar demasiado la operación.</p>
<p>Como el filesystem raíz es de solo lectura, las rutas que requieren escritura se montan explícitamente. El estado vive en el PVC y <code>/tmp</code> vive en un <code>emptyDir</code>. Eso obliga a que el manifiesto diga la verdad sobre lo que la aplicación necesita registrar.</p>
<h2>NetworkPolicy y salida controlada</h2>
<p>También agregué una NetworkPolicy. La idea no es crear una cárcel perfecta, sino adoptar una postura razonable: no aceptar tráfico entrante y permitir únicamente la salida que el agente necesita para funcionar.</p>
<p>En OpenShift con OVN-Kubernetes hay un detalle importante para DNS: hay que permitir el tráfico hacia <code>openshift-dns</code>, normalmente en UDP 5353. Después de eso, el agente necesita HTTPS para comunicarse con servicios externos como Telegram y OpenRouter.</p>
<pre><code class="language-yaml">- to:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: openshift-dns
    podSelector:
      matchLabels:
        app: dns
  ports:
  - port: 5353
    protocol: UDP
</code></pre>
<p>La tentación en un laboratorio siempre es abrir todo y avanzar. A veces toca hacerlo para diagnosticar. Pero si desde el inicio sé que el agente solo necesita resolver DNS y salir por HTTPS, prefiero que el manifiesto lo refleje.</p>
<h2>Build automático y GHCR</h2>
<p>No quería que la imagen dependiera de builds manuales en mi laptop. Por eso el repo incluye un workflow de GitHub Actions que construye la imagen y la publica en GitHub Container Registry.</p>
<p>El workflow fija la versión de OpenClaw en una variable, construye para <code>linux/amd64</code> y <code>linux/arm64</code>, genera SBOM y provenance y ejecuta Trivy contra la imagen. La política de escaneo intenta ser práctica: reportar severidades relevantes, pero fallar el pipeline solo por vulnerabilidades críticas.</p>
<p>Ese balance me gusta porque evita que el pipeline se convierta en ruido permanente, pero mantiene una barrera clara frente a problemas graves.</p>
<h2>Despliegue en OpenShift</h2>
<p>El despliegue está pensado para ser directo. Primero creo el namespace, luego creo el Secret desde un <code>.env</code> local y después aplico los manifiestos con Kustomize.</p>
<pre><code class="language-bash">oc apply -f manifests/namespace.yaml

oc create secret generic shiftclaw \
  --from-env-file=.env \
  --namespace=shiftclaw

oc apply -k manifests/
</code></pre>
<p>El <code>.env</code> no se commitea. Ahí van valores como el token de Telegram, la API key de OpenRouter y el token interno del gateway.</p>
<p>Para mirar el arranque uso los comandos de siempre.</p>
<pre><code class="language-bash">oc get pods -n shiftclaw -w
oc logs -l app.kubernetes.io/name=shiftclaw -n shiftclaw -f
</code></pre>
<p>Al primer arranque, OpenClaw requiere aprobar el emparejamiento con Telegram. El código aparece en los logs y la aprobación se realiza dentro del Pod.</p>
<pre><code class="language-bash">oc exec -it shiftclaw-0 -n shiftclaw -- sh
openclaw pairing approve telegram &lt;PAIRING_CODE&gt;
</code></pre>
<p>Una vez aprobado, esa información queda en el PVC. El Pod puede reiniciarse sin perder el emparejamiento.</p>
<h2>Correrlo localmente con Podman</h2>
<p>Aunque el objetivo principal es OpenShift, también quería una forma sencilla de probar ShiftClaw localmente. Para eso dejé un <code>run.sh</code> que corre la imagen con Podman, monta estado persistente desde el home del usuario, carga secretos desde <code>.env</code> y expone el gateway en <code>localhost:18789</code>.</p>
<p>El flujo local empieza por copiar el archivo de ejemplo y editar las credenciales.</p>
<pre><code class="language-bash">cp .env.example .env
./run.sh
</code></pre>
<p>Si quiero probar una imagen construida localmente, puedo hacerlo sin alterar demasiado el flujo.</p>
<pre><code class="language-bash">podman build -t localhost/shiftclaw:dev .
./run.sh localhost/shiftclaw:dev
</code></pre>
<p>Esto me permite iterar rápido sin tener que desplegar cada cambio en un clúster. Para mí es importante que el proyecto tenga ese doble camino: local para desarrollo, OpenShift para operación.</p>
<h2>Quadlet como punto intermedio</h2>
<p>También dejé una opción para ejecutar ShiftClaw como servicio de usuario con systemd mediante Quadlet. Me gusta mucho ese modelo porque es un punto intermedio entre “lo corro a mano” y “lo despliego en OpenShift”.</p>
<p>Con Quadlet puedo dejar el agente corriendo de forma persistente en una máquina Linux, sin root y sin clúster.</p>
<pre><code class="language-bash">mkdir -p ~/.local/share/shiftclaw
cp config/openclaw.json ~/.local/share/shiftclaw/openclaw.json

mkdir -p ~/.config/shiftclaw
cp .env.example ~/.config/shiftclaw/env

mkdir -p ~/.config/containers/systemd
cp shiftclaw.container ~/.config/containers/systemd/

loginctl enable-linger

systemctl --user daemon-reload
systemctl --user enable --now shiftclaw
</code></pre>
<p>Los logs quedan en journald.</p>
<pre><code class="language-bash">journalctl --user -u shiftclaw -f
</code></pre>
<p>No todo experimento necesita empezar en Kubernetes. A veces Podman y systemd son suficientes. Pero diseñar el proyecto para que pueda moverse entre esos mundos me parece valioso.</p>
<h2>Qué me dejó este experimento</h2>
<p>La primera conclusión es que un agente de IA deja de ser un juguete en el momento en que cuenta con tokens, estado, red y persistencia. En ese punto ya estás operando una aplicación, aunque se llame “agente” y hable por Telegram.</p>
<p>La segunda conclusión es que OpenShift calza muy bien con este tipo de carga. A veces pensamos en OpenShift solo para aplicaciones empresariales tradicionales, pero un agente persistente también necesita las mismas primitivas: Pod, Secret, PVC, NetworkPolicy, probes, límites y políticas de seguridad.</p>
<p>La tercera conclusión es que el estado es el centro del problema. Me interesa mucho la idea de que el agente tenga una especie de “alma” que no dependa del compute. Si mañana reemplazo el Pod, la imagen, el nodo o incluso el clúster, debería poder conservar su estado y configuración. Por ahora esa alma vive en un PVC. Más adelante quiero experimentar con opciones externas como S3, Redis o algún sistema de memoria más explícito.</p>
<p>Y la cuarta conclusión es que conviene diseñar la seguridad desde el primer manifiesto. Es mucho más fácil empezar con non-root, filesystem de solo lectura, capabilities eliminadas, NetworkPolicy y Secrets fuera de la imagen que intentar endurecer todo cuando el proyecto ya creció.</p>
<h2>Lo que quiero explorar después</h2>
<p>ShiftClaw todavía es un laboratorio, pero ya tiene forma de proyecto real. Lo siguiente que quiero investigar es cómo dar acceso controlado a un navegador, cómo limitar mejor el tráfico outbound con un proxy, cómo manejar la memoria externa, cómo desplegarlo con GitOps, cómo firmar imágenes con Cosign y cómo integrar MCPs útiles sin convertir el agente en una caja negra con demasiados permisos.</p>
<p>También quiero seguir probando modelos. OpenRouter facilita cambiar de backend, pero cada modelo se comporta de manera distinta. No todos sirven para el mismo tipo de agente ni para el mismo estilo de interacción.</p>
<p>El objetivo no es tener un bot simpático en Telegram. El objetivo es entender cómo operar agentes de IA con prácticas de infraestructura reales.</p>
<p>Correr un agente es fácil. Correrlo de forma persistente, reproducible, segura, actualizable y observable ya es otro tema.</p>
<p>ShiftClaw es mi primer intento de llevar OpenClaw a ese terreno. Y como todo buen experimento de infraestructura, seguro que todavía se va a romper varias veces.</p>
<p>Por eso, justamente, vale la pena documentarlo.</p>
]]></content:encoded></item></channel></rss>