Ambiente dev seguro y bajo demanda con AWS EC2 + AMI

2026-03-25

post-thumb

Índice

Hay situaciones en las que necesitas un ambiente de desarrollo completamente aislado de tu máquina principal: probar repositorios de terceros, ejecutar código en el que no confías totalmente, trabajar con herramientas que requieren Linux nativo, o simplemente tener un desktop dev desechable que no comprometa tus credenciales, claves SSH o tokens.

La idea aquí es simple: montarlo una vez, guardarlo como imagen y restaurar bajo demanda. Cuando terminas, lo destruyes. Cuando lo necesitas de nuevo, lo recreas en minutos. ¿Y el costo? Cerca de $1/mes cuando está parado.


Visión general de la arquitectura

Día de uso:
  AMI Snapshot --> Lanza EC2 --> Conecta vía DCV --> Trabaja --> Termina EC2

Entre usos:
  Solo el snapshot AMI existe (~$1/mes para 20 GB)
  Sin instancias corriendo, sin volúmenes EBS, sin costo de cómputo

¿Por qué este enfoque?

  • Pagas ~$1/mes cuando no lo estás usando (solo almacenamiento de la AMI)
  • Cada sesión empieza desde una imagen limpia y preconfigurada
  • Tu macOS queda completamente aislado, sin compartir archivos ni red
  • Amazon DCV ofrece escritorio remoto fluido (gratuito en EC2)
  • La instancia se destruye después de cada uso, sin datos residuales

Eligiendo la distro

Si tus herramientas requieren glibc (la mayoría lo requiere), Alpine (musl libc) no es compatible.

Recomendado: Ubuntu Server 24.04 LTS + XFCE

Mejor equilibrio entre compatibilidad, documentación y ligereza.

DistroDesktopRAM ociosaProsContras
Xubuntu 24.04XFCE~600-800 MBRepos Ubuntu/PPAs, comunidad enormeUn poco más pesado que Debian
Debian 12 + XFCEXFCE~400-600 MBMás liviano, establePaquetes más antiguos
Ubuntu 24.04GNOME~1.5-2 GBMejor experiencia out-of-boxPesado, desperdicia RAM
Fedora 41 XFCEXFCE~600-800 MBPaquetes más recientesdnf más lento que apt

La estrategia: usar la AMI oficial de Ubuntu Server 24.04 (gratuita en el AWS Marketplace) e instalar XFCE encima. Desktop liviano con total compatibilidad.


Prerrequisitos

Todo esto se hace una única vez en tu Mac.

1. Cuenta AWS y CLI

brew install awscli
aws configure
# Ingresa: Access Key ID, Secret Access Key, Región (ej: us-east-1), Output (json)

2. Cliente Amazon DCV

Descárgalo en docs.aws.amazon.com/dcv e instala el .dmg para macOS.

3. Par de claves SSH

aws ec2 create-key-pair \
  --key-name devenv-key \
  --query 'KeyMaterial' \
  --output text > ~/.ssh/devenv-key.pem

chmod 400 ~/.ssh/devenv-key.pem

4. Security Group

# Crear el security group
aws ec2 create-security-group \
  --group-name devenv-sg \
  --description "Safe dev environment"

# Permitir SSH (para setup inicial)
aws ec2 authorize-security-group-ingress \
  --group-name devenv-sg \
  --protocol tcp --port 22 \
  --cidr "$(curl -s https://checkip.amazonaws.com)/32"

# Permitir DCV (puerto 8443)
aws ec2 authorize-security-group-ingress \
  --group-name devenv-sg \
  --protocol tcp --port 8443 \
  --cidr "$(curl -s https://checkip.amazonaws.com)/32"

Importante: Esto restringe el acceso a tu IP pública actual. Si tu IP cambia, actualiza el security group antes de conectar.


Paso 1: Lanzar la instancia base

Encontrar la AMI de Ubuntu 24.04

AMI_ID=$(aws ec2 describe-images \
  --owners 099720109477 \
  --filters \
    "Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" \
    "Name=state,Values=available" \
  --query 'Images | sort_by(@, &CreationDate) | [-1].ImageId' \
  --output text)

echo "Usando AMI: $AMI_ID"

Lanzar la instancia

INSTANCE_ID=$(aws ec2 run-instances \
  --image-id "$AMI_ID" \
  --instance-type t3.xlarge \
  --key-name devenv-key \
  --security-groups devenv-sg \
  --block-device-mappings '[{
    "DeviceName": "/dev/sda1",
    "Ebs": {
      "VolumeSize": 80,
      "VolumeType": "gp3",
      "DeleteOnTermination": true
    }
  }]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=safe-devenv}]' \
  --query 'Instances[0].InstanceId' \
  --output text)

echo "Instancia: $INSTANCE_ID"

# Esperar a que la instancia esté corriendo
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"

# Obtener la IP pública
PUBLIC_IP=$(aws ec2 describe-instances \
  --instance-ids "$INSTANCE_ID" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)

echo "IP: $PUBLIC_IP"

Opciones de tipo de instancia

InstanciavCPUsRAMCosto/horaObservación
t3.large28 GB$0.0832Mínimo viable: una IDE a la vez
t3.xlarge416 GB$0.1664Recomendado: cómodo para la mayoría de escenarios
t3.2xlarge832 GB$0.3328Compilación pesada (Rust), múltiples IDEs
m5.xlarge416 GB$0.192CPU dedicada, sin límites de burst

Las instancias t3 son burstable: acumulan créditos de CPU cuando están ociosas. Para coding común, t3 es suficiente y más barato. Para compilación pesada de Rust o suites de tests, considera m5.


Paso 2: Instalar desktop y DCV

ssh -i ~/.ssh/devenv-key.pem ubuntu@$PUBLIC_IP

Espera ~60 segundos después de que la instancia reporte “running” para que cloud-init finalice. Si recibes “Connection refused”, espera e intenta de nuevo.

Actualizar el sistema

sudo apt update && sudo apt upgrade -y

Instalar XFCE

sudo apt install -y xfce4 xfce4-goodies xfce4-terminal dbus-x11

# Definir XFCE como sesión por defecto
echo "xfce4-session" > ~/.xsession

No instales ubuntu-desktop, eso arrastra GNOME y agrega ~1.2 GB de overhead de RAM.

Display manager

sudo apt install -y lightdm
sudo systemctl enable lightdm

Contraseña del usuario ubuntu

sudo passwd ubuntu
# Elige una contraseña fuerte, esta es la credencial de login de DCV

Instalar Amazon DCV

# Importar clave GPG
wget https://d1uj6qtbmh3dt5.cloudfront.net/NICE-GPG-KEY
gpg --import NICE-GPG-KEY

# Descargar DCV server
wget https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-ubuntu2404-x86_64.tgz

# Extraer e instalar
tar -xvzf nice-dcv-ubuntu2404-x86_64.tgz
cd nice-dcv-*-x86_64
sudo apt install -y ./nice-dcv-server_*.deb ./nice-dcv-web-viewer_*.deb ./nice-xdcv_*.deb

# Limpiar
cd ~ && rm -rf nice-dcv-* NICE-GPG-KEY

Configurar DCV

# Habilitar sesión console automática
sudo sed -i '/^\[session-management\/automatic-console-session\]/,/^\[/{s/^#\?owner=.*/owner="ubuntu"/}' \
  /etc/dcv/dcv.conf

# Si la sección no existe, agregarla
if ! grep -q "automatic-console-session" /etc/dcv/dcv.conf; then
  sudo tee -a /etc/dcv/dcv.conf > /dev/null <<DCVEOF

[session-management/automatic-console-session]
owner="ubuntu"
DCVEOF
fi

# Habilitar e iniciar DCV
sudo systemctl enable dcv-server
sudo systemctl start dcv-server

Verificar y conectar

sudo systemctl status dcv-server
dcv list-sessions
# Debe mostrar una sesión "console" con owner "ubuntu"

En tu Mac: abre el Amazon DCV Client, conéctate a $PUBLIC_IP:8443, acepta el certificado autofirmado e inicia sesión con ubuntu y la contraseña definida.


Paso 3: Instalar herramientas de desarrollo

Paquetes esenciales

sudo apt install -y \
  build-essential curl wget git unzip \
  software-properties-common apt-transport-https \
  ca-certificates gnupg lsb-release fontconfig libfuse2

VS Code

curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
  | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-keyring.gpg] \
  https://packages.microsoft.com/repos/code stable main" \
  | sudo tee /etc/apt/sources.list.d/vscode.list

sudo apt update && sudo apt install -y code

Warp Terminal

curl -fsSL https://releases.warp.dev/linux/keys/warp.asc \
  | sudo gpg --dearmor -o /usr/share/keyrings/warp-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/warp-keyring.gpg] \
  https://releases.warp.dev/linux/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/warp.list

sudo apt update && sudo apt install -y warp-terminal

Node.js (vía nvm)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc

nvm install --lts
nvm use --lts

Go

GO_VERSION="1.23.6"
wget "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz"
rm "go${GO_VERSION}.linux-amd64.tar.gz"

echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc

Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env

JetBrains Toolbox (GoLand, WebStorm, etc.)

TOOLBOX_URL=$(curl -s "https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release" \
  | grep -oP '"linux":{"link":"\K[^"]+')

wget -O jetbrains-toolbox.tar.gz "$TOOLBOX_URL"
tar -xzf jetbrains-toolbox.tar.gz
sudo mv jetbrains-toolbox-*/jetbrains-toolbox /usr/local/bin/
rm -rf jetbrains-toolbox*

Lanza JetBrains Toolbox desde la terminal dentro de la sesión DCV (no vía SSH). Necesita un display para la GUI.


Paso 4: Blindaje de seguridad

Esta es la parte más importante. El objetivo es garantizar que la instancia no tenga ningún vínculo con tus cuentas personales.

Remover credenciales personales

# Sin claves SSH personales
rm -rf ~/.ssh/id_* ~/.ssh/known_hosts

# Sin credenciales Git
git config --global --unset credential.helper 2>/dev/null
rm -f ~/.git-credentials ~/.gitconfig

# Identidad genérica
git config --global user.name "Dev"
git config --global user.email "dev@localhost"

# Sin credenciales AWS dentro de la instancia
rm -rf ~/.aws/credentials ~/.aws/config

# Sin perfiles de navegador
rm -rf ~/.mozilla ~/.config/google-chrome ~/.config/chromium

Firewall interno

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 8443/tcp   # DCV
sudo ufw allow 22/tcp     # SSH (puedes quitarlo después si quieres)
sudo ufw enable

Deshabilitar servicios innecesarios

sudo systemctl disable cups-browsed 2>/dev/null
sudo systemctl disable avahi-daemon 2>/dev/null
sudo systemctl disable bluetooth 2>/dev/null

Script de verificación de aislamiento

Guarda como ~/check-isolation.sh dentro de la instancia:

#!/bin/bash
echo "=== Verificación de Aislamiento ==="

ISSUES=0

check() {
  if [ -e "$1" ]; then
    echo "ALERTA: $1 existe -- $2"
    ISSUES=$((ISSUES + 1))
  fi
}

check ~/.ssh/id_rsa "Clave SSH personal encontrada"
check ~/.ssh/id_ed25519 "Clave SSH personal encontrada"
check ~/.git-credentials "Credenciales Git encontradas"
check ~/.aws/credentials "Credenciales AWS encontradas"
check ~/.docker/config.json "Login Docker encontrado"
check ~/.npmrc "Token NPM puede estar presente"
check ~/.config/gcloud "Config Google Cloud encontrada"
check ~/.azure "Config Azure encontrada"

GIT_EMAIL=$(git config --global user.email 2>/dev/null || echo "")
if [ -n "$GIT_EMAIL" ] && [ "$GIT_EMAIL" != "dev@localhost" ]; then
  echo "ALERTA: Email Git es '$GIT_EMAIL' -- debería ser dev@localhost"
  ISSUES=$((ISSUES + 1))
fi

echo ""
if [ $ISSUES -eq 0 ]; then
  echo "TODO LIMPIO -- el ambiente está seguro."
else
  echo "$ISSUES problema(s) encontrado(s). Revisa arriba."
fi
chmod +x ~/check-isolation.sh

Paso 5: Crear la Golden AMI

Esta es la imagen reutilizable. Todo lo que instalaste queda grabado en ella.

Limpiar antes de crear el snapshot

# Limpiar cache de paquetes
sudo apt clean
sudo apt autoremove -y

# Limpiar archivos temporales
rm -rf /tmp/* ~/.cache/*

# Limpiar historial del bash
history -c
> ~/.bash_history

# Apagar para snapshot limpio
sudo shutdown -h now

Crear la AMI (desde tu Mac)

# Esperar a que la instancia se detenga
aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID"

# Crear la AMI
AMI_DEVENV=$(aws ec2 create-image \
  --instance-id "$INSTANCE_ID" \
  --name "safe-devenv-$(date +%Y%m%d)" \
  --description "Safe dev environment: Xubuntu 24.04 + XFCE + DCV + Node/Go/Rust" \
  --no-reboot \
  --query 'ImageId' \
  --output text)

echo "Golden AMI: $AMI_DEVENV"

# Esperar a que la AMI esté disponible
aws ec2 wait image-available --image-ids "$AMI_DEVENV"
echo "AMI lista."

Terminar la instancia base

aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"
echo "Instancia terminada. Solo el snapshot AMI permanece."

A partir de aquí solo pagas por el snapshot: ~$0.05/GB/mes. Una imagen de 80 GB con ~20 GB usados cuesta alrededor de $1/mes.


Paso 6: Uso en el día a día

Script de inicialización

Guarda como ~/devenv-start.sh en tu Mac:

#!/bin/bash
set -euo pipefail

AMI_ID="ami-XXXXXXXXXXXXXXXXX"  # Tu Golden AMI
INSTANCE_TYPE="t3.xlarge"
KEY_NAME="devenv-key"
SG_NAME="devenv-sg"

echo "Lanzando ambiente dev seguro..."

# Actualizar security group con IP actual
MY_IP=$(curl -s https://checkip.amazonaws.com)
SG_ID=$(aws ec2 describe-security-groups \
  --group-names "$SG_NAME" \
  --query 'SecurityGroups[0].GroupId' \
  --output text)

aws ec2 revoke-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 22 --cidr "0.0.0.0/0" 2>/dev/null || true
aws ec2 revoke-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 8443 --cidr "0.0.0.0/0" 2>/dev/null || true

aws ec2 authorize-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 22 --cidr "${MY_IP}/32"
aws ec2 authorize-security-group-ingress --group-id "$SG_ID" \
  --protocol tcp --port 8443 --cidr "${MY_IP}/32"

# Lanzar
INSTANCE_ID=$(aws ec2 run-instances \
  --image-id "$AMI_ID" \
  --instance-type "$INSTANCE_TYPE" \
  --key-name "$KEY_NAME" \
  --security-group-ids "$SG_ID" \
  --block-device-mappings '[{
    "DeviceName": "/dev/sda1",
    "Ebs": {
      "VolumeSize": 80,
      "VolumeType": "gp3",
      "DeleteOnTermination": true
    }
  }]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=safe-devenv}]' \
  --query 'Instances[0].InstanceId' \
  --output text)

echo "Instancia: $INSTANCE_ID"
echo "Esperando a que esté lista..."

aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"

PUBLIC_IP=$(aws ec2 describe-instances \
  --instance-ids "$INSTANCE_ID" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)

echo ""
echo "========================================"
echo "  ¡Ambiente listo!"
echo "  DCV: ${PUBLIC_IP}:8443"
echo "  SSH: ssh -i ~/.ssh/devenv-key.pem ubuntu@${PUBLIC_IP}"
echo "  Instance ID: ${INSTANCE_ID}"
echo "========================================"
chmod +x ~/devenv-start.sh

Script de teardown

Guarda como ~/devenv-stop.sh en tu Mac:

#!/bin/bash
set -euo pipefail

echo "Buscando instancias del ambiente dev..."

INSTANCE_ID=$(aws ec2 describe-instances \
  --filters \
    "Name=tag:Name,Values=safe-devenv" \
    "Name=instance-state-name,Values=running,stopped" \
  --query 'Reservations[0].Instances[0].InstanceId' \
  --output text)

if [ "$INSTANCE_ID" = "None" ] || [ -z "$INSTANCE_ID" ]; then
  echo "Ninguna instancia encontrada."
  exit 0
fi

echo "Terminando instancia: $INSTANCE_ID"
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"

echo "Instancia terminada. Solo estás pagando por el snapshot AMI."
chmod +x ~/devenv-stop.sh

Costo estimado

EscenarioCosto mensual
AMI parada (sin uso)~$1/mes
4 sesiones de 3h (t3.xlarge)$2 compute + $1 AMI = **$3**
Stop/Start (EBS siempre activo, 80 GB)$6.40 EBS + $2 compute = **$8.40**
Siempre encendido (24/7)~$127/mes

El enfoque AMI ahorra ~50% comparado con Stop/Start y ~97% comparado con mantenerlo siempre encendido.


Troubleshooting

DCV muestra pantalla negra

sudo systemctl restart lightdm
sudo systemctl restart dcv-server
# Si persiste:
sudo reboot

Conexión rechazada en DCV

# Verificar si está corriendo
sudo systemctl status dcv-server

# Verificar puerto
ss -tlnp | grep 8443

# Verificar si tu IP actual coincide con el security group
curl -s https://checkip.amazonaws.com

Warp Terminal no abre (problema de GPU)

# Forzar renderización por software
LIBGL_ALWAYS_SOFTWARE=1 warp-terminal

# Para hacerlo permanente:
sudo sed -i 's|Exec=warp-terminal|Exec=env LIBGL_ALWAYS_SOFTWARE=1 warp-terminal|' \
  /usr/share/applications/dev.warp.Warp.desktop

Comandos rápidos

# === En tu Mac ===

# Iniciar ambiente
~/devenv-start.sh

# Apagar ambiente
~/devenv-stop.sh

# Listar tus AMIs
aws ec2 describe-images --owners self \
  --query 'Images[*].[ImageId,Name,CreationDate]' \
  --output table

# Eliminar AMI antigua
aws ec2 deregister-image --image-id ami-OLD
aws ec2 delete-snapshot --snapshot-id snap-OLD

# === Dentro de la instancia ===

# Verificar aislamiento
~/check-isolation.sh

# Verificar sesiones DCV
dcv list-sessions

# Reiniciar DCV
sudo systemctl restart dcv-server