Vés al contingut

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. NodejsFunction el crea sol, amb el document de confiança i el permís de logs.
  • El permís de DynamoDB és una línia. grantReadWriteData escriu la política amb els ARN exactes de la taula i dels índexs. Molt millor que AmazonDynamoDBFullAccess.
  • El lambda add-permission tampoc hi és. HttpLambdaIntegration afegeix 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 MyQueue o ListaCompra escrits 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 }
CDKSAM
LlenguatgeTypeScript, Python, Java…YAML
Lògica (bucles, condicions)La del llenguatgeMínima (Conditions, !If)
Provar en localNo ve de sèriesam local invoke i sam local start-api
PermisosgrantRead, grantReadWriteDataPlantilles de polítiques (DynamoDBCrudPolicy)
Per sotaCloudFormationCloudFormation

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 el bootstrap a 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 posem DESTROY; en producció, deixa el RETAIN.
  • Has canviat una cosa a la consola i el següent deploy l'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​

  1. Fes la pila d'aquesta lliçó i desplega-la. Prova les quatre rutes amb curl i la capçalera x-usuari.
  2. Mira la pila a la consola de CloudFormation: compta quants recursos ha creat (la majoria no els has escrit tu).
  3. Fes un canvi de memòria a la funció i mira el diff abans del deploy.
  4. npx cdk destroy i comprova que no queda res: ni la taula, ni el grup de logs, ni el rol.
  5. Opcional: la mateixa API amb SAM, per comparar quantes línies et costa cada eina.
Repàs: quatre preguntes
  1. Què hi ha sempre per sota de CDK i de SAM? CloudFormation.
  2. Què fa cdk diff? Compara el que generaria el codi amb el que hi ha desplegat, sense tocar res.
  3. 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.
  4. 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.