Este artigo é um exemplo/tutorial de como criar uma função lambda na AWS para gerar thumbnails.
Para entender melhor o conteúdo deste artigo, é necessário o conhecimento básico sobre terraform, o que é AWS (Amazon Web Services) e Node JS.
Download do código fonte aqui.
Quais ferramentas vamos utilizar?
AWS Lambda
Serviço para executar funções sem precisar da alocação de servidores. Possui diversos mecanismos de disparo, integra com as demais ferramentas da AWS e seu custo é baseado no tempo de execução e na quantidade de memória RAM alocada.
Informação importante, o lambda tem limitações de uso de disco (512MB na pasta /tmp).
AWS Sqs(Simple Queue Service)
Serviço de enfileiramento de mensagens.
AWS S3
Serviço de armazenamento com excelente disponibilidade, segurança e durabilidade.
FFMpeg
Ferramenta open-source composta por diversas bibliotecas para conversão, compactação, edição e até mesmo stream de vídeos e áudios.
Node JS
Motor(run time) multiplataforma construída para executar código Javascript.
Terraform
Ferramenta para criação de infraestrutura em Cloud Computing com código (AWS neste exemplo/tutorial).
Qual foi a minha motivação?
Durante alguns anos, nossa aplicação responsável por gerar Thumbnails dos vídeos de nossos usuários, teve junto em sua implantação a ferramenta ffmpeg no mesmo Container.
Nossas aplicações estão em um ambiente kubernetes.
Nossa plataforma tem um crescimento constante e nos últimos meses a aplicação de thumbnail apresentou erros durante a execução do ffmpeg. A ferramenta apresentava o erro associado ao consumo excessivo do processador e da memória do Pod.
Durante os maiores picos de requisição o provisionamento automático da aplicação não era suficiente e nem rápido o bastante para atender a demanda. Aumentar a memória dos Pods já não era mais viável.
Para resolver o problema em definitivo foi necessária uma pequena mudança na arquitetura da aplicação.
Criamos uma função lambda para realizar a tarefa de gerar thumbnails, adaptando nossas aplicações para trabalhar de forma assíncrona. A comunicação entre a API e a função lambda foi feita via filas de mensagem: uma fila para enviar as solicitações e outra para notificar a conclusão do trabalho.
Mãos à obra!
Node JS
Em nosso projeto temos três dependências cruciais:
ffmpeg-installer/ffmpeg
Realiza o download e faz a instalação do ffmpeg compatível
fluent-ffmpeg
ffmpeg é uma ferramenta executada em linha de comando. Esta dependência facilita a construção do comando em forma de objeto.
aws-sdk
Faz a integração com as ferramentas da AWS. Será utilizado para enviar mensagens para filas Sqs e realizar o upload para o s3 da imagem gerada.
Para começar, vamos criar uma classe para gerenciar a execução do ffmpeg.
thumbnail-util.js
// Busca onde o ffpmeg foi instalado
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path
var FFmpeg = require('fluent-ffmpeg')
FFmpeg.setFfmpegPath(ffmpegPath)
class ThumbnailGenerator {
contentType () {
return 'image/jpg'
}
exec (options) {
new FFmpeg({ source: options.source })
// Ignorar as trilhas de audio
.withNoAudio()
// Tempo do frame a ser utilizado
.setStartTime(options.startTime)
// Quantidade de frames a ser retirada
.takeFrames(1)
// Codec a ser utilizado
.withVideoCodec('mjpeg')
// Local para salvar o arquivo
.saveToFile(options.output)
// Imprimime o comando a ser executado
.on('start', (commandLine) => {
console.log(`command-line: ${commandLine}`)
})
// Se durante a execução do ffmpeg algum erro for lançado
// o capturamos aqui
.on('error', (err) => {
console.log('Error generating thumbnail:')
console.log(err)
if (options.onError) {
options.onError(err)
}
})
// Executado quando o comando terminar
.on('end', () => {
if (options.onEnd) {
options.onEnd()
}
})
}
}
module.exports = new ThumbnailGenerator()
Utilizando o aws-sdk criaremos uma classe para realizar o upload da imagem gerada para o s3.
s3-util.js
const AWS = require('aws-sdk')
const fs = require('fs')
//Não precisamos de nenhuma configuração adicional no client
//As credenciais já estão associadas a instância no lambda
let s3 = new AWS.S3()
//Criamos uma classe com a responsabilidade de subir nosso arquivo no bucket
class S3Util {
upload(key, orign, contentType) {
return s3.upload({
Bucket: process.env.BUCKET,
// caminho/caminho/arquivo.jpeg
Key: key,
Body: fs.createReadStream(orign),
ACL: 'private',
ContentType: contentType,
StorageClass: 'STANDARD_IA'
}).promise()
}
}
module.exports = new S3Util()
E, novamente com a ajuda do aws-sdk criaremos outra classe com a responsabilidade de enviar mensagens para uma fila SQS.
sqs-util.js
const AWS = require('aws-sdk')
class SqsUtil {
constructor() {
this.sqs = new AWS.SQS({region: process.env.REGION})
}
sendMessage (body, delay) {
var sqsMessage = {
// Caso você precise de atrasar a entrega da mensagem
DelaySeconds: delay ? delay : 10,
// As mensagens na fila precisam ser string
MessageBody: JSON.stringify(body),
QueueUrl: process.env.RESULT_QUEUE_URL
};
return new Promise( (res, rej) => {
this.sqs.sendMessage(sqsMessage, (err, data) => {
if (err) {
rej(err)
} else {
res(data.MessageId)
}
})
})
}
}
module.exports = new SqsUtil()
Criaremos mais duas classes: uma para receber e tratar a mensagem recebida pelo SQS e outra para processar a mensagem.
app.js
const thumbnail = require('./utils/thumbnail-util')
const s3util = require('./utils/s3-util')
const sqsUtil = require('./utils/sqs-util')
class App {
constructor (source, path, startTime) {
this.fileName = 'thumbnail.jpeg'
this.output = `/tmp/${this.fileName}`
this.bucketFileKey = `${path}/${this.fileName}`
this.path = path
this.source = source
this.startTime = startTime
}
async run() {
try {
await this.generateThumbnail()
await this.uploadThumbnail()
await this.notifyDone()
} catch (e) {
console.log('Unexpected error')
console.log(e)
await this.notifyError()
}
}
generateThumbnail () {
console.log("generating thumbnail STARTED")
return new Promise ( (res, rej) => {
thumbnail.exec({
source: this.source,
output: this.output,
startTime: this.startTime,
onError: (err) => {
console.log(`generating thumbnail FINISHED WITH ERROR: ${err}`)
rej(err)
},
onEnd: () => {
console.log(`generating thumbnail FINISHED`)
res()
}
})
})
}
uploadThumbnail () {
console.log('Uploading thumbnail to S3')
return s3util.upload(
this.bucketFileKey,
this.output,
thumbnail.contentType())
}
notifyError() {
let body = {
source : this.source,
startTime : this.startTime,
key : this.bucketFileKey,
path : this.path,
success: false
}
console.log('Sending error message to Sqs')
return sqsUtil.sendMessage(body, 0)
}
notifyDone() {
let body = {
source : this.source,
startTime : this.startTime,
key : this.bucketFileKey,
path : this.path,
success: true
}
console.log('Sending success message to Sqs')
return sqsUtil.sendMessage(body, 0)
}
}
module.exports = App
index.js
const App = require('./main/app')
/* Função para validar o corpo da mensagem.
{
Records: [
{
body: "{raw json message}"
}
]
}
*/
let messageParser = (event) => {
//Records[] sempre há um item no array
let strbody = event.Records[0].body
try {
let message = JSON.parse(strbody)
if (!message.hasOwnProperty('source') ||
!message.hasOwnProperty('path') ||
!message.hasOwnProperty('startTime')) {
console.log('unparseable sqs message')
console.log(message)
} else {
return message;
}
} catch (error) {
console.log('unparseable sqs message')
console.log(strbody)
}
}
//este é o método a ser executado inicialmente pelo lambda
exports.handler = (event, context) => {
let message = messageParser(event)
if (message) {
let app = new App(
//source será a url do vídeo
message.source,
//Path é o diretório no qual o arquivo gerado será salvo.
message.path,
//Segundo do vídeo do qual a imagem será extraída
message.startTime)
app.run()
}
}
//Expondo o método método messageParser apenas para teste unitário
exports.messageParser = messageParser;
Terraform
Inicialmente vamos utilizar o terraform para criar um bucket para fazer upload do código do lambda.
Criaremos um bucket privado com o nome “example-application-uploader” no s3 com a classe de armazenamento padrão (STANDARD). Ser privado significa que o acesso aos arquivos armazenados pode ser feito apenas por pessoas/aplicações autenticadas ou por URLs assinadas.
Obs: O código fonte do projeto contém dois diretórios para o terraform, pois este recurso pertence à infraestrutura e não à aplicação.
resource "aws_s3_bucket" "application-uploader-files-bucket" {
bucket = "example-application-uploader"
acl = "private"
tags = {
Team = "Devops"
Terraform = "TRUE"
}
}
O código abaixo cria duas filas: uma para enviar ao lambda os vídeos que precisam da geração de thumbnails e outra com o resultado da operação. As filas possuem 5 minutos de retenção da mensagem, significando que a aplicação que consumir a mensagem tem até 5 minutos para processar e deletar a mensagem, caso contrário, esta voltará para a fila.
resource "aws_sqs_queue" "thumbnail_request_queue" {
name = "thumbnail-request-queue"
visibility_timeout_seconds = 300
tags = {
Team = "Thumbnail",
Terraform = "TRUE"
}
}
resource "aws_sqs_queue" "thumbnail_result_queue" {
name = "thumbnail-result-queue"
visibility_timeout_seconds = 300
tags = {
Team = "Thumbnail",
Terraform = "TRUE"
}
}
Vamos criar um segundo bucket para salvar as imagens geradas pelo lambda
resource "aws_s3_bucket" "thumbnails-s3-bucket" {
bucket = "example-thumbnail-generator-files"
acl = "private"
tags = {
Team = "Thumbnail"
Terraform = "TRUE"
}
}
O código a seguir, cria o lambda, o gatilho, as políticas de acesso e o Cloud Watch para armazenar o log.
# Cria grupo de log no cloudwatch.
# Infelizmente é a melhor forma de debugar o lambda (Cloud Watch custa caro)
# e tbm é o logger mais fácil de ser plugado no serviço.
resource "aws_cloudwatch_log_group" "thumbnail_generator_lambda_log_group" {
name = aws_lambda_function.example-thumbnail-generator-lambda.function_name
retention_in_days = 1
}
#Criamos aqui a role com as permissões básicas para execução do serviço
resource "aws_iam_role" "thumbnail_generator_lambda_iam_role" {
name = "thumbnail_generator_lambda_iam_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
#aqui criamos uma política definindo quais são os recursos da aws que o lambda
#pode acessar.
#Estamos o autorizando a escrever, enviar e apagar mensagens nas filas,
#ler, listar, salvar e editar arquivos no bucket e escrever os
#logs no Cloud Watch.
resource "aws_iam_role_policy" "thumbnail_generator_lambda_iam_policy" {
name = "thumbnail_generator_lambda_iam_policy"
role = aws_iam_role.thumbnail_generator_lambda_iam_role.id
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:SendMessage",
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:ChangeMessageVisibility"
],
"Resource": [
"arn:aws:sqs:us-east-1:YOURACCOUNTID:thumbnail-request-queue",
"arn:aws:sqs:us-east-1:YOURACCOUNTID:thumbnail-request-queue/*",
"arn:aws:sqs:us-east-1:YOURACCOUNTID:thumbnail-result-queue",
"arn:aws:sqs:us-east-1:YOURACCOUNTID:thumbnail-result-queue/*"
]
},
{
"Effect": "Allow",
"Action": [
"sqs:ListQueues"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:GetObjectAcl"
],
"Resource": [
"arn:aws:s3:::example-thumbnail-generator-files",
"arn:aws:s3:::example-thumbnail-generator-files/*"
]
}
]
}
EOF
}
#Cria a função lambda
resource "aws_lambda_function" "example-thumbnail-generator-lambda" {
#Como nosso arquivo compactado é muito grande, uma conexão
#com baixa taxa de upload pode causar erro durante a execução do terraform.
#Eu escolhi fazer o upload da aplicação para o s3 para evitar este tipo de problema
s3_bucket = "example-application-uploader"
s3_key = "thumbnail/lambda.zip"
#Uma alternativa ao S3 é utilizar o filebase64sha256
#recomendo apenas projetos onde o arquivo zip é pequeno.
#filename = "lambda.zip"
#source_code_hash = filebase64sha256("lambda.zip")
function_name = "example_thumbnail_generator_lambda"
role = aws_iam_role.thumbnail_generator_lambda_iam_role.arn
#Definição da localização do método principal
handler = "index.handler"
runtime = "nodejs10.x" // 12.x já disponível
#Recomendo a utilização de 512MB de RAM para execução do lambda.
#Fiz meus testes com um vídeo de 14.4Gb e o lambda gastou 438Mb de
#memória. A quantidade de memória utilizada vai variar conforme o tamanho (em tempo e/ou arquivo).
# que você pretende utilizar
#memory_size = 512
memory_size = 128 // Free Tier
timeout = 60 // Duração máxima obs: (no meu teste durou 5 segs com o arquivo de 14.4Gb)
publish = true
#aqui podemos declarar as variáveis de ambiente. Muito útil para rodar a aplicação
#em ambientes diferentes.
environment {
variables = {
RESULT_QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/YOURACCOUNTID/thumbnail-result-queue",
BUCKET = "example-thumbnail-generator-files",
REGION = "us-east-1"
}
}
}
#Este trecho cria o gatilho do nosso lambda. No caso é a nossa fila thumbnail-request-queue.
#Basicamente sempre que chegar uma mensagem a aws dispara nosso lambda
resource "aws_lambda_event_source_mapping" "thumbnail_generator_lambda_source_mapping" {
event_source_arn = "arn:aws:sqs:us-east-1:YOURACCOUNTID:thumbnail-request-queue"
enabled = true
function_name = aws_lambda_function.example-thumbnail-generator-lambda.arn
#Maior número de registros que o lambda pode receber por execução
batch_size = 1
}
Implantação
Você pode clicar aqui para ver um vídeo com o passo a passo para implantação ou seguir o script a baixo.
#!/bin/sh
cd terraform-infra
terraform init
terraform apply -auto-approve
cd ..
npm install --production
zip lambda.zip -r node_modules main package.json index.js
aws s3 cp lambda.zip s3://example-application-uploader/thumbnail/lambda.zip
cd terraform
terraform init
terraform apply -auto-approve
Testes
Abra o console da AWS no navegador e acesse a página do Sqs
Vamos enviar manualmente uma mensagem na fila thumbnail-request-queue para executar o lambda.
{ "source" : "https://somePublicVideo.mp4", "path" : "path/in/s3/we/want/save", "startTime" : 1 }
Vamos até o cloudwatch ver o log do lambda
Sucesso! Vamos abrir a página do Sqs novamente e dar uma olhada na fila de resposta.
Conclusão
Nossos problemas com a geração dos thumbnails foram solucionados, uma vez que os erros com ffmpeg foram cessados. Ainda, reduzimos a quantidade de Pods, a quantidade de memória RAM e processador alocados para API de Thumbnail. Portanto, minha conclusão é que o Lambda é uma excelente forma de executar tarefas assíncronas, pois possuí fácil integração e pode aliviar o peso de processamento de dados complexos das APIs.
Já planejamos outras tarefas para migrar para o lambda, como a análise dos vídeos ou gerar watermark em documentos.
Essa foi a minha contribuição de hoje! Deixem nos comentários dúvidas ou compartilhe outras tarefas onde vocês também têm sucesso utilizando o lambda.
Espero ter ajudado, obrigado.
Top comments (0)