AWS Application Composer
Diseñe visualmente y cree rápidamente aplicaciones sin servidor

En Noviembre pasado se realizó el AWS re:Invent 2022 el máximo evento de AWS que se realizó en Las Vegas, Nevada que nos proporcionó una gran cantidad de anuncios, nuevos servicios y actualizaciones.
AWS Application Composer
Uno de esos servicios nuevos llamo mi atencion: AWS Application Composer (https://docs.aws.amazon.com/application-composer/latest/dg/what-is-composer.html ), un servicio que permite desde una interfaz Drag-and-Drop arrastrar y soltar distintos servicios asi como interconectarlos para poder crear una arquitectura cloud serverless, entregando el código en cloudformation.
De esta manera podemos concentrarnos en diseñar nuestra arquitectura y dejar que Application Composer construya cómo se logra, sin necesidad de ser un experto en AWS CloudFormation.
Podemos usarlo creando un proyecto de cero o cargando uno ya existente, en ambos casos tenemos 2 modos disponibles: conectados o no conectados,si elegimos el estado conectado los cambios que hagamos en las fuentes se verán reflejados en AWS Application Composer y viceversa, los cambios realizados en AWS Application Composer se reflejarán en el código fuente de nuestra aplicación.
Después de diseñar la aplicación serverless con AWS Application Composer, podemos usar cualquier servicio compatible con AWS CloudFormation para la implementación.
En mi caso, utilice: AWS SAM, que me permite realizar depuración y pruebas locales, así como ayudarme con el empaquetado y despliegue de mi aplicación.
La aplicación que desarrollé es un servicio de procesamiento de video. Para ello emplee un servicio que me ayuda con esta tarea que es AWS Elemental MediaConvert (https://aws.amazon.com/es/mediaconvert/) que funciona tomando un video como entrada y, transcodificando el video a una o más versiones diferentes y luego coloca estas versiones en otro bucket S3.

Para hacer la aplicación serverless, utilice un bucket S3 en donde se cargan los archivos a procesar, cada vez que se detectaba el evento cuando se cargaba un video al bucket, éste se enviaba a una cola SQS que era consumida por una función Lambda (está basada en NodeJS versión 18) que se encarga de llamar a AWS Elemental MediaConvert para el procesamiento del video.
Manos a la obra!
1- Configuración de AWS CLI
Necesitamos configurar AWS CLI en nuestro entorno local.
http://docs.aws.amazon.com/cli/latest/userguide/installing.html
Tenemos que tener nuestras credenciales de seguridad (Access keys)
Luego ejecutamos desde una terminal en su sistema:
aws configure
La CLI de AWS solicita varias cosas:
Las credenciales de usuario (Access keys)
La región que usaremos: us-east-1
El formato de salida predeterminado: json.
2 - Obtener el endpoint de la API de MediaConvert
Para ello iremos al panel del AWS Elemental MediaConvert y luego iremos al menú lateral izquierdo en la sección Account, este endpoint lo usaremos para una variable de entorno de nuestra función lambda
3- Crear un rol: media-convert-role
Necesitamos crear un rol para nuestra función lambda, MediaConvert necesita tener acceso a S3, así como a API Gateway. Sin esta función, MediaConvert simplemente no se ejecutará cuando intente invocarlo desde Lambda.
Iremos al panel IAM y luego a Roles, haremos clic en Crear rol
Seleccionamos AWS Service y luego en el select buscamos MediaConvert, clic en el botón Siguiente
AWS ya ha predefinido qué políticas necesita para este rol.
Elegimos Siguiente
Establecemos el nombre
Asigne a su función el nombre media-convert-role y luego haga clic en Crear rol.
Necesitaremos el ARN del rol para una variable de entorno de nuestra función lambda.
4 - Bucket de carga de videos a procesar
Desde AWS Application Composer tenemos el primer bucket que es donde se cargaran los videos a procesar, el nombre de este bucket por defecto es el nombre de nuestra pila o stack junto con el número de nuestra cuenta de AWS:
BucketName: !Sub ${AWS::StackName}-bucket-${AWS::AccountId}
Lo podemos encontrar en la pestaña Template o bien desde nuestras fuentes en template.yaml y buscar dentro de recursos a nuestro Bucket, podemos cambiarlo a otro nombre de nuestra preferencia.
5- Nuestra cola SQS
Cada vez que un video se cargue al bucket S3 definido en el paso 4, este evento es enviado a una cola SQS que se creará cuando despleguemos nuestro proyecto.
6- La función Lambda
En el caso de nuestra función Lambda podemos indicar varios parámetros dentro de AWS Application Composer, seleccionando el elemento y luego la opción Details que mostrará más opciones en el lateral derecho de la pantalla
Podemos especificar el runtime de nuestra Lambda, así como la ubicación del código de nuestra función (src/Function) y el método que se ejecuta cuando nuestra función es invocada (index.handler)
También podemos especificar otros valores desde esta pantalla, como es el caso de las políticas de permisos:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:GetRole
- iam:PassRole
- mediaconvert:*
Resource: arn:aws:iam::619747668685:role/media-convert-role
Y también las variables de entorno que necesitamos especificar:
MEDIA_ENDPOINT: el endpoint de mediaConvertMEDIA_ROLE: el ARN del rol previamente creadoTRANSCODED_VIDEO_BUCKET: el nombre de nuestro bucket de salida donde almacenaremos los videos procesador por MediaConvert
Todas las modificaciones que hagamos desde AWS Application Composer las veremos reflejadas en nuestro archivo local template.yaml
También debemos definir nuestro rol para la función lambda: aws-sam-lambda-role
El cual haremos referencia en nuestro archivo local template.yaml
El código fuente de la función lambda es:
'use strict';
const AWS = require('aws-sdk')
// Obtiene el valor de la variable de entorno MEDIA_ENDPOINT que está configurada en template.yml
const mediaConvert = new AWS.MediaConvert({
endpoint: process.env.MEDIA_ENDPOINT
});
// Obtiene el nombre del bucket donde se enviaran los videos procesados de la variable de entorno TRANSCODED_VIDEO_BUCKET que está configurada en template.yml
const outputBucketName = process.env.TRANSCODED_VIDEO_BUCKET;
exports.handler = async (event) => {
const s3Event = JSON.parse(event.Records[0].body)
try {
const key = s3Event.Records[0].s3.object.key;
const sourceKey = decodeURIComponent(key.replace(/\+/g, ' '));
const outputKey = sourceKey.split('.')[0];
// Establece la ubicación del video de entrada para la definición de trabajo de MediaConvert
const input = 's3://' + s3Event.Records[0].s3.bucket.name + '/' + key;
// Create S3 service object
const outputBucket = new AWS.S3({apiVersion: '2006-03-01'});
// Create the parameters for calling createBucket
var bucketParams = {
Bucket : outputBucketName
};
// call S3 to create the bucket
outputBucket.createBucket(bucketParams, function(err, data) {
if (err) {
console.log("Error", err);
} else {
console.log("Success", data.Location);
}
});
// Establece el bucket de salida para los nuevos archivos de video
const output = 's3://' + outputBucketName + '/' + outputKey + '/';
const job = {
// Obtiene el ARN del rol MediaConvert que se especifica en template.yml
"Role": process.env.MEDIA_ROLE,
"Settings": {
"Inputs": [{
"FileInput": input,
// Especifica el selector de audio para la definición de trabajo de MediaConvert. De manera predeterminada, nombrará una sola pista de audio en el video.
"AudioSelectors": {
"Audio Selector 1": {
"SelectorType": "TRACK",
"Tracks": [1]
}
}
}],
"OutputGroups": [{
"Name": "File Group",
"Outputs": [{
"Preset": "System-Generic_Hd_Mp4_Avc_Aac_16x9_1920x1080p_24Hz_6Mbps",
"Extension": "mp4",
"NameModifier": "_16x9_1920x1080p_24Hz_6Mbps"
}, {
"Preset": "System-Generic_Hd_Mp4_Avc_Aac_16x9_1280x720p_24Hz_4.5Mbps",
"Extension": "mp4",
"NameModifier": "_16x9_1280x720p_24Hz_4.5Mbps"
}, {
"Preset": "System-Generic_Sd_Mp4_Avc_Aac_4x3_640x480p_24Hz_1.5Mbps",
"Extension": "mp4",
"NameModifier": "_4x3_640x480p_24Hz_1.5Mbps"
}],
"OutputGroupSettings": {
"Type": "FILE_GROUP_SETTINGS",
"FileGroupSettings": {
"Destination": output
}
}
}]
}
};
const mediaConvertResult = await mediaConvert.createJob(job).promise();
console.log('mediaConvertResult ' , mediaConvertResult);
} catch (error) {
console.error(error);
}
};
Nuestra función lambda utiliza AWS SDK por lo que necesitamos agregar dicha dependencia dentro de nuestro package.json.
{
"name": "function",
"version": "1.0.0",
"devDependencies": {
"aws-sdk": "^2.1270.0"
}
}
Y luego de ello ejecutar npm i dentro de la carpeta donde su ubica este archivo src/Function/ y debemos agregar unas líneas a nuestro template.yaml.

Quedando finalmente así:
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AWS::StackName}-alfalfita-cloud-bucket-${AWS::AccountId}
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: alias/aws/s3
PublicAccessBlockConfiguration:
IgnorePublicAcls: true
RestrictPublicBuckets: true
NotificationConfiguration:
QueueConfigurations:
- Event: s3:ObjectCreated:*
Queue: !GetAtt Queue.Arn
- Event: s3:ObjectRemoved:*
Queue: !GetAtt Queue.Arn
DependsOn:
- BucketToQueuePermission
BucketBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref Bucket
PolicyDocument:
Id: RequireEncryptionInTransit
Version: '2012-10-17'
Statement:
- Principal: '*'
Action: '*'
Effect: Deny
Resource:
- !GetAtt Bucket.Arn
- !Sub ${Bucket.Arn}/*
Condition:
Bool:
aws:SecureTransport: 'false'
Queue:
Type: AWS::SQS::Queue
BucketToQueuePermission:
Type: AWS::SQS::QueuePolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: s3.amazonaws.com
Action: sqs:SendMessage
Resource: !GetAtt Queue.Arn
Condition:
ArnEquals:
aws:SourceArn: !Sub arn:${AWS::Partition}:s3:::${AWS::StackName}-alfalfita-cloud-bucket-${AWS::AccountId}
Queues:
- !Ref Queue
Function:
Type: AWS::Serverless::Function
Properties:
Description: !Sub
- Stack ${AWS::StackName} Function ${ResourceName}
- ResourceName: Function
CodeUri: src/Function
Handler: index.handler
Role: arn:aws:iam::619747668685:role/aws-sam-lambda-role
Runtime: nodejs18.x
MemorySize: 3008
Timeout: 30
Tracing: Active
Events:
Queue:
Type: SQS
Properties:
Queue: !GetAtt Queue.Arn
BatchSize: 1
Environment:
Variables:
MEDIA_ENDPOINT: https://lxlxpswfb.mediaconvert.us-east-1.amazonaws.com
MEDIA_ROLE: arn:aws:iam::619747668685:role/media-convert-role
TRANSCODED_VIDEO_BUCKET: serverless-video-transcoded-alfalfita-cloud-06122022
Policies:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:GetRole
- iam:PassRole
- mediaconvert:*
Resource: arn:aws:iam::619747668685:role/media-convert-role
Metadata:
BuildProperties:
UseNpmCi: true
FunctionLogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Retain
Properties:
LogGroupName: !Sub /aws/lambda/${Function}
Transform: AWS::Serverless-2016-10-31
Para más información:
6 - Instalar AWS SAM CLI
Para prepararnos para desplegar nuestra aplicación en la nube, necesitamos un servicio compatible con Cloudformation para la implementación, en este caso AWS SAM, la documentación para la instalación la podemos encontrar en:
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html
Una vez instalado AWS SAM CLI podemos usarlo desde la terminal, con los diversos comandos que proporciona
Ejecutamos primero sam-build
Y luego preparamos el despliegue guiado que nos permitirá establecer ciertos valores:
Confirmamos los cambios, y veremos como los recursos se irán creando:
Confirmamos que la pila existe en el panel de CloudFormation
Ahora probamos nuestra función, subiendo un video al bucket S3
Veremos que tendremos un nuevo bucket donde encontraremos diversos formatos de nuestro video original
AWS Elemental MediaConvert: https://docs.aws.amazon.com/mediaconvert/index.html
El repositorio con la solución lo encuentras en:
https://github.com/alfalfita/decoder-video-with-sam-and-lambda
