Infraestructura com a codi amb CDK
Fins ara hem muntat les coses amb comandes soltes: un create-role, un create-function, un create-api, un add-permission. Funciona per aprendre què hi ha a sota, però té tres problemes:
- No és reproduïble. Muntar-ho a un altre compte o a una altra regió vol dir tornar a escriure les vint comandes en el mateix ordre.
- No es desfà. Esborrar-ho tot vol dir recordar què vas crear, i sempre queda un rol o un grup de logs oblidat.
- No es revisa. Ningú pot llegir un historial de terminal i dir si allò és el que hi ha en producció.
La infraestructura com a codi (IaC) ho resol: descrius en un fitxer el que vols que existeixi, i una eina ho crea, ho actualitza o ho esborra. A AWS, la peça de sota sempre és CloudFormation. CDK és una manera d'escriure-li les instruccions en TypeScript en lloc de YAML.
Les peces
- Pila (stack): un conjunt de recursos que es despleguen i s'esborren junts. És una pila de CloudFormation, la pots veure a la consola.
- Constructe (construct): un bloc de codi que crea un o més recursos.
new dynamodb.TableV2(...)és un constructe. - Aplicació: el programa de TypeScript que crea una o més piles.
- Síntesi: CDK executa el teu codi i en treu una plantilla de CloudFormation. És el que es desplega de debò.
Els constructes tenen tres nivells: L1 (CfnTable) és el recurs de CloudFormation tal qual, amb totes les propietats; L2 (TableV2) hi afegeix valors per defecte raonables i mètodes com grantReadWriteData; L3 són patrons sencers. Fes servir L2 sempre que n'hi hagi.
Començar
mkdir llista-cdk && cd llista-cdk
npx aws-cdk@latest init app --language typescript
npm install --save-dev esbuild
Un cop per compte i regió, el bootstrap: crea una pila CDKToolkit amb un bucket on CDK puja el codi de les funcions i els rols que fa servir per desplegar.
npx cdk bootstrap aws://$(aws sts get-caller-identity --query Account --output text)/eu-west-1
esbuild és important: és el que fa servir NodejsFunction per compilar el TypeScript de les Lambdes. Si no hi és, CDK intenta fer-ho dins de Docker, i si tampoc tens Docker, falla.
La llista de la compra en una pila
Tot el que vam muntar a mà a les lliçons 02, 03 i 04, en un fitxer:
// lib/llista-stack.ts
import { CfnOutput, Duration, RemovalPolicy, Stack, type StackProps } from "aws-cdk-lib";
import type { Construct } from "constructs";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as logs from "aws-cdk-lib/aws-logs";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { CorsHttpMethod, HttpApi, HttpMethod } from "aws-cdk-lib/aws-apigatewayv2";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
export class LlistaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const taula = new dynamodb.TableV2(this, "Taula", {
partitionKey: { name: "usuari", type: dynamodb.AttributeType.STRING },
sortKey: { name: "producte", type: dynamodb.AttributeType.STRING },
globalSecondaryIndexes: [
{
indexName: "pendents",
partitionKey: { name: "usuari", type: dynamodb.AttributeType.STRING },
sortKey: { name: "pendentDes", type: dynamodb.AttributeType.STRING },
},
],
// Per al curs: en fer destroy, la taula s'esborra. En producció, no.
removalPolicy: RemovalPolicy.DESTROY,
});
const funcio = new NodejsFunction(this, "Api", {
entry: "src/api.ts",
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
memorySize: 256,
timeout: Duration.seconds(10),
environment: { TAULA: taula.tableName },
logGroup: new logs.LogGroup(this, "Logs", {
retention: logs.RetentionDays.TWO_WEEKS,
removalPolicy: RemovalPolicy.DESTROY,
}),
});
// Permís de lectura i escriptura sobre aquesta taula i els seus índexs. Res més.
taula.grantReadWriteData(funcio);
const api = new HttpApi(this, "Http", {
corsPreflight: {
allowOrigins: ["https://el-teu-frontend.cloudfront.net"],
allowMethods: [CorsHttpMethod.GET, CorsHttpMethod.POST, CorsHttpMethod.PUT, CorsHttpMethod.DELETE],
allowHeaders: ["content-type", "authorization"],
},
});
const integracio = new HttpLambdaIntegration("Llista", funcio);
api.addRoutes({ path: "/productes", methods: [HttpMethod.GET, HttpMethod.POST], integration: integracio });
api.addRoutes({ path: "/productes/{producte}", methods: [HttpMethod.PUT, HttpMethod.DELETE], integration: integracio });
new CfnOutput(this, "Url", { value: api.apiEndpoint });
}
}
Compara-ho amb el que vam fer a mà:
- El rol de la funció no hi és.
NodejsFunctionel crea sol, amb el document de confiança i el permís de logs. - El permís de DynamoDB és una línia.
grantReadWriteDataescriu la política amb els ARN exactes de la taula i dels índexs. Molt millor queAmazonDynamoDBFullAccess. - El
lambda add-permissiontampoc hi és.HttpLambdaIntegrationafegeix la política de recursos perquè API Gateway pugui invocar la funció. - Cap nom fix. La taula, la funció i l'API reben noms generats. Dos alumnes que despleguen la mateixa pila al mateix compte no xoquen; a classe, amb noms com
MyQueueoListaCompraescrits a la plantilla, xocaven. - Els logs caduquen als 14 dies des del primer desplegament, no quan te'n recordis.
La funció és una sola Lambda que decideix què fer segons la ruta:
// src/api.ts
import type { APIGatewayProxyEventV2 } from "aws-lambda";
import { afegeix, llista, marcaComprat, esborra } from "./llista"; // el codi de la lliçó 04
export const handler = async (event: APIGatewayProxyEventV2) => {
// Provisional: a la lliçó 08 l'usuari surt del token, no d'una capçalera.
const usuari = event.headers["x-usuari"];
if (!usuari) return { statusCode: 401, body: "" };
const producte = event.pathParameters?.producte;
switch (event.routeKey) {
case "GET /productes":
return resposta(200, await llista(usuari));
case "POST /productes": {
const { nom, quantitat } = JSON.parse(event.body ?? "{}");
return resposta(201, await afegeix(usuari, nom, quantitat));
}
case "PUT /productes/{producte}":
await marcaComprat(usuari, producte!);
return resposta(204);
case "DELETE /productes/{producte}":
await esborra(usuari, producte!);
return resposta(204);
default:
return resposta(404);
}
};
const resposta = (statusCode: number, cos?: unknown) => ({
statusCode,
headers: { "content-type": "application/json; charset=utf-8" },
body: cos === undefined ? "" : JSON.stringify(cos),
});
El cicle: synth, diff, deploy, destroy
npx cdk synth # genera la plantilla de CloudFormation a cdk.out/ i no toca res
npx cdk diff # què canviarà respecte del que hi ha desplegat
npx cdk deploy # ho aplica; si hi ha canvis de permisos, et demana confirmació
npx cdk destroy # ho esborra tot
Acostuma't a llegir el diff abans de cada deploy. Si diu que un recurs es reemplaça ([-] destroy + [+] add del mateix recurs), vol dir que l'esborrarà i en crearà un de nou. Amb una funció no passa res; amb una taula plena de dades, és perdre-les.
Canvia d'identificador lògic (el segon paràmetre, "Taula") o canvia la clau d'una taula i CDK no pot modificar-la: la reemplaça. És el mateix límit de DynamoDB de la lliçó 04, ara amb un avís abans.
I SAM?
Al curs presencial vam fer servir AWS SAM, que és l'altra eina oficial. És YAML de CloudFormation amb dreceres per a serverless:
Transform: AWS::Serverless-2016-10-31
Resources:
Llista:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Handler: api.handler
Environment:
Variables:
TAULA: !Ref Taula
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref Taula
Events:
Productes:
Type: HttpApi
Properties: { Path: /productes, Method: GET }
| CDK | SAM | |
|---|---|---|
| Llenguatge | TypeScript, Python, Java… | YAML |
| Lògica (bucles, condicions) | La del llenguatge | Mínima (Conditions, !If) |
| Provar en local | No ve de sèrie | sam local invoke i sam local start-api |
| Permisos | grantRead, grantReadWriteData | Plantilles de polítiques (DynamoDBCrudPolicy) |
| Per sota | CloudFormation | CloudFormation |
Totes dues són bones opcions. Si ja escrius TypeScript, CDK t'estalvia aprendre un dialecte de YAML i et dona autocompletat i tipus. Si el projecte és només un parell de funcions i vols provar-les en local, SAM és més directe. El que no val és barrejar-les a la mateixa pila.
Què costa això
CDK i CloudFormation són gratuïts per als recursos d'AWS. Pagues els recursos que creen, com si els haguessis creat a mà.
L'únic cost propi és el bucket del bootstrap, on CDK guarda cada versió del codi de les funcions. Són cèntims, però creix a cada desplegament i no s'esborra sol. Una regla de cicle de vida que n'esborri els objectes de més de 30 dies ho resol.
Errors habituals
SSM parameter /cdk-bootstrap/... not found. No has fet elbootstrapa aquesta regió o compte. Es fa un sol cop per cada parella.- La pila s'esborra però la taula o el bucket queden. La política d'esborrat per defecte dels recursos amb dades és
RETAIN: CDK els deixa a posta. Al curs posemDESTROY; en producció, deixa elRETAIN. - Has canviat una cosa a la consola i el següent
deployl'ha desfet. La plantilla mana. Tot el que es toqui a mà es perd o, pitjor, fa que la pila quedi desquadrada (drift). Si s'ha de canviar, es canvia al codi. - Despleguen dues persones i es trepitgen. Si cada alumne o cada entorn necessita la seva còpia, dona a cada pila un nom diferent (
new LlistaStack(app, "Llista-anna")). Els recursos de dins ja tenen noms únics. UPDATE_ROLLBACK_FAILED. Una actualització ha fallat i el retrocés també. Passa sobretot quan algú ha esborrat a mà un recurs de la pila. Des de la consola de CloudFormation, «Continue update rollback» i indica-li quins recursos ha de saltar.
Exercici
- Fes la pila d'aquesta lliçó i desplega-la. Prova les quatre rutes amb
curli la capçalerax-usuari. - Mira la pila a la consola de CloudFormation: compta quants recursos ha creat (la majoria no els has escrit tu).
- Fes un canvi de memòria a la funció i mira el
diffabans deldeploy. npx cdk destroyi comprova que no queda res: ni la taula, ni el grup de logs, ni el rol.- Opcional: la mateixa API amb SAM, per comparar quantes línies et costa cada eina.
Repàs: quatre preguntes
- Què hi ha sempre per sota de CDK i de SAM? CloudFormation.
- Què fa
cdk diff? Compara el que generaria el codi amb el que hi ha desplegat, sense tocar res. - Per què és mala idea posar noms fixos als recursos d'una plantilla? Perquè dues piles al mateix compte xoquen, i canviar-los força un reemplaçament.
- Si esborres la pila, què passa amb una taula de DynamoDB amb la política per defecte? Es queda (
RETAIN).
La IA a l'empresa, amb criteri
Cada setmana, una sola cosa que pots fer servir: la notícia que toca la teva empresa i què en faria jo, una pregunta de client, un cas real o un exercici de quinze minuts. Un correu per setmana com a molt, i te'n pots donar de baixa amb un clic.