Lab práctico · Semana 3: Almacenamiento: S3 a fondo, EFS, FSx, Backup e híbrido

S3 a fondo - versionado, cifrado, ciclo de vida, replicación y Object Lock

⏱ 90-120 minDificultad: mediaTask statements: 1.33.14.1

Qué vas a construir

Tres buckets que ponen en práctica casi todo lo que el examen pregunta de S3:

  1. Bucket principal en eu-south-2 (España) con versionado, una bucket policy que obliga a usar HTTPS, una regla de ciclo de vida, subidas multipart, lectura por rangos, SSE-KMS por objeto y una presigned URL.
  2. Bucket réplica en eu-west-1 (Irlanda) que recibe los objetos por Cross-Region Replication (CRR) en la clase Standard-IA.
  3. Bucket con Object Lock en governance mode para comprobar la protección WORM.
flowchart LR
    U["Tú (CloudShell)"] -->|"HTTPS, multipart, presign"| P["Bucket principal (eu-south-2)"]
    P -->|"CRR asíncrona (rol de IAM)"| R["Bucket réplica (eu-west-1, STANDARD_IA)"]
    U --> L["Bucket Object Lock (governance)"]

Antes de empezar

  • Entra con tu usuario de IAM Identity Center o de IAM con MFA (nunca con el usuario raíz) y abre AWS CloudShell en la región Europa (España) eu-south-2.
  • Necesitas permisos de administrador o, como mínimo, sobre S3, IAM (crear un rol) y KMS (usar la clave administrada aws/s3).
  • Las variables de CloudShell se pierden si la sesión caduca. Si pasa, vuelve a ejecutar el bloque del paso 0.
  • Coste: unos pocos MB en S3 durante una o dos horas, peticiones y una pequeña transferencia entre regiones. Céntimos.

Paso 0: variables

Los nombres de bucket son globales; añadimos tu número de cuenta para que no choquen.

export AWS_REGION=eu-south-2
CUENTA=$(aws sts get-caller-identity --query Account --output text)
B=lab05-${CUENTA}-principal
BR=lab05-${CUENTA}-replica
BL=lab05-${CUENTA}-lock
echo "$B $BR $BL"

Paso 1: crear el bucket y revisar los valores por defecto

aws s3api create-bucket --bucket "$B" --region eu-south-2 \
  --create-bucket-configuration LocationConstraint=eu-south-2

aws s3api get-public-access-block --bucket "$B"
aws s3api get-bucket-ownership-controls --bucket "$B"
aws s3api get-bucket-encryption --bucket "$B"

Fíjate en tres cosas que el examen da por supuestas en buckets nuevos:

  • Block Public Access: los cuatro ajustes a true.
  • Object Ownership: BucketOwnerEnforced (ACL desactivadas).
  • Cifrado por defecto: AES256 (SSE-S3). Según la fecha de creación, verás también que los tipos de cifrado bloqueados incluyen SSE-C.

Paso 2: versionado y delete markers

aws s3api put-bucket-versioning --bucket "$B" \
  --versioning-configuration Status=Enabled

echo "versión 1" > /tmp/notas.txt
aws s3 cp /tmp/notas.txt "s3://$B/notas.txt"
echo "versión 2" > /tmp/notas.txt
aws s3 cp /tmp/notas.txt "s3://$B/notas.txt"

aws s3api list-object-versions --bucket "$B" --prefix notas.txt \
  --query 'Versions[].{Version:VersionId,Actual:IsLatest,Fecha:LastModified}'

Ahora «borra» el objeto y observa que solo aparece un delete marker:

aws s3 rm "s3://$B/notas.txt"
aws s3 ls "s3://$B/"
aws s3api list-object-versions --bucket "$B" --prefix notas.txt \
  --query 'DeleteMarkers[].{Marker:VersionId,Actual:IsLatest}'

Recupéralo eliminando el delete marker (sustituye el identificador por el que te ha salido):

MARKER=$(aws s3api list-object-versions --bucket "$B" --prefix notas.txt \
  --query 'DeleteMarkers[0].VersionId' --output text)
aws s3api delete-object --bucket "$B" --key notas.txt --version-id "$MARKER"
aws s3 cp "s3://$B/notas.txt" -

Debe mostrar «versión 2». Acabas de hacer una recuperación ante borrado accidental.

Paso 3: obligar a HTTPS con una bucket policy

cat > /tmp/politica.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SoloHTTPS",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": ["arn:aws:s3:::${B}", "arn:aws:s3:::${B}/*"],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}
EOF
aws s3api put-bucket-policy --bucket "$B" --policy file:///tmp/politica.json

Prueba a listar el bucket por HTTP (debe fallar con AccessDenied) y por HTTPS (funciona):

aws s3 ls "s3://$B/" --endpoint-url http://s3.eu-south-2.amazonaws.com
aws s3 ls "s3://$B/"

Un Deny explícito gana a cualquier Allow, incluso a tus permisos de administrador.

Paso 4: SSE-KMS por objeto

Sube un objeto pidiendo cifrado con KMS (usa la clave administrada por AWS aws/s3) y compara las cabeceras con uno cifrado con SSE-S3:

echo "dato sensible" > /tmp/secreto.txt
aws s3 cp /tmp/secreto.txt "s3://$B/kms/secreto.txt" --sse aws:kms

aws s3api head-object --bucket "$B" --key kms/secreto.txt \
  --query '{Cifrado:ServerSideEncryption,Clave:SSEKMSKeyId}'
aws s3api head-object --bucket "$B" --key notas.txt \
  --query '{Cifrado:ServerSideEncryption}'

Si quisieras que todo el bucket usara SSE-KMS con S3 Bucket Keys (menos llamadas a KMS), el comando sería este (no hace falta que lo ejecutes; lo dejamos en SSE-S3 para que la replicación del paso 7 sea sencilla):

aws s3api put-bucket-encryption --bucket "$B" \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"},"BucketKeyEnabled":true}]}'

Paso 5: multipart upload y lectura por rangos

Crea un fichero de 200 MB y súbelo. La CLI lo divide en partes automáticamente (por encima de 8 MB por defecto):

dd if=/dev/urandom of=/tmp/grande.bin bs=1M count=200
time aws s3 cp /tmp/grande.bin "s3://$B/datos/grande.bin"
aws s3api head-object --bucket "$B" --key datos/grande.bin --query '{Tam:ContentLength,ETag:ETag}'

El ETag termina en -N: es la marca de un objeto subido con multipart (N = número de partes).

Ahora descarga solo el primer KB con un byte-range fetch:

aws s3api get-object --bucket "$B" --key datos/grande.bin \
  --range bytes=0-1023 /tmp/trozo.bin
ls -l /tmp/trozo.bin

Paso 6: ciclo de vida

Regla para logs/: a Standard-IA a los 30 días, a Glacier Deep Archive a los 180, expiración a los 7 años; versiones antiguas borradas a los 30 días; multipart incompletos abortados a los 7 días.

cat > /tmp/ciclo.json <<'EOF'
{
  "Rules": [
    {
      "ID": "logs-7-anios",
      "Filter": { "Prefix": "logs/" },
      "Status": "Enabled",
      "Transitions": [
        { "Days": 30, "StorageClass": "STANDARD_IA" },
        { "Days": 180, "StorageClass": "DEEP_ARCHIVE" }
      ],
      "Expiration": { "Days": 2555 }
    },
    {
      "ID": "higiene-general",
      "Filter": {},
      "Status": "Enabled",
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
    }
  ]
}
EOF
aws s3api put-bucket-lifecycle-configuration --bucket "$B" \
  --lifecycle-configuration file:///tmp/ciclo.json
aws s3api get-bucket-lifecycle-configuration --bucket "$B"

Las transiciones no se ven en una hora (cuentan días), pero la configuración queda aplicada. Pregúntate: ¿qué pasaría con ficheros de log de 2 KB? (Pista: por defecto, los objetos de menos de 128 KB no se transicionan.)

Paso 7: replicación entre regiones (CRR)

7.1 Bucket de destino en eu-west-1

aws s3api create-bucket --bucket "$BR" --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1
aws s3api put-bucket-versioning --bucket "$BR" --region eu-west-1 \
  --versioning-configuration Status=Enabled

7.2 Rol de IAM que S3 asume para replicar

cat > /tmp/confianza-s3.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Principal": { "Service": "s3.amazonaws.com" }, "Action": "sts:AssumeRole" }
  ]
}
EOF

cat > /tmp/permisos-replica.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetReplicationConfiguration", "s3:ListBucket"],
      "Resource": "arn:aws:s3:::${B}"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObjectVersionForReplication", "s3:GetObjectVersionAcl", "s3:GetObjectVersionTagging"],
      "Resource": "arn:aws:s3:::${B}/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ReplicateObject", "s3:ReplicateDelete", "s3:ReplicateTags"],
      "Resource": "arn:aws:s3:::${BR}/*"
    }
  ]
}
EOF

aws iam create-role --role-name lab05-replicacion \
  --assume-role-policy-document file:///tmp/confianza-s3.json
aws iam put-role-policy --role-name lab05-replicacion \
  --policy-name replica --policy-document file:///tmp/permisos-replica.json

7.3 Regla de replicación

Replicamos todo el bucket a Standard-IA en el destino, sin replicar delete markers:

cat > /tmp/replicacion.json <<EOF
{
  "Role": "arn:aws:iam::${CUENTA}:role/lab05-replicacion",
  "Rules": [
    {
      "ID": "todo-a-irlanda",
      "Priority": 1,
      "Status": "Enabled",
      "Filter": {},
      "DeleteMarkerReplication": { "Status": "Disabled" },
      "Destination": { "Bucket": "arn:aws:s3:::${BR}", "StorageClass": "STANDARD_IA" }
    }
  ]
}
EOF
sleep 10
aws s3api put-bucket-replication --bucket "$B" \
  --replication-configuration file:///tmp/replicacion.json

7.4 Comprueba qué se replica y qué no

echo "nuevo tras CRR" > /tmp/nuevo.txt
aws s3 cp /tmp/nuevo.txt "s3://$B/nuevo.txt"
aws s3 cp /tmp/secreto.txt "s3://$B/kms/secreto-2.txt" --sse aws:kms
sleep 60
aws s3api head-object --bucket "$B" --key nuevo.txt --query ReplicationStatus
aws s3 ls "s3://$BR/" --recursive --region eu-west-1
aws s3api head-object --bucket "$BR" --key nuevo.txt --region eu-west-1 \
  --query '{Estado:ReplicationStatus,Clase:StorageClass}'

Observa:

  • nuevo.txt aparece en Irlanda con REPLICA y clase STANDARD_IA; en el origen, COMPLETED.
  • notas.txt y datos/grande.bin no están: existían antes de la regla (para eso existe S3 Batch Replication).
  • kms/secreto-2.txt no se replica: los objetos SSE-KMS requieren activar SseKmsEncryptedObjects en la regla y dar permisos de KMS al rol.

Paso 8: presigned URL

Genera una URL de descarga válida 5 minutos y úsala sin credenciales:

URL=$(aws s3 presign "s3://$B/notas.txt" --expires-in 300)
curl -s "$URL"

En CloudShell la URL se firma con credenciales temporales, así que caducará antes si caducan esas credenciales. Con un usuario de IAM y SigV4 podrías llegar a 7 días.

Paso 9: Object Lock en governance mode

Object Lock se activa al crear el bucket (y activa el versionado).

aws s3api create-bucket --bucket "$BL" --region eu-south-2 \
  --create-bucket-configuration LocationConstraint=eu-south-2 \
  --object-lock-enabled-for-bucket

echo "registro de auditoría" > /tmp/auditoria.txt
aws s3api put-object --bucket "$BL" --key auditoria.txt --body /tmp/auditoria.txt
VID=$(aws s3api list-object-versions --bucket "$BL" --prefix auditoria.txt \
  --query 'Versions[0].VersionId' --output text)

HASTA=$(date -u -d '+1 day' +%Y-%m-%dT%H:%M:%SZ)
aws s3api put-object-retention --bucket "$BL" --key auditoria.txt --version-id "$VID" \
  --retention "{\"Mode\":\"GOVERNANCE\",\"RetainUntilDate\":\"$HASTA\"}"

Intenta borrar esa versión (debe fallar con AccessDenied):

aws s3api delete-object --bucket "$BL" --key auditoria.txt --version-id "$VID"

Con el permiso s3:BypassGovernanceRetention (lo tienes como administrador) sí puedes saltarte el modo governance:

aws s3api delete-object --bucket "$BL" --key auditoria.txt --version-id "$VID" \
  --bypass-governance-retention

Comprueba que funciona

  • list-object-versions muestra varias versiones de notas.txt y pudiste recuperarlo tras borrarlo.
  • La petición por HTTP devolvió AccessDenied.
  • head-object de kms/secreto.txt muestra aws:kms.
  • El ETag de grande.bin termina en -N.
  • nuevo.txt está en el bucket de Irlanda con clase STANDARD_IA; los objetos anteriores a la regla, no.
  • La presigned URL descargó el fichero sin credenciales.
  • El borrado de la versión bloqueada falló sin --bypass-governance-retention.

Limpieza

Los buckets versionados no se pueden borrar hasta vaciar todas las versiones y delete markers. Esta función lo hace (usa jq, que viene en CloudShell):

vaciar_bucket() {
  local b="$1" region="$2"
  for campo in Versions DeleteMarkers; do
    local lote
    lote=$(aws s3api list-object-versions --bucket "$b" --region "$region" --output json \
      --query "{Objects: ${campo}[].{Key: Key, VersionId: VersionId}, Quiet: \`true\`}")
    if [ "$(echo "$lote" | jq '.Objects | length')" -gt 0 ]; then
      aws s3api delete-objects --bucket "$b" --region "$region" \
        --delete "$lote" --bypass-governance-retention
    fi
  done
}

aws s3api delete-bucket-replication --bucket "$B"
vaciar_bucket "$B" eu-south-2
vaciar_bucket "$BR" eu-west-1
vaciar_bucket "$BL" eu-south-2

aws s3api delete-bucket --bucket "$B" --region eu-south-2
aws s3api delete-bucket --bucket "$BR" --region eu-west-1
aws s3api delete-bucket --bucket "$BL" --region eu-south-2

aws iam delete-role-policy --role-name lab05-replicacion --policy-name replica
aws iam delete-role --role-name lab05-replicacion

rm -f /tmp/grande.bin /tmp/trozo.bin /tmp/*.json /tmp/*.txt
aws s3 ls | grep lab05 || echo "Sin buckets del lab05"

Si vaciar_bucket deja algo (más de 1.000 versiones, por ejemplo), repítela hasta que delete-bucket funcione.

Preguntas para pensar como arquitecto

1. ¿Por qué la réplica en Irlanda no protege frente a un borrado malicioso de una versión concreta en el origen, y qué añadirías para protegerte también de un administrador comprometido?

En realidad sí protege de ese caso concreto: S3 no replica los borrados de una versión concreta, así que la versión sigue en el destino. Pero un administrador comprometido podría borrar también el bucket de destino si está en la misma cuenta. Para defenderte: replica a un bucket de otra cuenta con permisos separados, activa Object Lock (compliance para datos regulados) en el destino y considera AWS Backup con Vault Lock.

2. Te piden un RPO de replicación de 15 minutos con garantía contractual. ¿Qué cambiarías en la regla?

Activar S3 Replication Time Control (S3 RTC), que replica el 99,99 % de los objetos nuevos en 15 minutos con SLA, y las métricas de replicación para vigilar el retraso en CloudWatch.

3. El bucket recibe 10 millones de ficheros de 2 KB al mes y la regla de ciclo de vida no parece ahorrar nada. ¿Por qué y qué harías?

Por defecto, los objetos de menos de 128 KB no se transicionan, y aunque lo forzaras, el coste de las peticiones de transición y los 40 KB de metadatos extra por objeto en Glacier Flexible Retrieval o Deep Archive superarían el ahorro. Agrupa los ficheros (por ejemplo, en .tar o Parquet por hora o día) antes de archivarlos, o deja los pequeños en Standard-IA/Intelligent-Tiering según el acceso.

4. Una app móvil debe subir fotos al bucket sin que los usuarios tengan credenciales de AWS ni pasar el tráfico por tus servidores. ¿Cómo?

El backend (por ejemplo, una Lambda tras API Gateway, autenticando al usuario con Cognito) genera una presigned URL de PUT de pocos minutos para una clave concreta y la app sube directamente a S3. Si los usuarios están lejos y los ficheros son grandes, añade Transfer Acceleration y multipart.

5. ¿Qué tendrías que cambiar para que los objetos cifrados con SSE-KMS también se replicaran?

En la regla, SourceSelectionCriteria con SseKmsEncryptedObjects en Enabled y, en el destino, EncryptionConfiguration con ReplicaKmsKeyID (una clave KMS de la región de destino, porque las claves KMS son regionales). Además, el rol necesita kms:Decrypt sobre la clave de origen y kms:Encrypt sobre la de destino.


Volver al módulo