Skip to content
victor·stein
0%
booting ~/
./blog / infra / homelab-opentofu
infraopentofutailscale

Un amigo tacaño, mi Plex y mi primer OpenTofu

VS Victor Stein · 04 may 2026 · 7 min de lectura

Mi amigo es demasiado tacaño para pagar la función de reproducción remota de Plex. Igual quería ver pelis desde mi servidor. Esa es toda la razón por la que aprendí OpenTofu.

Tengo un servidor de Plex en casa — la máquina en sí es historia para otro día. Compartirlo por internet de la forma fácil significa o pagarle a Plex, o abrir un hueco en mi red doméstica, y sé perfectamente lo mala idea que es lo segundo. Meter a un amigo, aunque sea de confianza, a tu LAN de casa es confiar en su laptop, en sus torrents y en lo que sea que ya viva ahí dentro, con todo lo demás que tenés corriendo en casa.

Así que no abrí la red. Lo metí a mi tailnet de Tailscale — pero acotado, en código. Escribí un ACL de Tailscale en OpenTofu que le da exactamente una cosa: la máquina de Plex, y nada más en el tailnet. La máquina lleva un tag, tag:plex, y la política dice que su cuenta puede llegar a ese tag y ahí se detiene.

Ese fue mi primer cambio de verdad de infraestructura como código. Funcionó al primer apply, él consiguió sus pelis, y mi red se quedó cerrada. Y, calladito, me cambió la forma de ver todo lo demás que venía administrando a mano.

La bola de nieve

Una victoria limpia y ya no pude ignorar el resto. Lo siguiente en la lista fue GitHub. Tenía años de repos con configuraciones que había clicado a la existencia una por una y que no podía reconstruir. Así que empecé a codificarlo: configuraciones estandarizadas como reglas administradas por Tofu, un proceso de release tags repartido por todos los repos y — la parte que más me gusta — crear un repo nuevo pidiéndoselo a Claude en vez de repetir el mismo baile de configuraciones cada santa vez.

Después se me fue de las manos y metí Cloudflare — R2, Pages, un par de Workers, el DNS. Encajó perfecto en básicamente todo lo que toca un desarrollador web. A esas alturas dejó de ser “mi homelab” y se volvió todo mi control plane, en un solo repo, detrás de pull requests.

La noche más larga fue release-please

La noche que casi me quiebra fue el proceso de release.

Saco actualizaciones para mis TUIs y mis repos personales un par de veces al mes, y cada release lo hacía a mano: actualizar una fórmula de Homebrew si el proyecto es un tap, publicar en npm si es un paquete, o solo sacar una versión nueva en main si no es ninguna de las dos. La misma ceremonia, re-derivada por repo, para siempre. Me cansé y decidí construir una plantilla de release-please y repartirla a todos los repos — el workflow, la config y el manifest los empuja Tofu a cada repo (github_repository_file), no se commitean a mano.

La parte de release-please era la mitad fácil. La mitad dolorosa fue que cada repo existente ya tenía su propio deploy a la medida, y tuve que doblar todos a una sola forma sin romper ninguno. Ahí salió el patrón que lo salvó: release-please es dueño del versionado (PR feat:/fix: → abre y auto-mergea un release PR → tag), y una acción ad-hoc, aparte, es dueña del envío real — repartir a una fórmula de brew, publicar en npm, o no hacer nada, según el repo. Dos trabajos, separados limpiamente, en vez de un solo script enredado por proyecto.

Y release-please tiene colmillos. Su parser lee el commit de squash entero — tu título más la lista de bullets que GitHub le grapa debajo — y si un bullet lo hace tropezar (un feat(scope): perdido, un {a,b} dentro de un bloque de código), falla en silencio total: sin error, sin release, aunque el título del PR diga claramente feat:. El PR #10 se comió un release exactamente así, y tuve que forzarlo con un footer Release-As:. No estaba en ninguna doc que hubiera leído — solo una cicatriz.

Una noche brutal. Pero las ganancias son enormes: no he vuelto a hacer un release a mano desde entonces.

Cómo está cableado

Es un solo root module, un solo state file, y unas cuantas reglas que hacen más de lo que aparentan.

El estado vive en un bucket de Cloudflare R2 que el propio Tofu administra — importado, nunca creado, con prevent_destroy para que un plan descuidado no lo recree y deje el estado huérfano. Locking nativo por lockfile de S3; ninguna tabla de DynamoDB que cuidar.

La política de Tailscale se autorepara. El recurso tailscale_acl pone overwrite_existing_content = true, así que si meto la pata con una regla en la consola de admin a la 1am, el siguiente apply la machaca de vuelta a lo que dice acl.hujson. El acceso del amigo está clavado igual: un recurso tailscale_device_tags mantiene tag:plex en la máquina, así que si el tag se cae en la UI, el siguiente apply lo vuelve a poner. Lo que construí para mantenerlo en su carril se defiende solo.

GitHub también está aquí dentro — configuraciones de repo, secrets de Actions, el andamiaje de release-please, hasta un split de archivado-vs-activo (los repos archivados dan 403 ante cualquier cambio de settings, así que Tofu administra solo su flag archived e ignora el resto).

Y el hack que voy a defender: dos bloques de provider github, uno autenticado por App y otro con alias a un PAT de permisos finos. El recurso del repositorio corre sobre el PAT porque el provider github v6 tiene un bug donde, bajo auth de App en una cuenta personal, decide que no sos una organización, y el camino de lectura se cae calladito. ¿Feo? Un poco. Pero es una salida de emergencia de una línea con un comentario explicando exactamente por qué, y la alternativa era quedarme bloqueado esperando un fix upstream.

La disciplina es toda manual, que es la parte graciosa: la protección de ramas ni siquiera está activada (los rulesets quieren GitHub Pro en repos privados), así que nada me impide hacer push a main. Lo que me detiene es el cableado — plan.yml postea el diff en cada PR, apply.yml corre al mergear — y una regla que me guardo a mí mismo: leer el plan, luego mergear. Un check verde solo prueba que Tofu alcanzó los providers; no prueba que el diff sea lo que yo quería.

Lo que codifico, y lo que no

La parte más ridícula la agregué sin ninguna razón práctica: mi perfil de GitHub — el repo victorstein/victorstein cuyo README es lo primero que ve cualquiera que cae en mi página — también lo administra OpenTofu, desde el mismo repo stein-infra. El README se escribe en el apply con un bloque github_repository_file. ¿Hay una sola razón práctica para renderizar mi propio perfil a través de OpenTofu? Ninguna. Porque puedo. lol.

Lo único que dejo manual a propósito es la invitación. A mi amigo lo agregué al tailnet a mano — esa es una decisión humana y quiero que siga siéndolo. Pero todo lo que viene después de la invitación — a qué puede llegar, que es Plex y solo Plex — es código, revisado en un PR, y se autorepara si alguien lo toca.

El servidor en sí es historia para otro día. ¿Pero la regla que deja a un amigo tacaño ver pelis sin que sea dueño de un byte más de mi red del que necesita? Esa sí le puedo hacer git blame. Y si no le puedo hacer git blame, ya me las arreglaré.

infraopentofutailscaleiacgithub-actions ← todas las entradas
SIGUIENTE →
Evité la Mac que quería por no migrar