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
- L'asimetria entre codi i infraestructura
- Què és la infraestructura com a codi, i declaratiu enfront d'imperatiu
- Anatomia d'una plantilla
- Paràmetres: tipus, restriccions i valors des d'SSM
- Mappings, Conditions i funcions intrínseques
- Piles: cicle de vida, estats i
ROLLBACK_COMPLETE - Polítiques d'eliminació i protecció de terminació
- Conjunts de canvis i els tres tipus d'actualització
- Detecció de deriva
- La xarxa de MercadoFresco en una plantilla
- La capa d'aplicació que importa les sortides
- Diverses piles: exportacions enfront d'SSM
- Piles imbricades, mòduls i recursos personalitzats
- Importar recursos existents a una pila
- StackSets i desplegament des del pipeline
- Bones pràctiques,
cfn-linticfn-guard - Límits, cost i neteja
- Errors habituals i consells
- Exercicis
- 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-04e5f6a7L'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_64Tres coses separen una plantilla amateur d'una de professional:
- Els tipus específics d'AWS validen abans de començar. Amb
Type: Stringper a una clau SSH inexistent, la pila crea mitja dotzena de recursos i falla en arribar a la instància. AmbAWS::EC2::KeyPair::KeyNamefalla al segon zero, sense crear res. 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.NoEchono 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 Vpc → vpc-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 tableLes 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 DELETEDEls 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-1Set 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 horaEls 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:
- Escriure la plantilla que descriu exactament el que existeix. Aquí no s'improvisa: si diu
/healthi el grup de destinació real té/salud, la importació passa i el següentupdate-stackcanvia la realitat sense avisar. - Afegir
DeletionPolicy: Retaina tots els recursos a importar —és obligatori— i preparar el fitxer d'identificadors, que aparella cada nom lògic amb el recurs real. - 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 OBLIGATORILa 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_IAMSELF_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:
- Plantilles petites i amb una responsabilitat. Més de 400 línies o 80 recursos: es parteix, per cicle de vida.
- 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.
- 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,Mappingsi SSM cobreixen tots els casos. I fixarBucketNameoGroupNameimpedeix actualitzacions sense interrupció i bloqueja desplegar dues vegades la mateixa plantilla en un compte. - Tot passa per PR i per validació automàtica. Cap
deploymanual 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.guardlet 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 tableSi 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.jsonLa 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
- Què és AWS?
- Configuració del teu compte d'AWS
- Infraestructura global d'AWS
- Consola d'administració d'AWS
- AWS CLI i SDK
Mòdul 2: Serveis principals d'AWS
Mòdul 3: Xarxes i lliurament de contingut
- Amazon VPC
- Grups de seguretat i llistes de control d'accés
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Mòdul 4: Seguretat i identitat
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager i Parameter Store
- AWS Shield
- AWS WAF
Mòdul 5: Monitoratge i gestió
- Amazon CloudWatch
- AWS X-Ray i traçabilitat distribuïda
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Mòdul 6: Bases de dades
- Com triar la base de dades adequada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Mòdul 7: Integració d'aplicacions
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrons d'integració: idempotència, reintents i cues de missatges fallits
