Introducción a AWS Step Functions usando Terraform como herramienta de infrastructura como código

JJOC007
15 min readJul 17, 2023

Introducción

Las arquitecturas basadas en microservicios y sin servidor están en auge en el panorama actual de la tecnología de la información. Sin embargo, la coordinación y el manejo de errores entre estos servicios pueden ser un desafío. Aquí es donde el servicio de AWS Step Functions entra en juego, ya que proporciona una herramienta para orquestar componentes de microservicios y manejar flujos de trabajo en AWS.

En este artículo, vamos a introducir el concepto de las funciones de paso de AWS, explicando su utilidad y sus ventajas en la creación de aplicaciones basadas en la nube. Luego, como un caso práctico, mostraremos cómo implementar y manejar las funciones de paso de AWS usando Terraform, una herramienta de infraestructura como código muy popular. La finalidad de este artículo es proporcionar una comprensión clara de las funciones de paso de AWS y su implementación mediante Terraform, para que pueda aprovechar al máximo este servicio y mejorar la eficiencia y la robustez de sus aplicaciones.

Qué es AWS Step Functions

AWS Step Functions es un servicio de orquestación de flujo de trabajo completamente administrado que facilita la coordinación de componentes de aplicaciones distribuidas. Este servicio permite a los desarrolladores diseñar visualmente flujos de trabajo, o ‘funciones de paso’, que coordinan los componentes de sus aplicaciones en un patrón específico, como secuencias, bifurcaciones y combinaciones.

Características AWS Step Functions

Gestión de Estado

AWS Step Functions realiza un seguimiento del estado de cada flujo de trabajo y mantiene su actividad y datos en todas las etapas.

Retries y Manejo de Errores

Proporciona manejo automático de errores y reintentos.

Visualización

Ofrece una interfaz gráfica para visualizar y modificar flujos de trabajo.

Compatibilidad

AWS Step Functions puede interactuar con casi todos los otros servicios de AWS.

Programación

Los desarrolladores pueden programar la lógica de coordinación y condicional en su aplicación, en lugar de implementarla en el código.

Ventajas de usar AWS Step Functions

Resiliente

AWS Step Functions tiene funcionalidades integradas de manejo de errores, lo que hace que los flujos de trabajo sean resistentes a los fallos.

Escalable

Como servicio administrado, AWS Step Functions puede escalar según sea necesario para ejecutar flujos de trabajo.

Reducir el código

AWS Step Functions elimina la necesidad de escribir código de ‘pegamento’ para coordinar microservicios.

Fácil de monitorizar

AWS Step Functions está integrado con CloudWatch, permitiendo un fácil monitoreo de los flujos de trabajo.

Desventajas de usar AWS Step Functions

Costo

Aunque AWS Step Functions puede reducir la cantidad de código que necesitas escribir, no es gratuito. El costo puede acumularse rápidamente para flujos de trabajo complicados o de alto volumen.

Complejidad

AWS Step Functions introduce un nuevo nivel de complejidad a la aplicación, ya que ahora se debe gestionar y entender el servicio de Step Functions.

Limitaciones de tiempo

Cada ejecución de una función de paso tiene una duración máxima de un año. Esto puede no ser adecuado para algunos flujos de trabajo a largo plazo.

Dependencia del proveedor

Al usar AWS Step Functions, te estás bloqueando en la plataforma AWS. Si alguna vez necesitas migrar a otra plataforma en el futuro, este podría ser un factor limitante.

Qué es Terraform

Terraform es una herramienta de Infraestructura como Código (IaC) de código abierto creada por HashiCorp. Permite a los desarrolladores definir y proporcionar la infraestructura de los centros de datos utilizando un lenguaje de configuración declarativo. Esto incluye servidores, almacenamiento y redes en una variedad de proveedores de servicios en la nube, incluido AWS.

Ventajas de usar Terraform

Plataforma agnóstica

A diferencia de las herramientas de IaC específicas del proveedor, como AWS CloudFormation, Terraform es agnóstico al proveedor de la nube, lo que significa que puede funcionar con cualquier proveedor de servicios en la nube, incluidos AWS, Google Cloud, Azure y otros.

Lenguaje de configuración declarativo

Terraform utiliza un lenguaje de configuración declarativo, lo que significa que especificas qué recursos deseas crear sin tener que detallar las etapas para crearlos.

Gestión del estado

Terraform mantiene un estado de tu infraestructura y puede determinar qué se ha cambiado desde la última ejecución, lo que permite una planificación eficaz de los cambios.

Desventajas de usar Terraform

Complejidad

Aunque Terraform puede ser muy potente, también puede ser complicado de configurar y utilizar correctamente.

Documentación

A veces, la documentación de Terraform puede ser insuficiente o confusa, especialmente para casos de uso más complejos.

Velocidad de despliegue

Terraform puede ser más lento para admitir nuevas características de los servicios de nube en comparación con las herramientas de IaC específicas del proveedor.

Ejemplo Básico en Terraform “Hello World”

A continuación, se muestra un ejemplo básico de cómo se vería un script de Terraform para crear un solo servidor EC2 en AWS. Esto es equivalente a un ‘Hola Mundo’ en Terraform:

provider "aws" {
region = "us-west-2"
}

resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"

tags = {
Name = "example-instance"
}
}

Este es un script muy simple. Aquí está lo que está sucediendo:

  • provider “aws”: Esto especifica que vamos a utilizar AWS como proveedor para nuestro recurso. La región se especifica dentro de este bloque.
  • resource “aws_instance” “example” : Esto define un recurso, en este caso una instancia EC2. “example” es un nombre arbitrario que le damos a este recurso.
  • Dentro del bloque de recursos, especificamos la ID de la imagen de la máquina de Amazon (AMI) que queremos utilizar para nuestra instancia y el tipo de instancia. En este caso, estamos utilizando una AMI para una instancia básica de Ubuntu y t2.micro para el tipo de instancia, que es la opción de menor costo.
  • El bloque tags permite agregar etiquetas a la instancia, en este caso, simplemente le damos a la instancia el nombre “example-instance”.

Para ejecutar este script, debes tener instalado Terraform y las credenciales de AWS configuradas en tu entorno. Luego puedes inicializar Terraform con terraform init, planificar la ejecución con terraform plan y finalmente aplicar los cambios con terraform apply.

Arquitectura del ejemplo a seguir

En esta sección vamos a ver cómo podemos implementar un flujo de pasos sencillo que podemos implementar con el servicio de AWS Step functions

Arquitectura general Ejemplo AWS Step functions

El flujo inicia con la ejecución de una lambda que crea un número aleatorio entre de 1 a 100. Éste número va a ser validado por el flujo, si el número es par va a ejecutarse la lambda “Even” y si es impar va a ejecutarse la lambda “Odd”. Luego de esto terminamos el flujo.

Para dejar funcional el ejemplo debemos realizar los siguientes pasos:

  • Crear la lógica de la Lambda “number Generator”: Esta lambda simplemente va a generar un número aleatorio entre 1 a 100 y luego ese número generado lo va a retornar como respuesta. Esta lambda va a estar desarrollada en Node 18.x
  • Crear la lógica de la Lambda “Even”: Esta lambda va a recibir como parámetro de entrada el número generado anteriormente y va a imprimir un mensaje en logs especificando que el número es Par. Al igual que la lambda anterior la lógica va a ser desarrollada en Node 18.x
  • Crear la lógica de la Lambda “Odd” : Está lambda va a recibir como parámetro de entrada el número generado anteriormente y va a imprimir un mensaje en logs especificando que el número es Impar.
  • Crear código base del proyecto de terraform: Vamos a crear un proyecto inicial de terraform importando el provider de AWS.
  • Generar un mecanismo de empaquetamiento de cada una de las lambdas: para poder subir esta lógica como función lambda a a AWS es necesario que estén empaquetadas como archivos .zip. Esto lo vamos a realizar por medio de terraform.
  • Crear Infraestructura de las 3 lambdas por medio de Terraform: En la base de código creada en terraform vamos a agregarle la infraestructura de las 3 lambdas anteriormente mencionadas y a asociar el código de la lógica de cada una de ellas.
  • Crear infraestructura del step function que vamos a utilizar en el proyecto de terraform: En la base de código creada en terraform vamos a implementar los recursos de infraestructura necesarios para crear el step function teniendo en cuenta el flujo planteado anteriormente.
  • Crear componentes de infraestructura que permita a la step function ejecutar las lambdas: Es necesario agregar un rol al step function para que pueda ejecutar las lambdas especificadas anteriormente.
  • ¡Probar el step function creado! 👏 👏

Implementación del ejemplo

Ya que hemos explicado el ejemplo que vamos a realizar con AWS Step Functions vamos a implementar cada paso.

Crear la lógica de la Lambda “number Generator”

Para la lambda a crear vamos a crear una carpeta donde vamos a almacenar la logica de ella. en esta carpeta vamos a tener el archivo package.json y main.js

Estrcutura inicial de archivos para lambdas

El archivo package.json va a contener esto:

{
"name": "number-generator",
"version": "1.0.0",
"description": "number-generator lambda",
"main": "main.js",
"dependencies": {},
"devDependencies": {
"aws-sdk": "^2.1045.0"
}
}

El archivo main.js va a tener la logica para generar el número aleatorio y retornarlo, junto con la validacion de si el numero es o no par:

exports.handler = async event => {
var number = Math.floor(Math.random() * 100) + 1;
console.log(`El numero generado es: ${number}`);
return {number: number, is_even: number % 2 === 0}
};

Crear la lógica de la Lambda Even

Para la lógica de la lambda even vamos a hacer lo mismo que para la lambda anterior tener un archivo package.json y main.js

package.json:

{
"name": "even number",
"version": "1.0.0",
"description": "even number lambda",
"main": "main.js",
"dependencies": {},
"devDependencies": {
"aws-sdk": "^2.1045.0"
}
}

main.js:

exports.handler = async event => {
console.log(`Mi numero par es: ${event.number}`);
return {number: event.number}
};

Crear la lógica de la Lambda Odd

Por último lor achivos de la lambda Odd:

package.json:

{
"name": "odd number",
"version": "1.0.0",
"description": "odd number lambda",
"main": "main.js",
"dependencies": {},
"devDependencies": {
"aws-sdk": "^2.1045.0"
}
}

main.js:

exports.handler = async event => {
console.log(`Mi numero impar es: ${event.number}`);
return {number: event.number}
};

Para la creación de la lógica de las 3 lambdas es importante tener en cuenta que:

  • Se agregó la dependencia aws-sdk, para el ejemplo no se va a utilizar pero se agrega para ilustrar cómo podemos importar librerías propias de aws para poder interactuar con los servicios.
  • Es importante que en cada carpeta de lambda ejecutemos el comando npm i, para que se puedan instalar las dependencias que estamos agregando a cada una de las lambdas.
  • La estructura de carpetas final debe quedar similar a esta:
Structura final de carpetas para la logica de las lambdas

Crear código base del proyecto de terraform

Al igual que la lógica de las lambdas debemos tener una carpeta en la cual podamos almacenar todo el proyecto de terraform que nos va a permitir crear los componentes de infraestructura necesarios para que el ejemplo pueda funcionar. Inicialmente vamos a tener una carpeta llamada terraform y un archivo llamado main.tf:

Structura inical de carpetas para el proyecto en terraform

En el archivo main.tf, definiremos los proveedores que emplearemos para crear los componentes de nuestra infraestructura. Un proveedor en Terraform representa el conjunto de recursos que podemos aprovechar para configurar y gestionar componentes:

  • aws: Este proveedor suministra todos los componentes de infraestructura que podemos utilizar de Amazon Web Services (AWS).
  • archive: Este proveedor proporciona funcionalidades utilitarias para el manejo de archivos. Usaremos este proveedor para generar los archivos finales de cada una de las funciones Lambda que hemos estado creando.

Contenido del archivo main.tf:

provider "aws" {
version = "~> 3.0"
region = "us-east-1"
}

provider "archive" {}

Ahora debemos inicializar el proyecto, para esto debemos ejecutar el comando terraform init:

Proyecto inicializado en terraform

De esta manera ya tenemos la base de proyecto terraform y ya esta listo para iniciar con la creacion de componentes de infraestructura

Generar un mecanismo de empaquetamiento de cada una de las lambdas

La lógica de cada una de las lambdas debe estar empaquetado en un archivo .zip para poderlo subir a AWS, para esto amor a utilizar el provider archive.

Para esto vamos a crear un archivo nuevo en el proyecto de terraform llamado lambda_resources.tf

data "archive_file" "number_generator" {
type = "zip"
source_dir = "../lambdas/1_number_generator/"
output_path = "../lambdas/dist/1_number_generator.zip"
}

data "archive_file" "even" {
type = "zip"
source_dir = "../lambdas/2_even/"
output_path = "../lambdas/dist/2_even.zip"
}

data "archive_file" "odd" {
type = "zip"
source_dir = "../lambdas/3_odd/"
output_path = "../lambdas/dist/3_odd.zip"
}

Cada bloque de codigo utiliza el proveedor archive_file para crear un archivo zip a partir de un directorio de origen. Vamos a desglosar cada línea:

  • data archive_file number_generator: Esta línea define un nuevo recurso de tipo archive_file llamado number_generator. La palabra data indica que estamos obteniendo datos de un recurso existente, en lugar de crear uno nuevo.
  • type = zip: Esta línea especifica el tipo de archivo de salida que queremos. En este caso, estamos creando un archivo zip.
  • source_dir = ../lambdas/1_number_generator/: Esta línea especifica el directorio de origen que queremos comprimir. Todos los archivos y subdirectorios dentro de ../lambdas/1_number_generator/ serán incluidos en el archivo zip.
  • output_path = ../lambdas/dist/1_number_generator.zip: Esta línea especifica la ruta y el nombre del archivo de salida. El archivo zip resultante se guardará en ../lambdas/dist/1_number_generator.zip.

Este tipo de operación es bastante común en aplicaciones serverless como AWS Lambda, donde necesitas subir tu código de función en formato zip a AWS. Por lo tanto, este bloque de código te ayuda a preparar tu función Lambda para su despliegue, empaquetando el código de la función en un archivo zip que luego se puede subir a AWS.

Para ver el resultado de esto ejecutamos terraform apply. Luego de esto veremos que ya se han creado las lambdas empaquetadas:

Lambdas Empaquetadas

Crear Infraestructura de las 3 lambdas por medio de Terraform

Ya teniendo la lógica de las lambdas generadas y listas para subir a AWS, debemos crear la infraestructura en el proyecto de terraform, para lo cual vamos tenemos que:

  • Crear un rol de IAM que van a utilizar las lambdas, esto es necesario para asociar permisos a cada una de ellas.
  • Crear los recursos de infraestructura de cada lambda y asociarlas al rol creado anteriormente

Rol IAM para las lambdas

Para este paso, vamos a crear un nuevo archivo llamado iam.tf con el siguiente contenido:

resource "aws_iam_role" "example_lambda_role" {
name = "example_lambda_role_for_numbers"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

En este segmento de codigo tenemos que:

  • resource aws_iam\_role example_lambda_role: Esta línea está declarando un recurso de Terraform del tipo aws_iam_role (un rol IAM en AWS) con el nombre local example_lambda_role. Este nombre local es el que usarías dentro de tu configuración de Terraform para referirte a este recurso.
  • name = example_lambda_role_for_numbers: Este es el nombre que tendría el rol de IAM en AWS.
  • assume_role_policy = EOF : Esto es un documento de política que define qué entidades están permitidas para asumir el rol. En este caso, el servicio de Lambda de AWS.

La política en sí misma es un documento JSON que contiene una lista de declaraciones, y cada declaración define una regla. En este caso, hay una única declaración:

  • Action: sts:AssumeRole: Esta acción permite a las entidades asumir el rol.
  • Principal: Service: lambda.amazonaws.com: Esta es la entidad que tiene permitido asumir el rol. En este caso, es el servicio de Lambda de AWS.
  • Effect: Allow: Esta es la decisión de la política. En este caso, está permitiendo (Allow) la acción.
  • Sid: Este es el ID de la declaración de la política. En este caso, está vacío, pero puedes usarlo para darle un identificador único a cada declaración.

Este código, en definitiva, está creando un rol de IAM que permite que las funciones de AWS Lambda asuman este rol. Este es un patrón común en AWS cuando quieres permitir que tus funciones Lambda interactúen con otros servicios de AWS.

Recursos de infraestructura de las lambdas

Para este paso vamos a crear un arhivo llamado lambda.tf y va a tener el siguiente contenido:

resource "aws_lambda_function" "number_generator_lambda" {
filename = data.archive_file.number_generator.output_path
source_code_hash = data.archive_file.number_generator.output_base64sha256
function_name = "poc_number_generator_lambda"
role = aws_iam_role.example_lambda_role.arn
handler = "1_number_generator/main.handler"
runtime = "nodejs18.x"
}

resource "aws_lambda_function" "even_lambda" {
filename = data.archive_file.even.output_path
source_code_hash = data.archive_file.even.output_base64sha256
function_name = "poc_even_lambda"
role = aws_iam_role.example_lambda_role.arn
handler = "2_even/main.handler"
runtime = "nodejs18.x"
}

resource "aws_lambda_function" "odd_lambda" {
filename = data.archive_file.odd.output_path
source_code_hash = data.archive_file.odd.output_base64sha256
function_name = "poc_odd_lambda"
role = aws_iam_role.example_lambda_role.arn
handler = "3_odd/main.handler"
runtime = "nodejs18.x"
}

Este fragmento de código es un ejemplo de cómo usar Terraform para crear una función de AWS Lambda. AWS Lambda es un servicio que te permite ejecutar tu código sin provisionar o administrar servidores. Solo cargas tu código (conocido como función Lambda) y Lambda se encarga del resto.

Analizaremos cada línea del código para entender mejor qué está haciendo:

  • resource aws_lambda_function odd_lambda: Esta línea está declarando un recurso de Terraform del tipo aws_lambda_function con el nombre local odd_lambda. Terraform usa este nombre local para referirse a este recurso en otros lugares de la configuración.
  • filename = data.archive_file.odd.output_path: Esta línea especifica la ruta del archivo zip que contiene el código de la función Lambda. En este caso, está utilizando un recurso de archivo de datos para generar este archivo zip. data.archive_file.odd.output_path se refiere a la ruta de salida del archivo generado
  • source_code_hash = data.archive_file.odd.output_base64sha256 : Esto es un hash del código fuente de la función Lambda. Terraform usa este hash para determinar si el código fuente ha cambiado y si necesita volver a desplegar la función Lambda.
  • function_name = poc_odd_lambda : Este es el nombre que tendrá la función Lambda en AWS.
  • role = aws_iam_role.example_lambda_role.arn: Este es el ARN (Amazon Resource Name) del rol IAM que la función Lambda asumirá cuando se ejecute. En este caso, está haciendo referencia al rol IAM creado en el ejemplo anterior.
  • handler = 3_odd/main.handler: Este es el manejador de la función Lambda, que es la función en tu código que Lambda llama cuando se ejecuta la función. El formato es file.function. En este caso, 3_odd/main.handler significa que Lambda llamará a la función handler en el archivo main del directorio odd.
  • runtime = nodejs18.x: Este es el entorno de ejecución en el que se ejecutará la función Lambda. En este caso, la función se ejecutará en un entorno Node.js 18.x.

En resumen, este código de Terraform está creando una función Lambda que ejecutará el código ubicado en la ruta especificada en un entorno de Node.js 18.x, asumirá un rol IAM específico cuando se ejecute y tendrá un nombre específico en AWS.

Para probar esto vamos a ejecutar el comando terraform apply y vamos a darle yes en la confirmacion:

Recursos creados

Luego si vamos a la consola de AWS veremos las lambdas ya creadas:

Recursos creados

Crear componentes de infraestructura que permita a la step function ejecutar las lambdas

Para que las lambdas creadas anteriormente puedan ser ejecutadas por un step function es necesario crear un rol que pueda asumir el step function que le permita ejecutar las lambdas. Para esto vamos a crear los siguientes recursos dentro del archivo iam.tf

  • resource aws_iam_role step_functions_role: Rol que va a asumir la step function
  • data aws_iam_policy_document lambda_access_policy: IAM POlicy que va a tener el rol de la step function
  • resource aws_iam_policy step_functions_policy_lambda : Recurso de IAM policy para asociar al rol de la step function
  • resource aws_iam_role_policy_attachment step_functions_to_lambda: Asociacion explicita de la IAM Policy al Rol

resource aws_iam_role step_functions_role

resource "aws_iam_role" "step_functions_role" {
name = "step_functions_role_poc_sf"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "states.amazonaws.com"
}
}
]
})
}

data aws_iam_policy_document lambda_access_policy

data "aws_iam_policy_document" "lambda_access_policy" {
statement {
actions = [
"lambda:*"
]
resources = ["*"]
}
}

resource aws_iam_policy step_functions_policy_lambda

resource "aws_iam_policy" "step_functions_policy_lambda" {
name = "step_functions_policy_lambda_policy_all_poc_sf"
policy = data.aws_iam_policy_document.lambda_access_policy.json
}

resource aws_iam_role_policy_attachment step_functions_to_lambda

resource "aws_iam_role_policy_attachment" "step_functions_to_lambda" {
role = aws_iam_role.step_functions_role.name
policy_arn = aws_iam_policy.step_functions_policy_lambda.arn
}

Crear infraestructura del step function que vamos a utilizar en el proyecto de terraform

En esta sección vamos a crear los componentes de infraestructura que nos permita crear la step function con las interacciones a las lambdas creadas anteriormente

Para esto vamos a crear un archivo llamado step_function.tf en el proyecto de terraform y con el siguiente contenido:

# Step Functions State Machine
resource "aws_sfn_state_machine" "number_processor_sf" {
name = "NumberProcessorSF"
role_arn = aws_iam_role.step_functions_role.arn

definition = <<EOF
{
"Comment": "execute lambdas",
"StartAt": "NumberGenerator",
"States": {
"NumberGenerator": {
"Type": "Task",
"Resource": "${aws_lambda_function.number_generator_lambda.arn}",
"Next": "IsNumberEven"
},
"IsNumberEven": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.Payload.is_even",
"BooleanEquals": true,
"Next": "Even"
}
],
"Default": "Odd"
},
"Even": {
"Type": "Task",
"Resource": "${aws_lambda_function.even_lambda.arn}",
"End": true
},
"Odd": {
"Type": "Task",
"Resource": "${aws_lambda_function.odd_lambda.arn}",
"End": true
}
}
}
EOF
}

Para aplicar cambios sobre la nube al igual que en pasos anteriores basta con un terraform apply

¡Probar el step function creado!

Dentro de la consola de amazon debemos ir al servicio teps functions y debemos observar que existe el step function llamado: NumberProcessorSF

Detalle Step Function Creado

En la sección definition podremos ver un diagrama con el flujo definido, el cual es similar a la arquitectura definida al inicio del ejercicio:

Definición Step Function Creado

Ahora para probarlo vamos a la pestaña Executions y damos click al boton Start Execution:

Ejecutar Step Function Creado

En esta vista podemos ver el flujo de nuestra ejecución y cada uno de los pasos que se dieron para completar la ejecución. Adicionalmente si damos click en cualquiera de los pasos podremos ver logs, entradas y salidas de lo que va sucediendo, facilitando así la trazabilidad de cada ejecución.

Asi se ve una ejecución exitosa:

Ejecución Exitosa

Enlaces Informativos

Gracias 👏 👏

--

--