Vuit mòduls han construït una arquitectura completa: sis subxarxes, dos grups d'autoescalat, cinc funcions Lambda, quatre cues, un clúster Aurora, dues taules de DynamoDB, cinc buckets, un balancejador, una distribució de CloudFront, catorze alarmes i un pipeline que desplega sense tallar el servei. Tot funciona. I res de tot això no existeix en cap fitxer: existeix perquè algú va escriure una ordre algun dia. Aquesta lliçó arregla l'asimetria que la Marta va assenyalar en tancar el mòdul 8 amb l'eina nativa d'AWS per fer-ho: CloudFormation, el servei que converteix un fitxer de text en recursos reals i manté la relació entre tots dos.

Avís de cost. CloudFormation és gratuït quan gestiona recursos d'AWS: pagues els recursos, no les piles. Només cobra per recursos de tercers i tipus privats registrats (0,0009 USD per segon de manejador). La plantilla de xarxa crea dos NAT gateway: uns 0,045 USD/hora cadascun més trànsit, uns 65 USD al mes si la deixes posada. L'apartat de neteja l'esborra amb una ordre. Dades fictícies.

Contingut

  1. L'asimetria entre codi i infraestructura
  2. Què és la infraestructura com a codi, i declaratiu enfront d'imperatiu
  3. Anatomia d'una plantilla
  4. Paràmetres: tipus, restriccions i valors des d'SSM
  5. Mappings, Conditions i funcions intrínseques
  6. Piles: cicle de vida, estats i ROLLBACK_COMPLETE
  7. Polítiques d'eliminació i protecció de terminació
  8. Conjunts de canvis i els tres tipus d'actualització
  9. Detecció de deriva
  10. La xarxa de MercadoFresco en una plantilla
  11. La capa d'aplicació que importa les sortides
  12. Diverses piles: exportacions enfront d'SSM
  13. Piles imbricades, mòduls i recursos personalitzats
  14. Importar recursos existents a una pila
  15. StackSets i desplegament des del pipeline
  16. Bones pràctiques, cfn-lint i cfn-guard
  17. Límits, cost i neteja
  18. Errors habituals i consells
  19. Exercicis
  20. Conclusió

L'asimetria entre codi i infraestructura

La Marta fa inventari en una tarda i apunta quatre fets:

Fet Evidència Conseqüència
Ningú no pot recrear l'entorn No hi ha document ni script que ho descrigui Una regió nova és un projecte de setmanes
Preproducció no és igual que producció El seu SG permet 0.0.0.0/0 al 22 Els incidents no es reprodueixen a temps
No hi ha registre de per què La regla d'EventBridge no té autor ni motiu Ningú no gosa esborrar res
El pipeline es va crear a mà Un JSON al portàtil del Luis El que governa el codi no està governat

El tercer és el més car. Al mòdul 5 va aparèixer un grup de seguretat amb una regla cap a un rang d'IP que ningú no reconeixia i ningú no va esborrar: podia ser d'un proveïdor. Aquest és el cost real de no tenir infraestructura com a codi: no és que no la puguis crear; és que no la pots canviar amb confiança.

Què és la infraestructura com a codi: declaratiu enfront d'imperatiu

Consisteix a descriure la infraestructura en fitxers de text versionats a Git i deixar que una eina faci coincidir la realitat amb la descripció. No és "automatitzar la creació": és que el fitxer sigui la font de veritat. D'aquí en surten quatre propietats:

  • Reproductibilitat. El mateix fitxer produeix el mateix resultat en una altra regió, en un altre compte o d'aquí a sis mesos.
  • Revisió. La infraestructura passa per una pull request com el codi de 08-01: un canvi en un grup de seguretat es comenta, s'aprova i queda amb autor, data i motiu.
  • Deriva controlada i documentació viva. L'eina sap què hauria d'existir, així que pot comparar-ho amb el que existeix —sense IaC la deriva és invisible per definició— i el fitxer no pot quedar-se obsolet respecte a la realitat, perquè és el que la crea.
  • Es pot esborrar sense por. Quan aixecar un entorn costa una ordre, apagar-lo el divendres deixa de ser un risc i passa a ser un estalvi. És la propietat que convenç els escèptics.

MercadoFresco ha fet servir fins ara l'enfocament imperatiu: ordres que descriuen com arribar a l'estat. CloudFormation és declaratiu: descrius què vols i el servei calcula els passos.

# Aixi es va crear la VPC al modul 3. Executa aquest bloc dues vegades i tindras DUES VPC.
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --profile mercadofresco-dev
aws ec2 create-subnet --vpc-id vpc-0a1b2c3d --cidr-block 10.0.1.0/24 --availability-zone eu-west-1a
aws ec2 create-internet-gateway
aws ec2 attach-internet-gateway --vpc-id vpc-0a1b2c3d --internet-gateway-id igw-04e5f6a7

L'script no sap res de l'estat anterior: cada ordre crea. Fer-lo idempotent exigiria embolcallar cada línia en una comprovació, gestionar els identificadors generats i decidir què fer si el recurs existeix amb una altra configuració; és a dir, reimplementar CloudFormation en Bash.

Aspecte Scripts de la CLI (imperatiu) CloudFormation (declaratiu)
Què descrius Els passos per arribar a l'estat L'estat desitjat
Executar dues vegades Duplica recursos o falla No fa res: ja coincideix
Actualitzar / ordre de creació Un altre script; esperes manuals Edites el fitxer; l'ordre es dedueix de les dependències
Fallada a mitges Desfer manual, de vegades impossible Reversió automàtica de la pila
Esborrar-ho tot / inventari Script invers a mà / llista manual delete-stack / la pila és l'inventari
Canvis manuals Indetectables Detecció de deriva

L'imperatiu no desapareix: continua sent el correcte per a operacions puntuals (aws s3 cp, reiniciar una instància, consultar mètriques). La regla: si el recurs ha de continuar existint demà, va en una plantilla; si és una acció que passa una vegada, va en una ordre.

Anatomia d'una plantilla

Un document YAML o JSON amb nou seccions possibles. Només Resources és obligatòria.

AWSTemplateFormatVersion: '2010-09-09'   # Versio del format. L'unica que existeix.
Description: Xarxa base de MercadoFresco. # Documentacio. Posa-la sempre.
Metadata: { 'AWS::CloudFormation::Interface': {} }   # Per a eines i consola. No desplega res.
Parameters:                              # Entrades: fan la plantilla reutilitzable entre entorns.
  Entorno: { Type: String, AllowedValues: [ desarrollo, preproduccion, produccion ] }
Mappings: { PorEntorno: { produccion: { NatPorAz: 2 } } }   # Taules de consulta estatiques
Conditions:                              # Booleans a partir de parametres; habiliten recursos.
  EsProduccion: !Equals [ !Ref Entorno, produccion ]
Transform: AWS::Serverless-2016-10-31    # Macros. SAM es la mes coneguda. Opcional.
Resources:                               # L'UNICA SECCIO OBLIGATORIA.
  Vpc: { Type: 'AWS::EC2::VPC', Properties: { CidrBlock: 10.0.0.0/16 } }
Outputs:                                 # Valors que la pila publica, per llegir o per importar.
  IdVpc: { Value: !Ref Vpc }

Falten Rules (validació de combinacions de paràmetres) i Hooks, molt menys habituals. L'estructura d'un recurs sempre és la mateixa, amb Type i Properties obligatoris i la resta opcional:

  NombreLogico:                    # Identificador a la plantilla. NO es el nom del recurs.
    Type: AWS::EC2::Subnet         # AWS::<servei>::<recurs>
    Condition: EsProduccion        # Opcionals: condicio, dependencia explicita i politiques de
    DependsOn: [ AdjuntarIgw ]     # esborrat i de reemplacament, que s'expliquen mes avall
    DeletionPolicy: Retain
    Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.1.0/24 }

Primera regla important: canviar el nom lògic d'un recurs equival a esborrar-lo i crear-ne un altre. CloudFormation identifica els recursos pel seu nom lògic, no pel que fan. Reanomenar Vpc a VpcPrincipal en una plantilla desplegada destrueix la VPC.

Paràmetres: tipus, restriccions i valors des d'SSM

Sense paràmetres caldrien tres plantilles gairebé idèntiques per als tres entorns.

Parameters:
  Entorno:                                  # Sense Default a proposit: obliga a decidir-ho cada cop.
    Type: String
    AllowedValues: [ desarrollo, preproduccion, produccion ]
  CidrVpc:
    Type: String
    Default: 10.0.0.0/16
    AllowedPattern: '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
    ConstraintDescription: Ha de ser un CIDR valid, per exemple 10.0.0.0/16.
  CapacidadMinima: { Type: Number, Default: 2, MinValue: 1, MaxValue: 10 }
  Propietario:     { Type: String, Default: marta, MinLength: 2, MaxLength: 32 }
  ClaveSsh:     { Type: 'AWS::EC2::KeyPair::KeyName' }   # Valida que existeixi ABANS de crear res
  SubredesApp:  { Type: 'List<AWS::EC2::Subnet::Id>' }   # Llista de subxarxes existents
  Contrasena:   { Type: String, NoEcho: true }           # Oculta a consola i esdeveniments. NO xifra.
  AmiTienda:                                # El valor NO es la ruta: CloudFormation resol el
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>    # parametre d'SSM i fa servir el contingut
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64

Tres coses separen una plantilla amateur d'una de professional:

  1. Els tipus específics d'AWS validen abans de començar. Amb Type: String per a una clau SSH inexistent, la pila crea mitja dotzena de recursos i falla en arribar a la instància. Amb AWS::EC2::KeyPair::KeyName falla al segon zero, sense crear res.
  2. AWS::SSM::Parameter::Value<...> desacobla la plantilla dels valors. L'AMI de la botiga canvia cada mes; la plantilla no hauria de canviar per això. MercadoFresco ho fa servir amb /mercadofresco/produccion/ami-tienda.
  3. NoEcho no xifra, només oculta. Les contrasenyes no van en paràmetres: van a Secrets Manager (04-03) i es referencien amb resolució dinàmica, que s'avalua en el desplegament sense que el valor entri mai a la plantilla:
  # La mateixa sintaxi funciona amb ssm i ssm-secure per a Parameter Store
  MasterUserPassword: '{{resolve:secretsmanager:mercadofresco/produccion/rds/mfadmin:SecretString:password}}'

Mappings, Conditions i funcions intrínseques

Mappings és una taula de consulta estàtica de dos nivells, útil per variar per regió o per entorn; es llegeix amb Fn::FindInMap. Conditions calcula booleans amb Fn::Equals, Fn::And, Fn::Or i Fn::Not.

Mappings:
  PorEntorno:
    desarrollo:    { NatPorAz: 1, Tipo: t3.small,  RetencionRegistros: 7 }
    preproduccion: { NatPorAz: 1, Tipo: t3.medium, RetencionRegistros: 30 }
    produccion:    { NatPorAz: 2, Tipo: m6i.large, RetencionRegistros: 365 }

Conditions:
  EsProduccion: !Equals [ !Ref Entorno, produccion ]
  EsPreproduccion: !Equals [ !Ref Entorno, preproduccion ]
  NecesitaAltaDisponibilidad: !Or [ !Condition EsProduccion, !Condition EsPreproduccion ]
  CrearSegundoNat: !Equals [ !FindInMap [ PorEntorno, !Ref Entorno, NatPorAz ], 2 ]

Es llegeixen amb !FindInMap [ PorEntorno, !Ref Entorno, Tipo ]. Un recurs amb Condition: CrearSegundoNat només es crea si la condició és certa: nat-mercadofresco-b existeix en producció i no en desenvolupament amb la mateixa plantilla, i això estalvia uns 32 USD al mes per entorn no productiu. Avís: fes-les servir amb moderació. Una plantilla amb dotze condicions imbricades és il·legible i impossible de raonar quan falla; si la diferència entre entorns és gran, calen plantilles diferents, no més condicions.

Funcions intrínseques

Cadascuna té forma llarga (Fn::Sub) i curta (!Sub). En YAML es fa servir la curta tret que calgui imbricar dues curtes seguides, cosa que YAML no permet.

Funció Què retorna Exemple a MercadoFresco
!Ref Identificador "natural" del recurs o valor del paràmetre !Ref Vpcvpc-0a1b2c3d
!GetAtt Un atribut del recurs !GetAtt Alb.DNSName
!Sub Cadena amb substitució de variables !Sub 'mercadofresco-${Entorno}-registros'
!Join / !Select / !Split Uneix, extreu l'element N i divideix llistes !Select [ 0, !GetAZs '' ]
!ImportValue Valor exportat per una altra pila !ImportValue mercadofresco-red-IdVpc
!FindInMap / !If Valor d'un Mappings / valor condicional !If [ EsProduccion, m6i.large, t3.small ]
!GetAZs / !Cidr AZ de la regió / subxarxes calculades !Cidr [ 10.0.0.0/16, 6, 8 ]

El que més confusió genera és !Ref, perquè retorna coses diferents segons el tipus: en una VPC o subxarxa, el seu identificador; en un bucket, el nom; en una cua d'SQS, la URL; en un tema d'SNS, l'ARN; en un rol d'IAM, el nom; en una Lambda, el nom. Aquesta taula explica el 80 % dels errors del tipus Value of property X must be of type String. Quan necessitis un ARN, gairebé sempre és !GetAtt Recurso.Arn; la secció "Return values" de cada tipus té la llista exacta.

      # Substitucio de parametres i pseudoparametres
      BucketName: !Sub 'mercadofresco-${Entorno}-registros-${AWS::AccountId}'
      # Atributs d'altres recursos: la sintaxi amb punt tambe val
      Value: !Sub 'https://${Distribucion.DomainName}/catalogo'
      # Amb mapa de variables locals, quan cal imbricar una altra funcio
      Description: !Sub
        - 'Cua de comandes per a ${Ent} al compte ${Cuenta}'
        - { Ent: !Ref Entorno, Cuenta: !Ref 'AWS::AccountId' }
      # En desenvolupament, sense instantania final; en produccio, amb ella: NoValue omet la propietat
      FinalSnapshotIdentifier: !If [ EsProduccion, !Sub 'final-${AWS::StackName}', !Ref 'AWS::NoValue' ]

Els pseudoparàmetres són variables que AWS proporciona sempre: AWS::AccountId (111122223333), AWS::Region (eu-west-1), AWS::StackName, AWS::StackId, AWS::Partition, AWS::URLSuffix i AWS::NoValue.

Piles: cicle de vida, estats i ROLLBACK_COMPLETE

Una pila és una plantilla desplegada: recursos que es creen, s'actualitzen i s'esborren com una unitat. És la unitat de gestió, de permisos i de deriva.

stateDiagram-v2
    [*] --> CREATE_IN_PROGRESS: create-stack
    CREATE_IN_PROGRESS --> CREATE_COMPLETE: tot be
    CREATE_IN_PROGRESS --> ROLLBACK_IN_PROGRESS: fallada en la creacio
    ROLLBACK_IN_PROGRESS --> ROLLBACK_COMPLETE: recursos desfets
    ROLLBACK_COMPLETE --> [*]: nomes es pot ESBORRAR
    CREATE_COMPLETE --> UPDATE_IN_PROGRESS: update-stack
    UPDATE_IN_PROGRESS --> UPDATE_COMPLETE: tot be
    UPDATE_IN_PROGRESS --> UPDATE_ROLLBACK_COMPLETE: fallada, torna enrere i es pot reintentar
    CREATE_COMPLETE --> DELETE_IN_PROGRESS: delete-stack
    DELETE_IN_PROGRESS --> DELETE_FAILED: un recurs no es deixa esborrar

L'estat que desconcerta el primer dia és ROLLBACK_COMPLETE: passa quan falla la creació inicial i CloudFormation desfà el que s'ha creat. La pila queda existint però en estat terminal: no es pot actualitzar, només esborrar i tornar a crear amb la plantilla corregida. No s'ha de confondre amb UPDATE_ROLLBACK_COMPLETE, que sí que és sa: la pila va tornar a la seva versió anterior i es pot continuar actualitzant.

aws cloudformation validate-template --template-body file://red-mercadofresco.yaml   # nomes sintaxi
aws cloudformation create-stack --stack-name mercadofresco-red-produccion \
  --template-body file://red-mercadofresco.yaml \
  --parameters ParameterKey=Entorno,ParameterValue=produccion \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion Key=Componente,Value=red \
  --enable-termination-protection --profile mercadofresco-dev --region eu-west-1
aws cloudformation wait stack-create-complete --stack-name mercadofresco-red-produccion
# El PRIMER CREATE_FAILED es la causa real; els altres son consequencies
aws cloudformation describe-stack-events --stack-name mercadofresco-red-produccion \
  --query 'StackEvents[?ResourceStatus==`CREATE_FAILED`].[LogicalResourceId,ResourceStatusReason]' \
  --output table

Les etiquetes de la pila es propaguen a tots els recursos que les admetin: és la manera més barata de complir l'etiquetatge obligatori de MercadoFresco sense repetir-lo recurs a recurs (es reprèn a 11-02). Per al dia a dia, deploy crea la pila si no existeix i l'actualitza si existeix, amb un conjunt de canvis per sota:

aws cloudformation deploy --stack-name mercadofresco-red-produccion \
  --template-file red-mercadofresco.yaml --parameter-overrides Entorno=produccion \
  --no-fail-on-empty-changeset --capabilities CAPABILITY_NAMED_IAM

--no-fail-on-empty-changeset evita que el pipeline es posi en vermell quan no hi ha res per canviar, el cas més freqüent. --capabilities és obligatori quan la plantilla crea recursos d'IAM: és una confirmació explícita que saps que estàs creant permisos (CAPABILITY_IAM, CAPABILITY_NAMED_IAM si els poses nom, CAPABILITY_AUTO_EXPAND per a macros i piles imbricades).

Polítiques d'eliminació i protecció de terminació

Per omissió, esborrar una pila esborra els seus recursos. Per a aurora-mercadofresco-pedidos i mercadofresco-catalogo-fotos això és inacceptable.

  BaseDatosPedidos:
    Type: AWS::RDS::DBCluster
    DeletionPolicy: Snapshot          # En esborrar la pila: instantania final i despres esborra
    UpdateReplacePolicy: Retain       # Si una actualitzacio exigeix reemplacament: conserva el vell
  BucketFotos:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain            # El deixa dret, orfe de la pila
    UpdateReplacePolicy: Retain
    Properties: { BucketName: mercadofresco-catalogo-fotos }
Política Valors Què fa
DeletionPolicy Delete (per omissió) Esborra el recurs en esborrar la pila
Retain / RetainExceptOnCreate El deixa existint / igual, però sí que esborra en reversió de creació
Snapshot Instantània final i esborrat (RDS, Redshift, ElastiCache, EBS, Neptune)
UpdateReplacePolicy Els mateixos valors S'aplica quan una actualització reemplaça el recurs

UpdateReplacePolicy és el que la gent oblida i el que més disgustos evita: canviar el DBClusterIdentifier d'Aurora provoca un reemplaçament, i sense ella el clúster vell —amb les dades— desapareix tan bon punt el nou està llest. La protecció de terminació és una capa més, a nivell de pila: aws cloudformation update-termination-protection --stack-name mercadofresco-datos-produccion --enable-termination-protection, i amb ella activa delete-stack falla directament. La regla de MercadoFresco: protecció de terminació a les tres piles de producció, i Retain o Snapshot en tot recurs amb estat, en qualsevol entorn.

Conjunts de canvis i els tres tipus d'actualització

Un conjunt de canvis és una simulació: CloudFormation compara la plantilla nova amb la pila actual i diu què faria, sense fer-ho. És la resposta a la pregunta que ningú no podia contestar al mòdul 8: què passarà si aplico això?

aws cloudformation create-change-set --stack-name mercadofresco-red-produccion \
  --change-set-name anadir-endpoint-dynamodb --template-body file://red-mercadofresco.yaml \
  --parameters ParameterKey=Entorno,ParameterValue=produccion --capabilities CAPABILITY_IAM

aws cloudformation describe-change-set --stack-name mercadofresco-red-produccion \
  --change-set-name anadir-endpoint-dynamodb \
  --query 'Changes[].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]' --output table

# |  Add     |  EndpointDynamoDb  |  AWS::EC2::VPCEndpoint  |  None   |
# |  Modify  |  TablaRutasApp     |  AWS::EC2::RouteTable   |  False  |
# |  Modify  |  SubredDatosA      |  AWS::EC2::Subnet       |  True   |

L'última línia és una alarma vermella: Replacement: True significa que la subxarxa s'esborra i se'n crea una altra, amb identificador nou, i tot el que en depengui se'n veu afectat. Sense mirar el conjunt de canvis, això hauria passat un divendres a les onze.

Tipus Què passa Identificador físic Exemples
Sense interrupció S'actualitza en calent Es conserva Etiquetes, regles d'un SG, mida d'un ASG, capacitat de DynamoDB
Amb interrupció S'atura i arrenca Es conserva InstanceType d'EC2, classe d'RDS sense Multi-AZ, memòria d'una Lambda
Amb reemplaçament Se'n crea un de nou i s'esborra el vell Canvia AvailabilityZone o CidrBlock de subxarxa, BucketName, DBClusterIdentifier, KeyName

La tercera fila és la que fa mal: un reemplaçament a mercadofresco-catalogo-fotos significa bucket nou buit i bucket vell esborrat amb les 40.000 fotos dins, tret que UpdateReplacePolicy ho impedeixi. La documentació de cada tipus indica, propietat a propietat, el "Update requires", i cal mirar-la abans d'escriure el canvi, no després.

flowchart LR
    A[Editar plantilla] --> B[create-change-set]
    B --> C{describe-change-set}
    C -->|Replacement False| D[execute-change-set]
    C -->|Algun Replacement True| E{Es acceptable?}
    E -->|No| F[Un altre enfocament o<br/>migrar dades abans]
    E -->|Si, amb finestra| D
    D --> G[UPDATE_COMPLETE o<br/>UPDATE_ROLLBACK_COMPLETE]

Precaució: si algú modifica la pila entre que el crees i l'executes, la simulació deixa de ser vàlida; per això els pipelines el creen i l'executen en etapes consecutives, no amb hores pel mig.

Detecció de deriva

La deriva és la diferència entre el que diu la plantilla i el que hi ha. Apareix sempre: durant un incident, a les tres de la matinada, ningú no edita una plantilla.

ID=$(aws cloudformation detect-stack-drift --stack-name mercadofresco-red-produccion \
  --query StackDriftDetectionId --output text)                       # la deteccio es asincrona
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id "$ID"
aws cloudformation describe-stack-resource-drifts --stack-name mercadofresco-red-produccion \
  --stack-resource-drift-status-filters MODIFIED DELETED

Els estats per recurs són IN_SYNC, MODIFIED, DELETED i NOT_CHECKED. Aquest últim importa: no tots els tipus admeten detecció de deriva, així que un IN_SYNC de pila no garanteix que res no hagi canviat. En trobar deriva hi ha dos camins: actualitzar la plantilla perquè reflecteixi la realitat, si el canvi manual era legítim, documentant el motiu a la PR; o reaplicar la pila per desfer-lo. Matís important: CloudFormation no té un "corregir deriva" automàtic, perquè una actualització només toca el que ha canviat a la plantilla; per forçar-ho cal alterar el valor derivat a la plantilla i actualitzar. A MercadoFresco, una tasca programada l'executa cada setmana sobre les piles de producció i publica el resultat a alertas-mercadofresco. AWS Config (05-04) té a més la regla cloudformation-stack-drift-detection-check, que fa això de manera contínua.

La xarxa de MercadoFresco en una plantilla

red-mercadofresco.yaml, al repositori mercadofresco-infra, reprodueix la xarxa del mòdul 3.

AWSTemplateFormatVersion: '2010-09-09'
Description: MercadoFresco - Xarxa base. VPC amb sis subxarxes en dues AZ, IGW, NAT i endpoint d'S3.

Parameters:
  Entorno: { Type: String, AllowedValues: [ desarrollo, preproduccion, produccion ] }
  CidrVpc: { Type: String, Default: 10.0.0.0/16 }
  Propietario: { Type: String, Default: marta }

Mappings:
  PorEntorno: { desarrollo: { NatPorAz: 1 }, preproduccion: { NatPorAz: 1 }, produccion: { NatPorAz: 2 } }

Conditions:
  # Nomes produccio te dos NAT; a la resta, app-b surt pel NAT de la AZ a.
  CrearSegundoNat: !Equals [ !FindInMap [ PorEntorno, !Ref Entorno, NatPorAz ], 2 ]

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CidrVpc
      EnableDnsSupport: true        # Necessari per a l'endpoint d'S3 i els noms d'RDS
      EnableDnsHostnames: true
      Tags: [ { Key: Name, Value: vpc-mercadofresco }, { Key: Proyecto, Value: mercadofresco },
              { Key: Entorno, Value: !Ref Entorno }, { Key: Componente, Value: red },
              { Key: Propietario, Value: !Ref Propietario }, { Key: CentroCoste, Value: plataforma } ]

  Igw: { Type: 'AWS::EC2::InternetGateway',
    Properties: { Tags: [ { Key: Name, Value: igw-mercadofresco } ] } }
  AdjuntarIgw: { Type: 'AWS::EC2::VPCGatewayAttachment',
    Properties: { VpcId: !Ref Vpc, InternetGatewayId: !Ref Igw } }

  # --- Subxarxes. !Select amb !GetAZs evita fixar 'eu-west-1a': la plantilla val en una altra regio.
  SubredPublicaA:
    Type: AWS::EC2::Subnet
    Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.0.0/24, MapPublicIpOnLaunch: true,
      AvailabilityZone: !Select [ 0, !GetAZs '' ],
      Tags: [ { Key: Name, Value: snet-mercadofresco-publica-a } ] }
  SubredPublicaB:
    Type: AWS::EC2::Subnet
    Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.1.0/24, MapPublicIpOnLaunch: true,
      AvailabilityZone: !Select [ 1, !GetAZs '' ],
      Tags: [ { Key: Name, Value: snet-mercadofresco-publica-b } ] }
  SubredAppA: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.10.0/24,
    AvailabilityZone: !Select [ 0, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-app-a } ] } }
  SubredAppB: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.11.0/24,
    AvailabilityZone: !Select [ 1, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-app-b } ] } }
  SubredDatosA: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.20.0/24,
    AvailabilityZone: !Select [ 0, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-datos-a } ] } }
  SubredDatosB: { Type: 'AWS::EC2::Subnet', Properties: { VpcId: !Ref Vpc, CidrBlock: 10.0.21.0/24,
    AvailabilityZone: !Select [ 1, !GetAZs '' ], Tags: [ { Key: Name, Value: snet-mercadofresco-datos-b } ] } }

  # --- NAT: el segon nomes existeix en produccio
  IpNatA: { Type: 'AWS::EC2::EIP', Properties: { Domain: vpc } }
  IpNatB: { Type: 'AWS::EC2::EIP', Condition: CrearSegundoNat, Properties: { Domain: vpc } }
  NatA: { Type: 'AWS::EC2::NatGateway', Properties: { AllocationId: !GetAtt IpNatA.AllocationId,
    SubnetId: !Ref SubredPublicaA, Tags: [ { Key: Name, Value: nat-mercadofresco-a } ] } }
  NatB: { Type: 'AWS::EC2::NatGateway', Condition: CrearSegundoNat,
    Properties: { AllocationId: !GetAtt IpNatB.AllocationId, SubnetId: !Ref SubredPublicaB,
      Tags: [ { Key: Name, Value: nat-mercadofresco-b } ] } }

  # --- Taules de rutes
  TablaRutasPublica: { Type: 'AWS::EC2::RouteTable',
    Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-publica } ] } }
  RutaInternet:
    Type: AWS::EC2::Route
    DependsOn: AdjuntarIgw    # Dependencia EXPLICITA: no es dedueix de les propietats
    Properties: { RouteTableId: !Ref TablaRutasPublica, DestinationCidrBlock: 0.0.0.0/0, GatewayId: !Ref Igw }
  AsociarPublicaA: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
    Properties: { SubnetId: !Ref SubredPublicaA, RouteTableId: !Ref TablaRutasPublica } }
  AsociarPublicaB: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
    Properties: { SubnetId: !Ref SubredPublicaB, RouteTableId: !Ref TablaRutasPublica } }

  TablaRutasAppA: { Type: 'AWS::EC2::RouteTable',
    Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-app-a } ] } }
  RutaNatA: { Type: 'AWS::EC2::Route', Properties: { RouteTableId: !Ref TablaRutasAppA,
    DestinationCidrBlock: 0.0.0.0/0, NatGatewayId: !Ref NatA } }
  AsociarAppA: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
    Properties: { SubnetId: !Ref SubredAppA, RouteTableId: !Ref TablaRutasAppA } }

  TablaRutasAppB: { Type: 'AWS::EC2::RouteTable',
    Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-app-b } ] } }
  RutaNatB:   # Amb segon NAT surt pel seu; si no, comparteix el de la AZ a
    Type: AWS::EC2::Route
    Properties: { RouteTableId: !Ref TablaRutasAppB, DestinationCidrBlock: 0.0.0.0/0,
      NatGatewayId: !If [ CrearSegundoNat, !Ref NatB, !Ref NatA ] }
  AsociarAppB: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
    Properties: { SubnetId: !Ref SubredAppB, RouteTableId: !Ref TablaRutasAppB } }

  # Les subxarxes de dades NO tenen ruta a 0.0.0.0/0: sense sortida a internet, per disseny (03-01).
  TablaRutasDatos: { Type: 'AWS::EC2::RouteTable',
    Properties: { VpcId: !Ref Vpc, Tags: [ { Key: Name, Value: rt-mercadofresco-datos } ] } }
  AsociarDatosA: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
    Properties: { SubnetId: !Ref SubredDatosA, RouteTableId: !Ref TablaRutasDatos } }
  AsociarDatosB: { Type: 'AWS::EC2::SubnetRouteTableAssociation',
    Properties: { SubnetId: !Ref SubredDatosB, RouteTableId: !Ref TablaRutasDatos } }

  EndpointS3:   # De tipus Gateway: no costa res i estalvia el transit per NAT de les copies a S3
    Type: AWS::EC2::VPCEndpoint
    Properties: { VpcId: !Ref Vpc, VpcEndpointType: Gateway,
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3',
      RouteTableIds: [ !Ref TablaRutasAppA, !Ref TablaRutasAppB, !Ref TablaRutasDatos ] }

Outputs:
  IdVpc: { Value: !Ref Vpc, Export: { Name: !Sub '${AWS::StackName}-IdVpc' } }
  SubredesPublicas: { Value: !Join [ ',', [ !Ref SubredPublicaA, !Ref SubredPublicaB ] ],
    Export: { Name: !Sub '${AWS::StackName}-SubredesPublicas' } }
  SubredesApp: { Value: !Join [ ',', [ !Ref SubredAppA, !Ref SubredAppB ] ],
    Export: { Name: !Sub '${AWS::StackName}-SubredesApp' } }
  SubredesDatos: { Value: !Join [ ',', [ !Ref SubredDatosA, !Ref SubredDatosB ] ],
    Export: { Name: !Sub '${AWS::StackName}-SubredesDatos' } }

Quatre comentaris. L'ordre de creació no és enlloc: CloudFormation dedueix que SubredPublicaA necessita Vpc perquè la referencia amb !Ref, i crea en paral·lel el que és independent; l'únic DependsOn és l'única dependència que no es dedueix de les propietats. !GetAZs evita fixar eu-west-1a. !If amb !Ref NatB degrada en entorns petits sense duplicar la plantilla. I les sortides s'exporten amb el prefix ${AWS::StackName}, de manera que producció i preproducció no xoquen: els noms d'exportació són únics per regió.

cfn-lint red-mercadofresco.yaml
aws cloudformation deploy --stack-name mercadofresco-red-produccion \
  --template-file red-mercadofresco.yaml --parameter-overrides Entorno=produccion Propietario=marta \
  --tags Proyecto=mercadofresco Entorno=produccion Componente=red Propietario=marta \
         CentroCoste=plataforma --profile mercadofresco-dev --region eu-west-1

Set minuts després —els NAT triguen uns dos minuts cadascun— la xarxa completa existeix. I aquesta vegada existeix també en un fitxer, amb historial a Git.

La capa d'aplicació que importa les sortides

aplicacion-mercadofresco.yaml munta l'ALB, el grup de destinació i l'ASG a sobre, sense conèixer ni un sol identificador:

Parameters:
  Entorno: { Type: String, AllowedValues: [ desarrollo, preproduccion, produccion ] }
  PilaRed: { Type: String, Default: mercadofresco-red-produccion }
  AmiTienda: { Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>',
    Default: /mercadofresco/produccion/ami-tienda }

Mappings:
  PorEntorno: { desarrollo: { Tipo: t3.small, Min: 1, Max: 2 },
    preproduccion: { Tipo: t3.medium, Min: 2, Max: 3 }, produccion: { Tipo: m6i.large, Min: 2, Max: 4 } }

Resources:
  SgAlb: { Type: 'AWS::EC2::SecurityGroup', Properties: {
    GroupDescription: Entrada HTTPS publica cap a l ALB,
    VpcId: !ImportValue { 'Fn::Sub': '${PilaRed}-IdVpc' },
    SecurityGroupIngress: [ { IpProtocol: tcp, FromPort: 443, ToPort: 443, CidrIp: 0.0.0.0/0 } ] } }

  SgTienda: { Type: 'AWS::EC2::SecurityGroup', Properties: {    # Origen per grup de seguretat i
    GroupDescription: Trafic de l ALB cap a la botiga,          # no per CIDR: el patro de 03-02
    VpcId: !ImportValue { 'Fn::Sub': '${PilaRed}-IdVpc' },
    SecurityGroupIngress: [ { IpProtocol: tcp, FromPort: 8080, ToPort: 8080,
                              SourceSecurityGroupId: !Ref SgAlb } ] } }

  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: { Name: !Sub 'alb-mercadofresco-tienda-${Entorno}', Scheme: internet-facing,
      Type: application, SecurityGroups: [ !Ref SgAlb ],
      Subnets: !Split [ ',', !ImportValue { 'Fn::Sub': '${PilaRed}-SubredesPublicas' } ] }

  GrupoDestino:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties: { Name: !Sub 'tg-mercadofresco-tienda-${Entorno}', Port: 8080, Protocol: HTTP,
      VpcId: !ImportValue { 'Fn::Sub': '${PilaRed}-IdVpc' },
      HealthCheckPath: /salud, HealthCheckIntervalSeconds: 15,
      HealthyThresholdCount: 2, UnhealthyThresholdCount: 3,
      TargetGroupAttributes: [ { Key: deregistration_delay.timeout_seconds, Value: '30' } ] }

  PlantillaLanzamiento:
    Type: AWS::EC2::LaunchTemplate
    Properties: { LaunchTemplateName: !Sub 'lt-mercadofresco-tienda-${Entorno}',
      LaunchTemplateData: { ImageId: !Ref AmiTienda, SecurityGroupIds: [ !Ref SgTienda ],
        InstanceType: !FindInMap [ PorEntorno, !Ref Entorno, Tipo ],
        IamInstanceProfile: { Name: rol-mercadofresco-tienda },
        MetadataOptions: { HttpTokens: required } } }    # IMDSv2 obligatori (04-01)

  Asg:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: !Sub 'asg-mercadofresco-tienda-${Entorno}'
      MinSize: !FindInMap [ PorEntorno, !Ref Entorno, Min ]
      MaxSize: !FindInMap [ PorEntorno, !Ref Entorno, Max ]
      HealthCheckType: ELB
      HealthCheckGracePeriod: 120
      VPCZoneIdentifier: !Split [ ',', !ImportValue { 'Fn::Sub': '${PilaRed}-SubredesApp' } ]
      TargetGroupARNs: [ !Ref GrupoDestino ]
      LaunchTemplate: { LaunchTemplateId: !Ref PlantillaLanzamiento,
        Version: !GetAtt PlantillaLanzamiento.LatestVersionNumber }
    UpdatePolicy:   # Com substituir les instancies quan canvia la plantilla de llancament
      AutoScalingRollingUpdate: { MinInstancesInService: 2, MaxBatchSize: 1, PauseTime: PT5M }

Outputs:
  DnsAlb: { Value: !GetAtt Alb.DNSName }

Dos detalls. YAML no admet dues etiquetes curtes seguides, així que !ImportValue amb !Sub a dins obliga a escriure la interna en forma llarga ('Fn::Sub'); l'error que produeix oblidar-ho, unsupported structure, no ajuda gens. I UpdatePolicy decideix què passa quan canvia la plantilla de llançament: sense ella, CloudFormation actualitza l'ASG i no toca les instàncies existents, així que el canvi no s'aplica fins al següent escalat. És el desplegament per renovació de 08-03 aplicat a la infraestructura.

Diverses piles: exportacions enfront d'SSM

Una pila amb 300 recursos triga 40 minuts, reverteix sencera davant de qualsevol fallada i no la pot tocar una persona sense coordinar-se amb les altres dues. La partició segueix dos criteris: per cicle de vida (el que canvia junt va junt) i per propietat (cada pila té un amo clar).

Pila Contingut Freqüència Propietari
mercadofresco-red-<entorno> VPC, subxarxes, NAT, rutes, endpoints Molt baixa Marta
mercadofresco-datos-<entorno> Aurora, DynamoDB, ElastiCache, buckets Baixa Marta
mercadofresco-integracion-<entorno> Cues, DLQ, temes, bus, regles, Step Functions Mitjana Luis
mercadofresco-aplicacion-<entorno> ALB, grups de destinació, ASG, Lambdes Alta Luis
mercadofresco-observabilidad-<entorno> Alarmes, panells, filtres de mètriques Mitjana Marta
Criteri Exportacions i Fn::ImportValue Paràmetres d'SSM
Acoblament Fort: no es pot canviar ni esborrar una exportació en ús Feble: és només un valor llegit
Àmbit Mateix compte i mateixa regió Qualsevol: creua comptes i regions
Actualitzar el valor / ordre d'esborrat Impossible mentre s'importi; esborrat imposat Lliure; l'ordre queda a la disciplina
Llegibilitat Alta: la dependència és explícita Menor: cal saber quina ruta llegeix cada pila
Risc típic Quedar-se sense poder canviar la xarxa Que algú canviï el valor i ningú no se n'assabenti

La primera fila és la clau: una exportació en ús és un forrellat. Això és bo —evita trencar coses— i és dolent —bloqueja evolucions—. La regla de MercadoFresco: exportacions per a l'estructural que no canviarà (VPC i subxarxes), i paràmetres d'SSM per a tota la resta i sempre que el valor creui comptes o regions (ARN de cues, noms de taules, àlies de claus).

  ParametroArnColaPedidos:            # Publica des de la pila d'integracio
    Type: AWS::SSM::Parameter
    Properties: { Type: String, Value: !GetAtt ColaPedidos.Arn,
      Name: !Sub '/mercadofresco/${Entorno}/integracion/arn-cola-pedidos' }
# I la pila d'aplicacio el consumeix com a parametre:
#   ArnColaPedidos: { Type: 'AWS::SSM::Parameter::Value<String>',
#     Default: /mercadofresco/produccion/integracion/arn-cola-pedidos }

Piles imbricades, mòduls i recursos personalitzats

Les piles imbricades inclouen una plantilla dins d'una altra com si fos un recurs; la filla ha de ser a S3 (mercadofresco-artefactos serveix) i les seves sortides es llegeixen amb !GetAtt PilaSubredes.Outputs.SubredesApp.

  PilaSubredes:
    Type: AWS::CloudFormation::Stack
    Properties: { Parameters: { IdVpc: !Ref Vpc, Entorno: !Ref Entorno },
      TemplateURL: https://s3.eu-west-1.amazonaws.com/mercadofresco-artefactos/plantillas/subredes.yaml }
Enfocament Quan Inconvenients
Piles separades Cicles de vida i propietaris diferents Cal coordinar l'ordre
Piles imbricades Un mateix cicle de vida amb parts repetitives Depuració difícil; pujar a S3; CAPABILITY_AUTO_EXPAND
Mòduls Un patró idèntic en moltes plantilles Cal registrar-los per compte i regió; poc estesos

Tard o d'hora apareix alguna cosa que CloudFormation no sap crear: un usuari en una eina externa, una càrrega inicial de dades. Un recurs personalitzat delega en una Lambda: CloudFormation li envia un esdeveniment amb RequestType i una URL prefirmada, i la funció ha de respondre a aquesta URL.

import json, urllib.request

def respondre(esdeveniment, context, estat, dades=None, motiu=""):
    """Respondre a la URL prefirmada. Si no es crida, la pila es queda una hora penjada."""
    cos = json.dumps({"Status": estat,                  # SUCCESS o FAILED
        "Reason": motiu or f"Veure registres: {context.log_stream_name}",
        "PhysicalResourceId": esdeveniment.get("PhysicalResourceId", context.log_stream_name),
        "StackId": esdeveniment["StackId"], "RequestId": esdeveniment["RequestId"],
        "LogicalResourceId": esdeveniment["LogicalResourceId"], "Data": dades or {}}).encode()
    peticio = urllib.request.Request(esdeveniment["ResponseURL"], data=cos, method="PUT")
    peticio.add_header("content-type", "")
    urllib.request.urlopen(peticio)

def handler(esdeveniment, context):
    try:
        if esdeveniment["RequestType"] == "Delete":
            return respondre(esdeveniment, context, "SUCCESS")   # Res per desfer
        carregar_categories(esdeveniment["ResourceProperties"]["Tabla"])
        respondre(esdeveniment, context, "SUCCESS", {"Categorias": "12"})
    except Exception as error:                              # Capturar SEMPRE: una excepcio sense
        respondre(esdeveniment, context, "FAILED", motiu=str(error))   # respondre penja la pila 1 hora

Els proveïdors de recursos són l'alternativa formal: es registra un tipus nou, per exemple MercadoFresco::Facturacion::Cliente, amb el seu esquema JSON i els seus manejadors, i a partir d'aquí es fa servir com qualsevol altre recurs, amb deriva i validació incloses. Costa més i es paga per segon de manejador, però el registre públic ja porta tipus de tercers —Datadog, MongoDB Atlas, GitHub— que s'activen en un clic.

Importar recursos existents a una pila

Aquesta és l'operació que MercadoFresco necessita de veritat, perquè la seva infraestructura ja existeix. L'alternativa ingènua —crear piles noves i esborrar el vell— implicaria migrar dades i una finestra de tall. La importació adopta els recursos existents sense tocar-los. Quatre passos:

  1. Escriure la plantilla que descriu exactament el que existeix. Aquí no s'improvisa: si diu /health i el grup de destinació real té /salud, la importació passa i el següent update-stack canvia la realitat sense avisar.
  2. Afegir DeletionPolicy: Retain a tots els recursos a importar —és obligatori— i preparar el fitxer d'identificadors, que aparella cada nom lògic amb el recurs real.
  3. Crear i executar un conjunt de canvis de tipus IMPORT.
[
  { "ResourceType": "AWS::EC2::VPC", "LogicalResourceId": "Vpc",
    "ResourceIdentifier": { "VpcId": "vpc-0a1b2c3d4e5f6a7b8" } },
  { "ResourceType": "AWS::EC2::Subnet", "LogicalResourceId": "SubredAppA",
    "ResourceIdentifier": { "SubnetId": "subnet-0c1d2e3f4a5b6c7d8" } },
  { "ResourceType": "AWS::SQS::Queue", "LogicalResourceId": "ColaPedidos", "ResourceIdentifier": {
      "QueueUrl": "https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-pedidos" } }
]
aws cloudformation create-change-set --stack-name mercadofresco-red-produccion \
  --change-set-name importar-red-existente --change-set-type IMPORT \
  --resources-to-import file://recursos-a-importar.json \
  --template-body file://red-mercadofresco.yaml \
  --parameters ParameterKey=Entorno,ParameterValue=produccion
aws cloudformation execute-change-set --stack-name mercadofresco-red-produccion \
  --change-set-name importar-red-existente
aws cloudformation detect-stack-drift --stack-name mercadofresco-red-produccion   # PAS OBLIGATORI

La importació no comprova que la plantilla coincideixi amb la realitat: només que els recursos existeixin i siguin del tipus declarat. La deriva immediatament després és el que revela les diferències. La Marta en troba tres:

Recurs Deriva Decisió
SubredAppB Etiqueta Propietario absent Corregir la realitat: aplicar la plantilla
TablaRutasDatos Ruta 0.0.0.0/0 cap al NAT que ningú no recordava Investigar i treure: les dades no surten
EndpointS3 Política més restrictiva que la plantilla Corregir la plantilla: la realitat era la bona

El tercer cas explica per què l'ordre importa: primer importar i detectar, després corregir. Escriure la plantilla "com hauria de ser" i aplicar-la directament hauria relaxat una política de seguretat sense que ningú se n'assabentés. MercadoFresco importa per capes, començant per la xarxa —la més estable— i deixant la de dades per al final, amb finestra acordada i còpia recent d'Aurora.

StackSets i desplegament des del pipeline

Un StackSet desplega la mateixa plantilla en un conjunt de comptes i regions des d'una única operació. Avui MercadoFresco té un compte, així que la seva utilitat és limitada; a 09-04, amb quatre, serà la peça que garanteixi que tot compte nou neix amb la línia base posada: trail, gravador de Config, alarmes de facturació i rols.

aws cloudformation create-stack-set --stack-set-name ss-mercadofresco-linea-base \
  --template-body file://linea-base.yaml --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true --capabilities CAPABILITY_NAMED_IAM

SELF_MANAGED exigeix crear a mà dos rols d'IAM; SERVICE_MANAGED es recolza en Organizations i no requereix res. Amb --auto-deployment Enabled=true, un compte afegit a una OU destinació rep la pila sol. I la conclusió pràctica del mòdul 8: si la infraestructura és codi, es desplega des del pipeline. CodePipeline té acció nativa amb quatre modes; el patró de MercadoFresco separa simulació d'execució amb una aprovació al mig.

flowchart TB
    A[Font: mercadofresco-infra] --> B[Construccio:<br/>cfn-lint + cfn-guard]
    B --> C[CHANGE_SET_REPLACE]
    C --> D[Publicar el resum<br/>del conjunt de canvis]
    D --> E{Aprovacio de la Marta}
    E -->|Rebutja| G[Fi: cap canvi aplicat]
    E -->|Aprova| F[CHANGE_SET_EXECUTE] --> H[Verificar deriva i sortides]
- name: InfraestructuraProduccion
  actions:
    - name: PrepararCambio
      actionTypeId: { category: Deploy, owner: AWS, provider: CloudFormation, version: '1' }
      runOrder: 1
      configuration:
        ActionMode: CHANGE_SET_REPLACE
        StackName: mercadofresco-red-produccion
        ChangeSetName: cambio-desde-pipeline
        TemplatePath: 'Infra::plantillas/red-mercadofresco.yaml'
        TemplateConfiguration: 'Infra::parametros/produccion.json'
        RoleArn: arn:aws:iam::111122223333:role/rol-cloudformation-mercadofresco
        Capabilities: CAPABILITY_NAMED_IAM
    - { name: AprobacionMarta, runOrder: 2,
        actionTypeId: { category: Approval, owner: AWS, provider: Manual, version: '1' } }
    - name: EjecutarCambio
      actionTypeId: { category: Deploy, owner: AWS, provider: CloudFormation, version: '1' }
      runOrder: 3
      configuration: { ActionMode: CHANGE_SET_EXECUTE, StackName: mercadofresco-red-produccion,
        ChangeSetName: cambio-desde-pipeline }

RoleArn és clau: CloudFormation assumeix aquest rol per crear els recursos, no els permisos de qui llança el pipeline, així que el Luis pot disparar un desplegament d'infraestructura sense tenir ell permís per crear VPC. I TemplateConfiguration apunta a un JSON per entorn al repositori amb Parameters i Tags: amb això, la diferència entre preproducció i producció deixa de ser un misteri i passa a ser un fitxer que es compara amb diff en deu segons.

Bones pràctiques, cfn-lint i cfn-guard

Sis regles al README de mercadofresco-infra:

  1. Plantilles petites i amb una responsabilitat. Més de 400 línies o 80 recursos: es parteix, per cicle de vida.
  2. Paràmetres mínims. Cada paràmetre és una decisió que algú pot prendre malament. El que no varia entre entorns és una constant, no un paràmetre.
  3. Res de valors màgics, i noms físics només quan calen. Ni AMI a mà, ni CIDR repetits, ni ARN a pèl: !Sub, Mappings i SSM cobreixen tots els casos. I fixar BucketName o GroupName impedeix actualitzacions sense interrupció i bloqueja desplegar dues vegades la mateixa plantilla en un compte.
  4. Tot passa per PR i per validació automàtica. Cap deploy manual contra producció; una emergència obliga a obrir la PR de reconciliació el mateix dia.
pip install cfn-lint && cfn-lint plantillas/*.yaml
# E3002 Invalid Property Resources/GrupoDestino/Properties/HealthCheckPeriod
# W2001 Parameter Propietario not used
cfn-guard validate --data plantillas/ --rules reglas/mercadofresco.guard
let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]
rule buckets_cifrados when %buckets !empty {
  %buckets.Properties.BucketEncryption exists
  <<Tot bucket de MercadoFresco ha de declarar xifratge. Veure 04-02.>>
}
let sgs = Resources.*[ Type == 'AWS::EC2::SecurityGroup' ]
rule sin_ssh_abierto when %sgs !empty {
  %sgs.Properties.SecurityGroupIngress[*] { when FromPort == 22 { CidrIp != '0.0.0.0/0' } }
}

Aquesta segona regla és la que hauria impedit la diferència entre preproducció i producció del principi de la lliçó. Totes dues eines s'executen a build-mercadofresco-integracion (08-02) i fallen la construcció: no són avisos.

Límits, cost i neteja

Límit Valor Què fer en arribar-hi
Recursos per pila / piles per compte i regió 500 / 2.000 Partir en diverses piles o imbricar; consolidar
Paràmetres / Sortides 200 / 200 Agrupar valors a SSM
Plantilla local / a S3 / imbricació 51.200 bytes / 1 MB / 5 nivells --template-url; partir; replantejar

CloudFormation no cobra per gestionar recursos d'AWS; cobra 0,0009 USD per segon de manejador en recursos de tercers i tipus privats, amb 1.000 operacions mensuals gratuïtes. El cost real és sempre el dels recursos creats.

# Esborrar en ordre invers a les dependencies: primer els consumidors
aws cloudformation delete-stack --stack-name mercadofresco-aplicacion-pruebas
aws cloudformation wait stack-delete-complete --stack-name mercadofresco-aplicacion-pruebas
aws cloudformation delete-stack --stack-name mercadofresco-red-pruebas
aws cloudformation wait stack-delete-complete --stack-name mercadofresco-red-pruebas
# Les IP elastiques orfes es cobren
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output table

Si l'esborrat queda en DELETE_FAILED, la causa gairebé sempre és un bucket amb objectes dins o una interfície de xarxa creada a mà a la subxarxa; --retain-resources permet acabar l'esborrat deixant fora aquests recursos concrets.

Errors Habituals i Consells

Error: reanomenar un recurs lògic i perdre el recurs. Canviar Vpc per VpcPrincipal no reanomena: esborra la VPC i en crea una altra. Consell: els noms lògics són immutables a la pràctica; si cal reanomenar, es fa en dos passos amb DeletionPolicy: Retain més una importació posterior.

Error: desplegar sense mirar el conjunt de canvis. És fusionar una PR sense llegir el diff. Consell: que el pipeline el generi i el publiqui abans de l'aprovació, parant atenció a la columna Replacement. Error: no entendre ROLLBACK_COMPLETE. És un estat terminal d'una creació fallida. Consell: esborra-la i torna-la a crear, però mira els esdeveniments abans d'esborrar, filtrant CREATE_FAILED: el primer és la causa, els altres són conseqüències.

Error: contrasenyes en paràmetres amb NoEcho creient que estan segures, o exportacions per a tot. Amb vint exportacions creuades la xarxa es torna immodificable. Consell: {{resolve:secretsmanager:...}} o {{resolve:ssm-secure:...}} per als secrets, exportacions només per a l'estructural i SSM per a la resta.

Error: creure que la importació valida la plantilla. No ho fa: detecció de deriva just després de cada importació, sense excepció.

Consell: fes servir --role-arn en tots els desplegaments i una política de pila en els recursos amb dades. El primer és la diferència entre "qualsevol que pugui desplegar pot crear qualsevol cosa" i "només la plantilla revisada crea el que declara"; el segon, amb set-stack-policy, denega Update:Replace i Update:Delete sobre recursos concrets, un cinturó per als elàstics d'UpdateReplacePolicy.

Consell: mira la plantilla desplegada, no només la del repositori. get-template --template-stage Processed la retorna amb macros i transformacions ja aplicades; és el que cal revisar quan el resultat no coincideix amb el que et pensaves escriure.

Consell: Description a la plantilla, a cada paràmetre i a cada sortida. És la documentació viva del principi, i costa trenta segons.

Exercicis

Exercici 1: la plantilla d'integració

Escriu integracion-mercadofresco.yaml: la cua cola-mercadofresco-pedidos amb la seva DLQ mercadofresco-pedidos-fallidos i 5 intents, el tema mercadofresco-pedido-confirmado, la subscripció de la cua al tema i la publicació de l'ARN de la cua a Parameter Store. Requisits: xifratge amb alias/mercadofresco-datos, retenció de 14 dies a la DLQ i 4 a la cua, sufix d'entorn als noms, etiquetatge obligatori complet i DeletionPolicy adequada a la DLQ. Indica quin tipus d'actualització provocaria canviar VisibilityTimeout i quin canviar QueueName.

Exercici 2: adoptar l'ALB existent

alb-mercadofresco-tienda i tg-mercadofresco-tienda porten vuit mesos en producció, creats a mà. La Marta els vol adoptar a mercadofresco-aplicacion-produccion sense tallar el servei. Descriu el procediment: què recopilar abans i amb quines ordres, què ha de portar la plantilla obligatòriament, com es construeix el fitxer d'identificadors, quines ordres s'executen i en quin ordre, i què es fa immediatament després. Explica què passaria si la plantilla declarés HealthCheckIntervalSeconds: 30 quan el grup de destinació real té 15, i en quin moment exacte es manifestaria.

Exercici 3: partir el monòlit de plantilla

Un company ha escrit una plantilla de 780 línies amb 96 recursos: VPC i subxarxes, Aurora, dues taules de DynamoDB, ElastiCache, ALB, ASG, quatre cues, bus d'esdeveniments, dotze alarmes i tres buckets. Triga 41 minuts i qualsevol fallada ho reverteix tot; la setmana passada, un error en una alarma va revertir també un canvi de subxarxa correcte. Proposa la partició: quines piles, amb quin contingut i amb quin criteri; per a cada frontera, si faries servir exportació o SSM i per què; l'ordre de desplegament i el d'esborrat; i què hauria passat la setmana passada amb la teva partició.

Solucions

Solució 1

Resources:
  ColaPedidosFallidos:
    Type: AWS::SQS::Queue
    DeletionPolicy: Retain          # La DLQ guarda missatges sense processar: no s'esborra amb la pila
    UpdateReplacePolicy: Retain
    Properties:
      QueueName: !Sub 'mercadofresco-pedidos-fallidos-${Entorno}'
      MessageRetentionPeriod: 1209600        # 14 dies, el maxim
      KmsMasterKeyId: alias/mercadofresco-datos
      Tags: &etiquetas                       # Ancora YAML: CloudFormation admet alies i evita repetir
        - { Key: Proyecto, Value: mercadofresco }
        - { Key: Entorno, Value: !Ref Entorno }
        - { Key: Componente, Value: integracion }
        - { Key: Propietario, Value: luis }
        - { Key: CentroCoste, Value: plataforma }

  ColaPedidos:
    Type: AWS::SQS::Queue
    Properties: { QueueName: !Sub 'cola-mercadofresco-pedidos-${Entorno}',
      MessageRetentionPeriod: 345600, VisibilityTimeout: 180,    # 4 dies de retencio
      KmsMasterKeyId: alias/mercadofresco-datos, Tags: *etiquetas,
      RedrivePolicy: { deadLetterTargetArn: !GetAtt ColaPedidosFallidos.Arn, maxReceiveCount: 5 } }

  TemaPedidoConfirmado:
    Type: AWS::SNS::Topic
    Properties: { TopicName: !Sub 'mercadofresco-pedido-confirmado-${Entorno}',
      KmsMasterKeyId: alias/mercadofresco-datos, Tags: *etiquetas }

  SuscripcionColaAlTema:
    Type: AWS::SNS::Subscription
    Properties: { TopicArn: !Ref TemaPedidoConfirmado,   # !Ref d'un tema retorna l'ARN
      Protocol: sqs, Endpoint: !GetAtt ColaPedidos.Arn, RawMessageDelivery: true }

  PoliticaColaParaSns:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues: [ !Ref ColaPedidos ]             # !Ref d'una cua retorna la URL: es el que cal aqui
      PolicyDocument:
        Version: '2012-10-17'
        Statement: [ { Effect: Allow, Action: 'sqs:SendMessage', Resource: !GetAtt ColaPedidos.Arn,
            Principal: { Service: sns.amazonaws.com },
            Condition: { ArnEquals: { 'aws:SourceArn': !Ref TemaPedidoConfirmado } } } ]

  ParametroArnCola:
    Type: AWS::SSM::Parameter
    Properties: { Type: String, Value: !GetAtt ColaPedidos.Arn,
      Name: !Sub '/mercadofresco/${Entorno}/integracion/arn-cola-pedidos' }

Canviar VisibilityTimeout és sense interrupció: SQS ho aplica en calent i la URL no canvia. Canviar QueueName és amb reemplaçament: SQS no permet reanomenar, així que es crea una cua nova i s'esborra la vella amb els missatges dins. És un exemple perfecte de per què fixar noms físics té cost i de per què la DLQ porta Retain. I un detall fàcil d'oblidar: sense la política de cua, SNS no pot lliurar i els missatges es perden en silenci; la condició aws:SourceArn evita que qualsevol altre tema del compte hi escrigui, el mínim privilegi de 04-01 aplicat a recursos.

Solució 2

Abans: llegir la configuració real, mai escriure-la de memòria.

aws elbv2 describe-load-balancers --names alb-mercadofresco-tienda > alb-real.json
aws elbv2 describe-target-groups --names tg-mercadofresco-tienda > tg-real.json
aws elbv2 describe-listeners --load-balancer-arn "$ARN_ALB" > listeners-real.json
aws elbv2 describe-target-group-attributes --target-group-arn "$ARN_TG" > tg-attrs.json

La plantilla s'escriu a partir d'aquests fitxers, propietat per propietat, inclosos els atributs poc evidents: deregistration_delay.timeout_seconds, stickiness.enabled, la política TLS de l'oient i el certificat d'ACM. Obligatori: DeletionPolicy: Retain a l'ALB, al grup de destinació i a l'oient; sense ella la importació es rebutja, i convé afegir UpdateReplacePolicy: Retain. El fitxer d'identificadors aparella LogicalResourceId amb LoadBalancerArn i TargetGroupArn, fent servir l'ARN complet (arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/alb-mercadofresco-tienda/a1b2c3).

Ordre: cfn-lint; create-change-set amb --change-set-type IMPORT; describe-change-set per confirmar que totes les accions són Import i cap no és Add o Modify —una sola Modify significa que la plantilla no coincideix amb la realitat i cal corregir-la abans—; execute-change-set; wait stack-import-complete. Immediatament després: detecció de deriva, resolent cas a cas si mana la plantilla o la realitat. El cas de 30 enfront de 15. La importació passa sense queixar-se: només verifica existència i tipus. La deriva ho marcaria com a MODIFIED. I si ningú no ho mira, el problema es manifesta en el següent update-stack qualsevol, encara que no tingui res a veure amb el grup de destinació: CloudFormation aplica la plantilla completa i l'interval passa a 30 segons. A la pràctica, l'ALB triga el doble a detectar una instància caiguda —de 30 a 60 segons amb dues comprovacions fallides—, just el tipus de degradació silenciosa que ningú no relaciona amb un desplegament d'una altra cosa. Aquest és l'argument sencer a favor del pas de deriva.

Solució 3

Cinc piles, les mateixes de la taula de la lliçó: red (gairebé immutable, tot en depèn), datos (estat, esborrat catastròfic, requereix protecció), integracion (sense estat durador, propietari el Luis), aplicacion (el que més canvia) i observabilidad (canvia amb els incidents, risc nul). Fronteres i ordre. Xarxa → totes: exportació, perquè els identificadors de VPC i subxarxes són estructurals, no canviaran, i volem el forrellat: ningú no ha de poder esborrar la xarxa mentre hi hagi alguna cosa a sobre. Dades → aplicació (endpoint d'Aurora, noms de taules): SSM, perquè canvien amb més facilitat de la que sembla —una migració a Serverless v2, un endpoint de lectura— i una exportació bloquejaria justament això. Integració → aplicació: SSM, a més perquè a 09-04 aquestes piles viuran en comptes diferents, on les exportacions no funcionen. Totes → observabilitat: SSM; la pila que menys importa i més canvia no ha de poder bloquejar ningú.

L'ordre de desplegament és xarxa → dades i integració en paral·lel → aplicació → observabilitat, i el d'esborrat, l'invers exacte: les exportacions l'imposen soles en el cas de la xarxa, i a la resta cal respectar-lo per disciplina perquè SSM no imposa res.

La setmana passada. L'error a l'alarma hauria fet fallar únicament mercadofresco-observabilidad, que desplega en menys de dos minuts i no té recursos crítics; el canvi de subxarxa, a mercadofresco-red, ja estaria aplicat i no s'hauria revertit. L'àmbit de la fallada passa de 96 recursos a 12, i els 41 minuts passen a quatre desplegaments d'entre 2 i 12 minuts, alguns en paral·lel. Un matís honest: la partició no és gratis. Introdueix ordre de desplegament, obliga a mantenir les fronteres i fa que un canvi que toca dues capes necessiti dues piles. La regla que ho justifica és la mateixa de 08-05 amb els desplegaments: l'àmbit d'una fallada ha de ser proporcional al risc del canvi, i barrejar una alarma amb una subxarxa viola aquesta regla.

Conclusió

La infraestructura de MercadoFresco és per fi en fitxers. red-mercadofresco.yaml descriu la VPC, les sis subxarxes, el gateway d'internet, els NAT —dos en producció i un a la resta, amb la mateixa plantilla— i l'endpoint d'S3, amb historial a Git; aplicacion-mercadofresco.yaml munta a sobre l'ALB, el grup de destinació i el grup d'autoescalat sense conèixer ni un sol identificador, important les sortides de la primera. I tens l'anatomia d'una plantilla, amb paràmetres que validen abans de començar gràcies als tipus específics d'AWS, valors que arriben des de Parameter Store sense tocar la plantilla i secrets resolts amb {{resolve:secretsmanager:...}} en lloc del fals amic que és NoEcho. Tens les funcions intrínseques i la taula que evita l'error més freqüent: que !Ref retorna la URL d'una cua, l'ARN d'un tema i el nom d'un rol, i que gairebé sempre el que busques és !GetAtt Recurso.Arn. I tens el cicle de vida d'una pila, amb l'estat que desconcerta el primer dia: ROLLBACK_COMPLETE, que només es pot esborrar.

El que de veritat canvia la manera de treballar són tres mecanismes. Els conjunts de canvis, que contesten per escrit la pregunta que al mòdul 8 ningú no podia contestar —què passarà si aplico això?— i que mostren la columna Replacement que separa un canvi rutinari de la destrucció d'una subxarxa. Les polítiques d'eliminació i de reemplaçament més la protecció de terminació, l'única cosa que s'interposa entre una ordre i les dades d'Aurora. I la detecció de deriva, que converteix els canvis manuals d'invisibles en un informe setmanal. Amb la importació de recursos existents, MercadoFresco no recrea res: adopta el que ja funciona, capa a capa, començant per la xarxa, i executa la deriva just després —el pas que ningú no ha de saltar-se, perquè la importació comprova que els recursos existeixin, no que la plantilla els descrigui bé—. Les cinc piles per entorn queden repartides per cicle de vida i per propietat, amb exportacions per a l'estructural i SSM per al que pot canviar o creuar comptes, sabent que una exportació en ús és un forrellat. I la infraestructura passa pel pipeline de 08-04 amb el mateix rigor que el codi: cfn-lint i cfn-guard que fallen la construcció, un conjunt de canvis publicat, l'aprovació de la Marta i l'execució amb RoleArn, perquè qui dispara no necessiti els permisos que s'exerceixen.

Queda una limitació que es nota tan bon punt la plantilla creix. La xarxa descriu sis subxarxes gairebé idèntiques repetides a mà: no hi ha bucles, no hi ha funcions, no hi ha manera d'escriure "crea una subxarxa per cada AZ" ni d'encapsular "la xarxa estàndard de MercadoFresco" en alguna cosa reutilitzable. Les condicions serveixen per a dos casos i es tornen il·legibles a partir de cinc. I no hi ha manera d'escriure una prova que verifiqui que cap plantilla no obre el port 22 al món: cfn-guard ajuda, però és un altre llenguatge més per aprendre.

A 09-02, «AWS CDK», aquesta mateixa infraestructura s'escriu en TypeScript i en Python, amb bucles, condicionals, tipus, constructes reutilitzables i proves unitàries de veritat, i el resultat continua sent una plantilla de CloudFormation desplegada com una pila. Tot el d'aquesta lliçó continua sent cert per sota: el CDK no substitueix CloudFormation, l'escriu per tu.

Curs d'AWS

Mòdul 1: Introducció a AWS

Mòdul 2: Serveis principals d'AWS

Mòdul 3: Xarxes i lliurament de contingut

Mòdul 4: Seguretat i identitat

Mòdul 5: Monitoratge i gestió

Mòdul 6: Bases de dades

Mòdul 7: Integració d'aplicacions

Mòdul 8: Eines per a desenvolupadors

Mòdul 9: Infraestructura com a codi i govern de comptes

Mòdul 10: Contenidors a AWS

Mòdul 11: Millors pràctiques i gestió de costos

© Copyright 2026. Tots els drets reservats