Blog de Amazon Web Services (AWS)
Despliegue de función AWS Lambda y su capa con CICD, haciendo uso de AWS CDK
Por Juan Miguel Bermúdez Mieles, arquitecto de soluciones en Amazon Web Services para Sector Público.
En el desarrollo de aplicaciones serverless, agregar, modificar o eliminar funcionalidades rápidamente puede ser complejo sin un enfoque estructurado. Los principales desafíos incluyen:
- Complejidad en el despliegue manual: Sin automatización, gestionar dependencias, configurar infraestructura y actualizar código es propenso a errores y consume tiempo.
- Falta de control de versiones: No rastrear cambios dificulta mantener la coherencia del código y genera confusión.
- Riesgos sin CI/CD: La ausencia de un flujo de Integración y Despliegue Continuo permite que errores impacten la producción. Las pruebas unitarias y de integración son esenciales.
- Falta de cohesión entre equipos: En proyectos complejos, coordinar equipos trabajando en distintos componentes sin integración adecuada provoca conflictos.
- Dificultad en la colaboración: Sin un flujo estructurado, los cambios simultáneos complican la integración, afectando la eficiencia y la entrega.
Teniendo en cuenta los puntos mencionados anteriormente, procederemos a plantear una solución al problema.
Implementación de CICD
Dada la necesidad de implementar la integración continua y despliegue continuo en el ciclo de vida de nuestra aplicación serverless, haremos uso de AWS CDK para crear y administrar la infraestructura del pipeline de CICD mediante código (IaC) Infraestructura como código [1], en donde aprovisionaremos distintos stacks (Pilas) de los recursos que usaremos en este proyecto. Dentro de este proyecto estaremos haciendo uso de tres pilas como lo vemos en la imagen a continuación:
Pila de Aplicación
En esta pila encontraremos los recursos asociados a la aplicación incluyendo una API y el backend, para esta, se usarán los siguientes servicios y características:
- AWS Lambda: Permite ejecutar código sin aprovisionar ni administrar servidores.
- Capas de Lambda: Es un archivo .zip que contiene código o datos adicionales.
- Alias de Lambda: Es un puntero a una versión de la función que se puede actualizar.
- Amazon API Gateway: Es un servicio para la creación, la publicación, el mantenimiento, el monitoreo y la protección de las API REST, HTTP y de WebSocket a cualquier escala.
Pila de llaves/secretos
Acá encontraremos los recursos que se crearán para manejar los secretos que serán usados por las otras pilas, por ejemplo las claves para interactuar con repositorios en GitHub, o para guardar los valores de api-keys que podrían ser usadas en las funciones lambdas. Dentro de esta pila, el servicio a usar será:
- AWS Secrets Manager: Ayuda a gestionar, recuperar y rotar las credenciales de las bases de datos, las credenciales de las aplicaciones, los tokens de OAuth, las claves de API y otros datos secretos a lo largo de sus ciclos de vida.
Pila de Pipeline/Canalización
Dentro de esta pila estarán los recursos necesarios para la creación del pipeline de Integración continua y despliegue continuo, donde encontraremos los distintos escenarios fuente, prueba, construcción y despliegue, cómo las acciones a realizar en los mismos. Los servicios a usar son:
- AWS CodePipeline: Es un servicio de entrega continua que puede utilizar para modelar, visualizar y automatizar los pasos necesarios para lanzar su software.
- AWS CodeBuild: Es un servicio de construcción en la nube totalmente gestionado, compila el código fuente, ejecuta pruebas unitarias y produce artefactos listos para su despliegue.
- AWS CodeDeploy: Es un servicio de implementación que automatiza las implementaciones de aplicaciones en instancias de Amazon EC2, instancias locales, funciones Lambda sin servidor o servicios de Amazon ECS.
Implementación
En este blog, se usará un repositorio en GitHub, el cual contendrá el código del proyecto de IaC con AWS CDK y el código de la función lambda de la aplicación, pero está pensado para usarse con dos repositorios independientes, que serán configurables por medio de variables de entornos. Estos repositorios independientes podrán ser administrados por dos equipos distintos, por ejemplo, el primer repositorio por el equipo infraestructura y el segundo por el equipo de desarrollo.
Estos repositorios servirán como fuentes para desencadenar las siguientes acciones de nuestra canalización (pipeline).
Creación de Token de Acceso Personal en Github
Para que AWS CodePipeline pueda tener como fuente GitHub, es necesario contar con un token de acceso personal clásico (Personal Access Token), el cuál puede ser creado de la siguiente manera:
- En nuestra cuenta de GitHub vamos a la pestaña de Settings
- Luego, en el panel lateral derecho, en la parte inferior del mismo hacemos clic en Developer settings.
- Desplegamos la pestaña de Personal access tokens, y hacemos clic en Tokens (classic)
- Hacemos clic en Generate new token (classic)
- Le damos un nombre al token en el campo Note
- Activamos los permisos de admin:repo_hook
- Luego hacemos clic en Generate token
El token generado tendrá un vencimiento por defecto de 30 días, este se puede cambiar al momento de crear el token. Si el repositorio es privado puede que sean necesarios otros permisos.
Una vez creado el token copiamos su valor y lo guardamos en un sitio seguro. Lo usaremos después.
Creación de repositorios en GitHub
Para realizar los siguientes pasos, como mencionamos anteriormente estaremos usando dos repositorios de GitHub, uno para la infraestructura y el otro para la lógica de la aplicación que estará en una función Lambda. Los nombres de los repositorios para este caso serán infra
y app
.
Agregando código al repositorio de la aplicación
Con los repositorios ya inicializados en nuestras máquinas, procederemos a crear el código de nuestra función lambda en el repositorio app
.
Dentro del repositorio creamos un archivo con el siguiente nombre app.py
en donde haremos la consulta del precio actual de Bitcoin por medio de una petición get. La url de esta API estará en la siguiente variable de entorno API_URL
, cuyo valor será https://api.coindesk.com/v1/bpi/currentprice.json
.
Y creamos nuestro archivo de requerimientos requirements.txt
con las siguientes librerías:
requests
Guardamos, y hacemos commit y un push para agregar nuestro archivo al repositorio.
Por ejemplo de la siguiente forma:
git add .
git commit -am "Agregando nuestro codigo de la funcion"
git push origin master
master
es el nombre de la rama actual del repositorio, este nombre puede varias en algunos casos.
Por lo pronto, no haremos más modificaciones a este repositorio.
Creando nuestra pila en el repositorio de Infraestructura
En este punto, debemos trabajar en el repositorio infra
previamente creado en Github, el cuál alojará el código de nuestra infraestructura.
Prerrequisitos
Para inicializar un proyecto con AWS CDK, debemos tener en cuenta lo siguiente:
- Instalación de AWS CLI
- Una cuenta de AWS con un usuario con permiso de administrador
- Configurar las credenciales en el AWS CLI con
aws configure
- Instalación de Node.js
- Instalación AWS CDK Toolkit
Con las configuraciones previamente realizadas procedemos a comprobar la versión del AWS CDK Toolkit, con el siguiente comando:
cdk version
y la salida debería ser algo parecido a 2.87.0 (build 9fca790)
.
Creamos una carpeta e inicializamos el proyecto
mkdir infra && cd infra
Usaremos el comando cdk init
para crear un nuevo proyecto de Python para CDK
cdk init sample-app --language python
Con la inicialización del proyecto se crea el entorno .venv
que debe ser activado, y se le instalará las librerías de AWS CDK que están en el archivo requirements.txt
.
. .venv/bin/activate
pip install -r requirements.txt
Configuración variables de entorno
Como se menciona con anterioridad, este proyecto puede usarse para separar el código de las funciones Lambdas y el código de la infraestructura, y para ello nos apoyaremos de las variables de entorno APP_REPO_NAME
y INFRA_REPO_NAME
, donde el valor de estas variables debería ser el nombre de los respectivos repositorios. Para efectos prácticos usaremos el mismo repositorio y el valor de ambas variables serán el mismo.
Modificación del código
Dentro de la ruta /infra/infra
crearemos un nuevo archivo secret_stack.py
que será el que tendrá la pila de nuestro secreto que será almacenado en AWS Secrets Manager:
# Rest of Code // Resto del código
class SecretStack(Stack):
# Rest of Code // Resto del código
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
secret = secretsmanager.Secret(
self, "my-github-token",
description="Secret for the github token",
generate_secret_string=secretsmanager.SecretStringGenerator()
)
Dentro del archivo app.py
localizado en la ruta /infra
importamos la clase donde se encuentra la pila con nuestro secreto, de la siguiente forma:
# Rest of Code // Resto del código
app = cdk.App()
secret = SecretStack(app, "secret")
infra = InfraStack(app, "infra")
app.synth()
Guardamos y hacemos el despligue de nuestra pila con el comando cdk deploy
de la siguiente forma:
cdk deploy secret
Después, nos vamos a la consola de AWS y navegamos a nuestro servicio de Secrets Manager y veremos nuestro secreto creado.
Hacemos clic en el nombre del secreto, luego nos dirigimos al recuadro Valor del secreto hacemos clic en Recuperar valor del secreto.
Por último hacemos clic en el botón Editar y pegamos el valor del token generado en Github y guardamos.
De regreso a nuestro código, crearemos la pila que soportará nuestra aplicación, con la creación del API Gateway y La función Lambda entre otros.
En la ruta infra/infra
crearemos un nuevo archivo llamado app_stack.py
y el código que tendrá es el siguiente:
class AppStack(Stack):
# Rest of Code // Resto del código
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_code = _lambda.Code.from_cfn_parameters()
lambda_layer_code = _lambda.Code.from_cfn_parameters()
app_function = _lambda.Function(
self, 'AppCoinDeskFunction',
runtime=_lambda.Runtime.PYTHON_3_10,
code=lambda_code,
handler='app.handler',
environment={
"API_URL": "https://api.coindesk.com/v1/bpi/currentprice.json"
}
)
# Rest of Code // Resto del código
self.alias = app_alias_dev
self.lambda_code = lambda_code
self.lambda_layer_code = lambda_layer_code
apigw.LambdaRestApi(
self, 'AppCoinDeskEndpoint',
handler=app_function,
)
En este caso como vamos a implementar Lambda a través de CodePipeline y no usaremos activos (Dado qué el código CDK y el código Lambda están separados), urar una clase de Lambda especial Code
, llamada CFNParametersCode
en los objetos lambda_code
y lambda_layer_code
.
Dentro del archivo app.py
localizado en la ruta /infra
importamos la clase donde se encuentra la pila de nuestra aplicación, de la siguiente forma:
#!/usr/bin/env python3
app = cdk.App()
application = AppStack(app, "app")
secret = SecretStack(app, "secret")
infra = InfraStack(app, "infra")
app.synth()
Nota: Esta pila será desplegada por nuestro pipeline
Creando nuestra pila para el pipeline de CICD
Para comenzar a construir la infraestructura sobre la que se va a soportar nuestro pipeline de integración continua y despliegue continuo, tendremos que modificar el archivo infra_stack.py
dentro de la ruta app_infra/infra/infra
. En esta pila estaremos haciendo referencia a los distintos servicios como AWS CodeBuild, AWS CodePipeline.
# Rest of Code // Resto del código
class InfraStack(Stack):
def __init__(
self,
scope: Construct,
id: str,
secrets,
lambda_code,
lambda_layer_code,
**kwargs
) -> None:
super().__init__(scope, id, **kwargs)
A nuestra clase InfraStack
, le estamos agregando tres parámetros adicionales, que provienen de las otras pilas, y de los cuales está pila es dependiente como lo son secrets
, lambda_code
, lambda_layer_code
.
Luego hacemos referencia a un recurso de AWS CodePipeline y creamos artefactos de salida para cada una de las etapas.
pipeline = codepipeline.Pipeline(
self, "CICD_Pipeline",
cross_account_keys=False,
)
cdk_source_output = codepipeline.Artifact()
lambda_source_output = codepipeline.Artifact()
cdk_build_output = codepipeline.Artifact()
lambda_build_output = codepipeline.Artifact()
lambda_layer_build_output = codepipeline.Artifact()
Siguiendo con el proceso, es necesario crear las acciones que se desarrollarán en cada etapa de nuestro pipeline, por lo que agregamos el siguiente código, en donde se hará la referencia a los repositorios en GitHub.
cdk_source_action = codepipeline_actions.GitHubSourceAction(
"""
Rest of Code // Resto del código
"""
)
lambda_source_action = codepipeline_actions.GitHubSourceAction(
"""
Rest of Code // Resto del código
"""
)
pipeline.add_stage(
stage_name="Fuente",
actions=[cdk_source_action, lambda_source_action]
)
Creamos el proyecto de construcción de CodeBuild y la acción de CodePipeline para la construcción:
cdk_build_project = codebuild.Project(self, "CdkBuildProject",
"""
Rest of Code // Resto del código
"""
)
cdk_build_action = codepipeline_actions.CodeBuildAction(
action_name="Construccion_CDK",
project=cdk_build_project,
input=cdk_source_output,
outputs=[cdk_build_output]
)
Posteriormente creamos el proyecto de construcción tanto para la función Lambda como para su capa y así mismo crearemos las acciones de CodePipeline:
lambda_build_project = codebuild.Project(self, "LambdaBuildProject",
"""
Rest of Code // Resto del código
"""
)
lambda_build_action = codepipeline_actions.CodeBuildAction(
action_name="Lambda_Build",
project=lambda_build_project,
input=lambda_source_output,
outputs=[lambda_build_output]
)
lambda_layer_build_project = codebuild.Project(self, "LambdaLayerBuildProject",
"""
Rest of Code // Resto del código
"""
)
lambda_layer_build_action = codepipeline_actions.CodeBuildAction(
action_name="Lambda_Layer_Build",
project=lambda_layer_build_project,
input=lambda_source_output,
outputs=[lambda_layer_build_output]
)
Como se puede notar en el anterior código, en el build_spec
del lambda_build_project
tenemos una llave llamada files
dentro del objeto artifacts
y cuyo valor es una lista. Allí es donde agregaremos los archivos que nuestra función lambda ha de usar. Mientras que el build_spec
del lambda_layer_build_project
ejecuta los comandos que nos permitirá la construcción de la capa para nuestra función lambda, la cual tendrá la librería de requests
.
Después agregamos la etapa y las acciones que estarán desarrollandose en la misma.
pipeline.add_stage(
stage_name="Construccion",
actions=[
cdk_build_action,
lambda_build_action,
lambda_layer_build_action
]
)
Para finalizar, con la infraestructura de nuestra pila de CICD, agregaremos una etapa de despliegue, en donde se hará la sobre escritura de los parámetros de los códigos de nuestra función lambda y de nuestra capa, dado que la ubicación en Amazon S3 de los artefactos y el nombre del objeto aun no son conocidos.
pipeline.add_stage(
stage_name="Despliegue",
actions=[
codepipeline_actions.CloudFormationCreateUpdateStackAction(
action_name="Lambda_CFN_Deploy",
template_path=cdk_build_output.at_path("infra/app.template.yaml"),
stack_name="ApplicationStackDeployed",
admin_permissions=True,
parameter_overrides={
**lambda_code.assign(
bucket_name=lambda_build_output.bucket_name,
object_key=lambda_build_output.object_key
),
**lambda_layer_code.assign(
bucket_name=lambda_layer_build_output.bucket_name,
object_key=lambda_layer_build_output.object_key
)
},
extra_inputs=[
lambda_build_output,
lambda_layer_build_output
]
),
]
)
El código completo de la infraestructura lo encontraremos en infra/infra/infra_stack.py
de nuestro repositorio.
Para finalizar, agregaremos las dependencias de nuestra pila de infraestructura en el archivo app.py
:
app = cdk.App()
application = AppStack(app, "app")
secret = SecretStack(app, "secret")
infra = InfraStack(app, "infra")
app.synth()
Ahora agreguemos al repositorio nuestro código y luego desplegamos.
git add .
git commit -am "Agregando codigo de las pilas, secretos, aplicacion e infra"
git push origin master
Para desplegar ingresamos a la terminal, dentro de la ruta app-infra/infra
ejecutamos el comando:
cdk deploy infra
Y se crearan los recursos mencionado previamente.
Si navegamos a la consola de AWS y buscamos AWS CodePipeline en la barra lateral canalizaciones veremos nuestro pipeline recientemente creado, y las etapas generadas:
Una vez finalizado el proceso de despliegue podemos navegar a Amazon API Gateway, hacemos clic en la api con el nombre Endpoint, luego hacemos clic en etapa en el menú lateral.
Hacemos clic en prod y posteriormente abrimos el enlace de Invocar URL en nuestro navegador.
Limpieza
Estos pasos son necesarios si se desea eliminar los recursos creados al realizar esta implementación de la siguiente manera:
Primero nos dirigimos a AWS CloudFormation, en Pilas seleccionamos ApplicationStackDeployed y luego hacemos clic en el botón Eliminar. Por último nos apoyamos en el comando de cdk destroy
para eliminar los recursos creados con el CDK Toolkit.
cdk destroy infra
Escribimos y
, luego
cdk destroy secret
Otra vez escribimos y
en nuestra terminal.
Conclusión
Como pudimos ver, el tener una implementación de integración continua y despliegue continuo (CICD) nos brinda capacidades que sin esta no tendríamos, una de ellas es la independencia de los equipos de desarrollo e infraestructura, ya qué el equipo de desarrollo con solo tener acceso al repositorio, podrán hacer cambios en su código, agregar nuevas funcionalidades o utilizar otras librerías, lo que brinda la capacidad de innovar y maximiza los tiempos de entrega de software. Por otro lado dentro de este ciclo de CICD se pueden agregar etapas de prueba tanto para el despliegue de la infraestructura de la aplicación, cómo de la funcionalidad del código a usarse.
Código
El código explicado en este blog se puede encontrar en el siguiente enlace: Despliegue de función Lambda y su capa con CDK
Anexos
[1] Infraestructura cómo código
Sobre el autor
Juan Miguel Bermúdez Mieles es arquitecto de soluciones en Amazon Web Services para Sector Público. Juan Miguel apoya a distintas entidades e instituciones públicas en Centro América y el Caribe en la adopción de nuevas tecnologías y prácticas que permitan el mejoramiento de sus servicios. |
Revisores técnicos
Hugo Dominguez es Arquitecto de Soluciones en Amazon Web Services (AWS), cuenta con amplia experiencia en diseño e implementación de soluciones cloud, Hugo ayuda a empresas latinoamericanas en la adopción estratégica de tecnologías, contribuyendo a la transformación digital del sector empresarial. |
Nicolas Bolaños es Arquitecto de Soluciones en Amazon Web Services para Sector Público. Nicolás trabaja para la vertical de salud con pagadores, prestadores del servicio, farmacias y entidades gubernamentales para la salud. Le apasiona el desarrollo de software, creación de soluciones SaaS, uso tecnologías serverless y aplicaciones en campos disruptivos como la IA generativa. |