Servicio de moderación de contenido impulsado por IA sin servidor

Este archivo ha sido traducido automaticamente por IA, pueden ocurrir errores
Hace aproximadamente un año creé una publicación sobre la creación de un servicio de Administrador de archivos. En esta publicación usaremos este servicio como base y lo extenderemos con moderación de contenido. Usaremos GuardDuty y Rekognition para ayudar en esta tarea. Como siempre, todo será sin servidor y basado en eventos.
Resumen
Para refrescar la memoria de todos, comencemos con un breve resumen.
El servicio almacenará archivos en S3 y mantendrá un registro de todos los archivos en una tabla de DynamoDB. La visión general del sistema se ve así, con una API que expone funcionalidad al usuario y luego ejecuta el trabajo de manera sin servidor y basada en eventos.

El flujo de carga se inicia cuando un cliente llama a la API, donde una función Lambda creará una URL pre-firmada de S3 que el cliente puede usar para cargar el archivo. No subimos el archivo directamente a través de la API, ya que Amazon API Gateway tiene un tamaño máximo de carga de 10 mb, y para admitir todo tipo de archivos esto se convertirá en una limitación.
Cuando el cliente luego carga el archivo a S3, esto generará un evento, la parte debajo de la línea punteada en la imagen, lo que activará una StepFunction que actualizará el inventario de archivos.

Arquitectura extendida
En la arquitectura extendida agregamos funcionalidad para usar el escaneo de malware de GuardDuty S3 y Rekognition para la moderación de imágenes. GuardDuty escaneará los nuevos archivos que lleguen al bucket de S3, que llamo staging, se agregará una etiqueta al objeto y el resultado del escaneo se publicará en el event-bus predeterminado. El resultado del escaneo, si está OK, activará una StepFunction que utilizará Rekognition para la moderación de imágenes. He implementado la misma lógica en esta StepFunction y agrego una etiqueta en el objeto y publico un evento en un event-bus. Finalmente, los archivos se mueven a un bucket de almacenamiento de cuarentena u otro.
Cada parte de la solución está desacoplada y puede ejecutarse independientemente, y se aplica un patrón saga para mover la lógica a la siguiente fase.

Ahora profundicemos un poco más en cada una de las partes de esta solución.
Escaneo de malware
El escaneo de malware de GuardDuty no requiere mucha configuración. Esta es una característica completamente administrada en GuardDuty y lo único que se requiere es configurarla. GuardDuty luego recogerá automáticamente los nuevos objetos.

Para lograr este flujo, lo único que necesitamos hacer es crear un S3MalwareProtectionPlan y asignarle los permisos apropiados. Una cosa importante a recordar, si cifras tus objetos en S3 con una Clave administrada por el cliente, no olvides darle permisos a GuardDuty para descifrar usando esta clave.
S3MalwareProtectionPlan:
Type: AWS::GuardDuty::MalwareProtectionPlan
Properties:
Actions:
Tagging:
Status: ENABLED
ProtectedResource:
S3Bucket:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:staging-bucket-name
Role: !GetAtt S3MalwareProtectionPlanRole.ArnModeración de imágenes
La parte de moderación con Rekognition implica un par de pasos más. Una StepFunction es activada por el resultado del escaneo de malware, y llama a Rekognition para moderar la imagen. Esta StepFunction luego etiquetará el objeto y publicará el resultado del escaneo en el event-bus personalizado de EventBridge. Una cosa importante a recordar, si cifras tus objetos en S3 con una Clave administrada por el cliente, no olvides darle permisos para descifrar usando esta clave. Rekognition te dará un error extraño Unsupported y no un error claro sobre por qué falló en este caso.

Para lograr este flujo, lo único que necesitamos hacer es crear la StateMachine y configurar los eventos en los que debe activarse.
ModerateImageStateMachineStandard:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: StateMachine/moderation.asl.yaml
DefinitionSubstitutions:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:event-bus-name
Tracing:
Enabled: true
Policies:
- Statement:
- Effect: Allow
Action:
- logs:*
Resource: "*"
- S3FullAccessPolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:staging-bucket-name
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:event-bus-name
- RekognitionDetectOnlyPolicy: {}
Events:
GuardDutyMalwareScanResult:
Type: EventBridgeRule
Properties:
InputPath: $.detail
Pattern:
source:
- aws.guardduty
detail-type:
- GuardDuty Malware Protection Object Scan Result
detail:
scanResultDetails:
scanResultStatus:
- NO_THREATS_FOUNDLa definición de StateMachine es bastante grande, y hay necesidad de algo de magia. Dado que no puedes agregar etiquetas a un objeto de S3, primero necesitamos obtener todas las etiquetas existentes, agregar nuestra nueva etiqueta y poner la matriz completa de etiquetas en el objeto. Esto probablemente sería más fácil de hacer en una función Lambda, pero dónde está la diversión en eso. Funciones intrínsecas para la victoria....
Comment: Moderate images using Rekognition
StartAt: Debug
States:
Debug:
Type: Pass
Next: Get Object Metadata
Get Object Metadata:
Type: Task
Parameters:
Bucket.$: $.s3ObjectDetails.bucketName
Key.$: $.s3ObjectDetails.objectKey
Resource: arn:aws:states:::aws-sdk:s3:headObject
Next: Get Object Tags
ResultPath: $.S3MetaData
Get Object Tags:
Type: Task
Parameters:
Bucket.$: $.s3ObjectDetails.bucketName
Key.$: $.s3ObjectDetails.objectKey
Resource: arn:aws:states:::aws-sdk:s3:getObjectTagging
Next: Is File Supported?
ResultPath: $.s3Tags
Is File Supported?:
Type: Choice
Choices:
- Or:
- Variable: $.S3MetaData.ContentType
StringMatches: image/png
- Variable: $.S3MetaData.ContentType
StringMatches: image/jpeg
Next: Moderate Image
Default: File Not Supported
File Not Supported:
Type: Pass
Next: Add FILE_NOT_SUPPORTED to Object Tags Array
Parameters:
ContentTypes: []
ModerationLabels: []
ModerationModelVersion: "7.0"
ThreatsFound: "-1"
ResultPath: $.RekognitionModeration
Add FILE_NOT_SUPPORTED to Object Tags Array:
Type: Pass
ResultPath: $.scanResult
Parameters:
status: FILE_NOT_SUPPORTED
newTagSet.$: >-
States.StringToJson(States.Format('[{},{}]',
States.ArrayGetItem(States.StringSplit(States.JsonToString($.s3Tags.TagSet),
'[]'),0), '{"Key":"ImageModerationStatus","Value":"FILE_NOT_SUPPORTED"}'))
Next: Tag S3 Object
Moderate Image:
Type: Task
Parameters:
Image:
S3Object:
Bucket.$: $.s3ObjectDetails.bucketName
Name.$: $.s3ObjectDetails.objectKey
Resource: arn:aws:states:::aws-sdk:rekognition:detectModerationLabels
Next: File Supported
ResultPath: $.RekognitionModeration
File Supported:
Type: Pass
Parameters:
ThreatsFound.$: States.ArrayLength($.RekognitionModeration.ModerationLabels)
ContentTypes.$: $.RekognitionModeration.ContentTypes
ModerationLabels.$: $.RekognitionModeration.ModerationLabels
ModerationModelVersion.$: $.RekognitionModeration.ModerationModelVersion
ResultPath: $.RekognitionModeration
Next: Was Threats Found?
Was Threats Found?:
Type: Choice
Choices:
- Variable: $.RekognitionModeration.ThreatsFound
NumericGreaterThan: 0
Next: Add THREATS_DETECTED to Object Tags Array
Default: Add NO_THREATS to Object Tags Array
Add THREATS_DETECTED to Object Tags Array:
Type: Pass
Parameters:
status: THREATS_FOUND
newTagSet.$: >-
States.StringToJson(States.Format('[{},{}]',
States.ArrayGetItem(States.StringSplit(States.JsonToString($.s3Tags.TagSet),
'[]'),0), '{"Key":"ImageModerationStatus","Value":"THREATS_FOUND"}'))
ResultPath: $.scanResult
Next: Tag S3 Object
Add NO_THREATS to Object Tags Array:
Type: Pass
Next: Tag S3 Object
Parameters:
status: NO_THREATS_FOUND
newTagSet.$: >-
States.StringToJson(States.Format('[{},{}]',
States.ArrayGetItem(States.StringSplit(States.JsonToString($.s3Tags.TagSet),
'[]'),0), '{"Key":"ImageModerationStatus","Value":"NO_THREATS_FOUND"}'))
ResultPath: $.scanResult
Tag S3 Object:
Type: Task
Parameters:
Bucket.$: $.s3ObjectDetails.bucketName
Key.$: $.s3ObjectDetails.objectKey
Tagging:
TagSet.$: $.scanResult.newTagSet
Resource: arn:aws:states:::aws-sdk:s3:putObjectTagging
ResultPath: null
Next: Post Scan Result Event
Post Scan Result Event:
Type: Task
Resource: arn:aws:states:::events:putEvents
Parameters:
Entries:
- Detail:
metadata: {}
data:
id.$: $.s3ObjectDetails.objectKey
status.$: $.scanResult.status
scanData.$: $.RekognitionModeration
DetailType: Moderation Scan Completed
EventBusName: ${EventBusName}
Source: ImageModeration
End: true
ResultPath: null
Finalizar la carga
La última parte de esta solución es reaccionar a la moderación y colocar el contenido ya sea en el bucket de cuarentena o en el bucket de almacenamiento a largo plazo. Para esto uso dos StepFunction diferentes con algunas diferencias en el evento que los activa.

Para lograr este flujo se crea una nueva StateMachine.
MoveFilesToPermanentStorageStateMachineStandard:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: StateMachine/move-to-permanent-storage.asl.yaml
DefinitionSubstitutions:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:event-bus-name
StorageBucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:storage-bucket-name
StagingBucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:staging-bucket-name
Tracing:
Enabled: true
Policies:
- Statement:
- Effect: Allow
Action:
- logs:*
Resource: "*"
- S3FullAccessPolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:staging-bucket-name
- S3FullAccessPolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:storage-bucket-name
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:event-bus-name
Events:
NoModerationThreatsFoundEvent:
Type: EventBridgeRule
Properties:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:event-bus-name
InputPath: $.detail
Pattern:
source:
- ImageModeration
detail-type:
- Moderation Scan Completed
detail:
data:
status:
- NO_THREATS_FOUND Con una definición de StateMachine que es un poco más fácil de seguir que la parte de moderación de imágenes.
Comment: Handle result and copy files to storage bucket
StartAt: Debug
States:
Debug:
Type: Pass
Next: Get Object Metadata
Get Object Metadata:
Type: Task
Parameters:
Bucket: ${StagingBucketName}
Key.$: $.data.id
Resource: arn:aws:states:::aws-sdk:s3:headObject
Next: CopyObject
ResultPath: $.S3MetaData
CopyObject:
Type: Task
Parameters:
Bucket: ${StorageBucketName}
CopySource.$: >-
States.Format('${StagingBucketName}/{}',$.data.id)
Key.$: $.data.id
Resource: arn:aws:states:::aws-sdk:s3:copyObject
ResultPath: null
Next: DeleteObject
DeleteObject:
Type: Task
Parameters:
Bucket: ${StagingBucketName}
Key.$: $.data.id
Resource: arn:aws:states:::aws-sdk:s3:deleteObject
ResultPath: null
Next: Post Event File Moved
Post Event File Moved:
Type: Task
Resource: arn:aws:states:::events:putEvents
Parameters:
Entries:
- Detail:
metadata: {}
data:
id.$: $.data.id
status: STORED
contentType.$: $.S3MetaData.ContentType
fileSize.$: $.S3MetaData.ContentLength
DetailType: Completed
EventBusName: ${EventBusName}
Source: ImageModeration
End: true
ResultPath: nullConclusión
Esta fue una breve publicación sobre cómo extendí mi administrador de archivos construido previamente con escaneo de malware y moderación de imágenes. El uso de servicios administrados hizo que esta fuera una tarea bastante fácil.
Para obtener el código fuente completo y desplegarlo usted mismo, visite Serverless-Handbook Image Moderation
Palabras finales
No olvide seguirme en LinkedIn y X para más contenido, y lea el resto de mis Blogs
Como dice Werner! ¡Ahora ve a construir!