Lab práctico · Semana 6: Despliegue de modelos, integración empresarial y herramientas de desarrollo
Pipeline CI/CD para una aplicación de IA generativa
Qué vas a construir
Un pipeline mínimo pero realista para la API de resúmenes del lab 11, con lo que diferencia el CI/CD de una aplicación de IA generativa del de una aplicación normal: una puerta de calidad de prompts que llama al modelo real con casos de referencia y bloquea el despliegue si el comportamiento empeora.
flowchart LR
DEV["git push a main"] --> CC["AWS CodeCommit"]
CC -->|"evento"| EB["EventBridge"]
EB --> CP["AWS CodePipeline V2"]
CP --> CB["CodeBuild: tests unitarios, puerta de calidad, package"]
CB -->|"packaged.yaml"| CFN["Acción CloudFormation: CREATE_UPDATE"]
CFN --> APP["Stack lab12-app: API Gateway + Lambda"]
CB -.->|"Converse"| BR["Bedrock: Nova Micro"]
Todo el pipeline se define como código en una plantilla de CloudFormation (repositorio, proyecto de CodeBuild, roles, pipeline y regla de EventBridge).
Antes de empezar
- Haber hecho el lab 11 y conservar la carpeta
~/lab11(código, pruebas yapp.yaml). Si la borraste, repite los pasos 1 a 3 del lab 11 (solo crear ficheros, sin desplegar). - Región eu-central-1 (Fráncfort), identidad con permisos de administración en tu cuenta de estudio (el despliegue crea roles IAM).
- AWS CodeCommit vuelve a estar abierto a clientes nuevos desde noviembre de 2025, así que puedes usarlo como repositorio. En una empresa lo habitual es GitHub, GitLab o Bitbucket conectados mediante AWS CodeConnections; el resto del pipeline sería igual.
- Coste esperado (estimación a 1/10/2026): CodePipeline V2 cobra 0,002 USD por minuto de ejecución de acción (con 100 minutos gratuitos al mes); CodeBuild
general1.smallcuesta del orden de 0,005 USD por minuto de build (con 100 minutos gratuitos al mes según las condiciones de la capa gratuita); cada ejecución dura pocos minutos. Tokens de la puerta de calidad: céntimos de céntimo. Máximo razonable: 0,20 USD. Consulta CodePipeline pricing y CodeBuild pricing.
Paso 1: añade la puerta de calidad y el buildspec
En CloudShell:
cd ~/lab11
La puerta de calidad llama al handler real (y por tanto a Bedrock) con casos de referencia (golden dataset) y comprueba propiedades objetivas: respuesta 200, stopReason igual a end_turn (no cortada por max_tokens), que el resumen no sea más largo que el original y que conserve los datos clave. Si algún caso falla, termina con código 1 y el build falla.
cat > tests/prompt_gate.py <<'EOF'
"""Puerta de calidad de prompts: llama al modelo real con casos de referencia."""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import app # noqa: E402
CASOS = [
{
"texto": "La factura 2024-118 vence el 30 de noviembre. Si no se paga, "
"se aplicará un recargo del 5 % y se suspenderá el servicio.",
"debe_contener": ["30 de noviembre", "5"],
},
{
"texto": "El equipo de soporte atiende de lunes a viernes de 9:00 a 18:00. "
"Los fines de semana solo hay atención por correo electrónico.",
"debe_contener": ["lunes", "correo"],
},
]
fallos = 0
for caso in CASOS:
r = app.handler({"body": json.dumps({"texto": caso["texto"]})}, None)
cuerpo = json.loads(r["body"])
resumen = cuerpo.get("resumen", "")
ok = (
r["statusCode"] == 200
and cuerpo.get("stopReason") == "end_turn"
and len(resumen) < len(caso["texto"]) * 1.2
and all(t.lower() in resumen.lower() for t in caso["debe_contener"])
)
print(("OK " if ok else "FALLO"), "|", resumen.replace("\n", " ")[:120])
fallos += 0 if ok else 1
if fallos:
print(f"{fallos} caso(s) no superan la puerta de calidad")
sys.exit(1)
print("Puerta de calidad superada")
EOF
El buildspec le dice a CodeBuild qué hacer:
cat > buildspec.yml <<'EOF'
version: 0.2
phases:
install:
runtime-versions:
python: 3.13
commands:
- pip install --quiet pytest boto3
build:
commands:
- echo "1/3 Tests unitarios con Bedrock simulado"
- python -m pytest -q tests/unit
- echo "2/3 Puerta de calidad de prompts contra el modelo real"
- python tests/prompt_gate.py
- echo "3/3 Empaquetado de la plantilla"
- aws cloudformation package --template-file app.yaml --s3-bucket "$ARTIFACT_BUCKET" --s3-prefix lambda --output-template-file packaged.yaml
artifacts:
files:
- packaged.yaml
EOF
Pruébalo en local antes de automatizarlo (sí llama al modelo, con coste insignificante):
AWS_DEFAULT_REGION=eu-central-1 python3 -m pytest -q tests/unit
AWS_REGION=eu-central-1 python3 tests/prompt_gate.py
Debes ver Puerta de calidad superada. Si con el prompt correcto algún caso falla, lee el resumen impreso: un modelo pequeño puede parafrasear (por ejemplo, escribir la fecha de otra forma). Ajusta los términos esperados a lo que es obligatorio conservar; una puerta de calidad que falla sin motivo acaba ignorándose.
Paso 2: despliega la infraestructura del pipeline
Guarda la plantilla del pipeline:
mkdir -p ~/lab12 && cd ~/lab12
cat > pipeline.yaml <<'EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: Lab 12 - Pipeline CI/CD minimo para la API de resumen del lab 11
Parameters:
RepoName:
Type: String
Default: lab12-genai-app
AppStackName:
Type: String
Default: lab12-app
ModelId:
Type: String
Default: eu.amazon.nova-micro-v1:0
BaseModelId:
Type: String
Default: amazon.nova-micro-v1:0
Resources:
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: borrar-artefactos-antiguos
Status: Enabled
ExpirationInDays: 7
Repo:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Ref RepoName
RepositoryDescription: Codigo de la API de resumen del lab 12
BuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: build
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub ${ArtifactBucket.Arn}/*
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketAcl
- s3:GetBucketLocation
Resource: !GetAtt ArtifactBucket.Arn
- Effect: Allow
Action: bedrock:InvokeModel
Resource:
- !Sub arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/${ModelId}
- !Sub arn:aws:bedrock:*::foundation-model/${BaseModelId}
BuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${RepoName}-build
ServiceRole: !GetAtt BuildRole.Arn
TimeoutInMinutes: 15
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yml
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux-x86_64-standard:5.0
EnvironmentVariables:
- Name: ARTIFACT_BUCKET
Value: !Ref ArtifactBucket
- Name: MODEL_ID
Value: !Ref ModelId
DeployRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: desplegar-app
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:*
- apigateway:*
Resource: '*'
- Effect: Allow
Action:
- iam:CreateRole
- iam:DeleteRole
- iam:GetRole
- iam:PassRole
- iam:TagRole
- iam:UntagRole
- iam:PutRolePolicy
- iam:DeleteRolePolicy
- iam:GetRolePolicy
- iam:AttachRolePolicy
- iam:DetachRolePolicy
- iam:ListRolePolicies
- iam:ListAttachedRolePolicies
Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/${AppStackName}-*
- Effect: Allow
Action: s3:GetObject
Resource: !Sub ${ArtifactBucket.Arn}/*
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: pipeline
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
- s3:GetBucketVersioning
Resource:
- !GetAtt ArtifactBucket.Arn
- !Sub ${ArtifactBucket.Arn}/*
- Effect: Allow
Action:
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:GetRepository
- codecommit:UploadArchive
- codecommit:GetUploadArchiveStatus
- codecommit:CancelUploadArchive
Resource: !GetAtt Repo.Arn
- Effect: Allow
Action:
- codebuild:StartBuild
- codebuild:BatchGetBuilds
Resource: !GetAtt BuildProject.Arn
- Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:UpdateStack
- cloudformation:DeleteStack
- cloudformation:DescribeStacks
- cloudformation:DescribeStackResources
- cloudformation:DescribeStackEvents
- cloudformation:GetTemplate
- cloudformation:CreateChangeSet
- cloudformation:DescribeChangeSet
- cloudformation:DeleteChangeSet
- cloudformation:ExecuteChangeSet
Resource: !Sub arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AppStackName}/*
- Effect: Allow
Action: cloudformation:ValidateTemplate
Resource: '*'
- Effect: Allow
Action: iam:PassRole
Resource: !GetAtt DeployRole.Arn
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Sub ${RepoName}-pipeline
PipelineType: V2
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: Codigo
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: '1'
Configuration:
RepositoryName: !GetAtt Repo.Name
BranchName: main
PollForSourceChanges: false
OutputArtifacts:
- Name: SourceOutput
- Name: Build
Actions:
- Name: TestsYEmpaquetado
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: !Ref BuildProject
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: BuildOutput
- Name: Deploy
Actions:
- Name: DesplegarStack
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
StackName: !Ref AppStackName
TemplatePath: BuildOutput::packaged.yaml
Capabilities: CAPABILITY_IAM
RoleArn: !GetAtt DeployRole.Arn
ParameterOverrides: !Sub '{"ModelId": "${ModelId}", "BaseModelId": "${BaseModelId}"}'
InputArtifacts:
- Name: BuildOutput
TriggerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: arrancar-pipeline
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: codepipeline:StartPipelineExecution
Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
PushRule:
Type: AWS::Events::Rule
Properties:
Description: Arranca el pipeline con cada push a main
EventPattern:
source:
- aws.codecommit
detail-type:
- CodeCommit Repository State Change
resources:
- !GetAtt Repo.Arn
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- main
Targets:
- Id: pipeline
Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
RoleArn: !GetAtt TriggerRole.Arn
Outputs:
CloneUrl:
Value: !GetAtt Repo.CloneUrlHttp
PipelineName:
Value: !Ref Pipeline
ArtifactBucket:
Value: !Ref ArtifactBucket
EOF
Qué define:
| Recurso | Para qué |
|---|---|
ArtifactBucket |
Artefactos del pipeline y zip de la Lambda; bloqueo de acceso público y expiración a 7 días |
Repo |
Repositorio CodeCommit lab12-genai-app |
BuildRole + BuildProject |
CodeBuild con imagen Amazon Linux 2023 estándar 5.0 y Python 3.13; su rol puede invocar solo Nova Micro (para la puerta de calidad) y escribir en el bucket |
DeployRole |
Rol que CloudFormation asume para crear la app: permisos limitados a Lambda, API Gateway y roles cuyo nombre empiece por lab12-app- |
PipelineRole + Pipeline |
Pipeline V2 con tres etapas: Source (CodeCommit), Build (CodeBuild) y Deploy (acción CloudFormation CREATE_UPDATE) |
TriggerRole + PushRule |
Regla de EventBridge que arranca el pipeline con cada push a main (en lugar de sondear el repositorio) |
Despliégala:
aws cloudformation deploy \
--region eu-central-1 \
--template-file pipeline.yaml \
--stack-name lab12-pipeline \
--capabilities CAPABILITY_IAM
aws cloudformation describe-stacks --region eu-central-1 --stack-name lab12-pipeline \
--query 'Stacks[0].Outputs' --output table
Al crearse, el pipeline intenta una primera ejecución y falla en Source porque el repositorio está vacío. Es normal.
Paso 3: sube el código al repositorio
Configura Git para autenticarse en CodeCommit con tus credenciales de CloudShell (el credential helper de la AWS CLI) y sube el código:
git config --global credential.helper '!aws codecommit credential-helper $@'
git config --global credential.UseHttpPath true
git config --global user.name "Borja"
git config --global user.email "borja@example.com"
CLONE=$(aws cloudformation describe-stacks --region eu-central-1 --stack-name lab12-pipeline \
--query "Stacks[0].Outputs[?OutputKey=='CloneUrl'].OutputValue" --output text)
cd ~ && git clone "$CLONE" lab12-repo && cd lab12-repo
git checkout -b main
cp -r ~/lab11/src ~/lab11/tests ~/lab11/app.yaml ~/lab11/buildspec.yml .
find . -name '__pycache__' -prune -exec rm -rf {} +
printf "__pycache__/\n*.pyc\npackaged.yaml\n" > .gitignore
git add .
git commit -m "API de resumen con puerta de calidad de prompts"
git push -u origin main
El push genera un evento de CodeCommit; la regla de EventBridge arranca el pipeline.
Paso 4: sigue la ejecución
PIPE=$(aws cloudformation describe-stacks --region eu-central-1 --stack-name lab12-pipeline \
--query "Stacks[0].Outputs[?OutputKey=='PipelineName'].OutputValue" --output text)
aws codepipeline get-pipeline-state --region eu-central-1 --name "$PIPE" \
--query 'stageStates[].[stageName,latestExecution.status]' --output table
Repite el comando cada minuto (o mira la consola de CodePipeline, más visual). En la etapa Build, abre los logs de CodeBuild: verás los tests unitarios, las líneas OK | ... de la puerta de calidad con los resúmenes reales y el package. En Deploy, CloudFormation crea el stack lab12-app.
Cuando las tres etapas estén en Succeeded, prueba la API desplegada por el pipeline:
URL=$(aws cloudformation describe-stacks --region eu-central-1 --stack-name lab12-app \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text)
KEY_ID=$(aws cloudformation describe-stacks --region eu-central-1 --stack-name lab12-app \
--query "Stacks[0].Outputs[?OutputKey=='ApiKeyId'].OutputValue" --output text)
API_KEY=$(aws apigateway get-api-key --region eu-central-1 --api-key "$KEY_ID" \
--include-value --query value --output text)
curl -s -X POST "$URL" -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
-d '{"texto": "La factura 2024-118 vence el 30 de noviembre. Si no se paga, se aplicará un recargo del 5 % y se suspenderá el servicio."}' | python3 -m json.tool
Paso 5: rompe el prompt y deja que el pipeline te proteja
Simula un cambio «inocente» de un compañero que estropea el comportamiento: pedir el resumen en inglés.
cd ~/lab12-repo
sed -i 's/Eres un asistente que resume textos en español de España. /You are an assistant that summarizes texts in English only. /' src/app.py
git diff --stat
git commit -am "Cambio de idioma del prompt"
git push
Los tests unitarios pasan (el modelo está simulado y el formato de la petición no cambia), pero la puerta de calidad falla: los resúmenes ya no contienen «lunes», «correo» o «30 de noviembre». El build termina en error, Deploy no se ejecuta y la API sigue sirviendo la versión buena. Compruébalo:
aws codepipeline get-pipeline-state --region eu-central-1 --name "$PIPE" \
--query 'stageStates[].[stageName,latestExecution.status]' --output table
Revierte el cambio:
git revert --no-edit HEAD
git push
El pipeline vuelve a ejecutarse y termina en verde. Este es el valor de una puerta de calidad específica de IA generativa: los tests clásicos no detectan una regresión de prompt.
Comprueba que funciona
- El primer push ejecuta las tres etapas en verde y crea el stack
lab12-app. - La API desplegada por el pipeline responde con un resumen en español.
- El cambio de idioma pasa los tests unitarios pero falla la puerta de calidad y no se despliega.
- Tras
git revert, el pipeline vuelve a verde.
Limpieza
El orden importa: el stack lab12-app lo gestiona CloudFormation con el DeployRole del stack del pipeline, así que bórralo primero.
REGION=eu-central-1
FUNCION=$(aws cloudformation describe-stacks --region $REGION --stack-name lab12-app \
--query "Stacks[0].Outputs[?OutputKey=='FunctionName'].OutputValue" --output text)
# 1) La aplicación desplegada por el pipeline
aws cloudformation delete-stack --region $REGION --stack-name lab12-app
aws cloudformation wait stack-delete-complete --region $REGION --stack-name lab12-app
aws logs delete-log-group --region $REGION --log-group-name "/aws/lambda/$FUNCION"
# 2) Vaciar el bucket de artefactos (si no, el stack no puede borrarlo)
BUCKET=$(aws cloudformation describe-stacks --region $REGION --stack-name lab12-pipeline \
--query "Stacks[0].Outputs[?OutputKey=='ArtifactBucket'].OutputValue" --output text)
aws s3 rm "s3://$BUCKET" --recursive
# 3) El pipeline, CodeBuild, CodeCommit, roles y regla de EventBridge
aws cloudformation delete-stack --region $REGION --stack-name lab12-pipeline
aws cloudformation wait stack-delete-complete --region $REGION --stack-name lab12-pipeline
# 4) Logs de CodeBuild y ficheros locales
aws logs delete-log-group --region $REGION --log-group-name "/aws/codebuild/lab12-genai-app-build"
rm -rf ~/lab12 ~/lab12-repo ~/lab11
Al borrar el stack del pipeline también se borra el repositorio CodeCommit con su historial. Si algún comando de logs dice que el grupo no existe, no pasa nada.
Preguntas para pensar como arquitecto
1. ¿Por qué no basta con tests unitarios para una aplicación de IA generativa?
Porque el componente crítico (el modelo con su prompt) es no determinista y está fuera del código que simulas. Un cambio de prompt, de parámetros o de versión del modelo puede degradar la calidad sin romper ningún contrato de software. Hacen falta pruebas de regresión de prompts contra el modelo real, con conjuntos de referencia y umbrales (propiedades objetivas, evaluación automática o LLM-as-a-judge), como puerta del pipeline (skills 2.3.5 y 5.1.4).
2. El proveedor publica una versión nueva del modelo. ¿Cómo la adoptarías con seguridad?
Tratando el cambio de modelo como un cambio de código o de configuración gobernado: actualizar el identificador (en AppConfig o en el repositorio), ejecutar la evaluación sobre el conjunto de referencia (calidad, formato, latencia y coste por petición), aprobar manualmente si procede y desplegar de forma gradual (canario con CodeDeploy o despliegue gradual de AppConfig) con rollback automático ligado a alarmas. Mantener el modelo anterior disponible hasta confirmar.
3. ¿Por qué el pipeline usa un DeployRole distinto del rol de CodeBuild?
Por mínimo privilegio y separación de funciones. CodeBuild ejecuta código del repositorio (tests, scripts): si alguien mete algo malicioso, solo puede leer y escribir en el bucket e invocar un modelo. La creación de recursos (Lambda, API Gateway, roles IAM) la hace CloudFormation con un rol acotado a los recursos de la app (lab12-app-*). Nadie tiene permisos de administrador «por si acaso».
4. ¿Qué cambiaría si varios equipos consumen modelos a través de un gateway de IA central?
El pipeline del gateway desplegaría la capa común (autenticación, planes de uso, guardrails, enrutado de modelos, logging), y las políticas (modelos aprobados, límites por equipo) vivirían en configuración versionada (AppConfig) con su propio flujo de aprobación. Cada equipo desplegaría sus aplicaciones consumiendo el gateway, sin permisos directos sobre Bedrock. La observabilidad central permite atribuir coste y detectar abusos por consumidor.
5. Kiro y Amazon Q Developer, ¿en qué parte de este flujo ayudarían?
En la productividad del desarrollador (skill 2.5.4): generar o refactorizar el código de la Lambda y las plantillas, proponer tests (incluidos casos de la puerta de calidad) y explicar errores del pipeline. Kiro, además, puede planificar el cambio con specs, aplicar reglas del proyecto con steering y ejecutarse en modo headless dentro de la CI. Q Developer en la consola ayuda a diagnosticar fallos de CloudFormation o CodeBuild. Ninguno sustituye a la puerta de calidad: generan código, no garantizan comportamiento.