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:

stack-aplicacion-infraestructura

Pilas de la infraestructura y aplicació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

arquitectura-de-referencia-cicd-aws-github-codepipeline-lambda

Arquitectura de referencia: Infreastructura de CICD y Aplicació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.
paso-1-seguridad-github

Paso 1: Ingreso a configuración de desarrollador

  • Desplegamos la pestaña de Personal access tokens, y hacemos clic en Tokens (classic)
paso-2-github-personal-access-tokens

Paso 2: Selección de token de acceso personal

  • Hacemos clic en Generate new token (classic)
paso-3-seleccion-de-scope-de-token

Paso 3: Selección de alcance de token para los repositorios

  • Le damos un nombre al token en el campo Note
  • Activamos los permisos de admin:repo_hook
paso-4-eleccion-permisos-token

Paso 4: Selección de permisos

  • 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:

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.

secreto-secrets-manager-aws

1. Secreto creado en la consola

Hacemos clic en el nombre del secreto, luego nos dirigimos al recuadro Valor del secreto hacemos clic en Recuperar valor del secreto.

recuperar-valor-secreto

2. Recuperar el valor del secreto

Por último hacemos clic en el botón Editar y pegamos el valor del token generado en Github y guardamos.

editar-valor-secreto-aws-secrets-manager

3. Reemplazar el valor del secreto en AWS Secrets Manager

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:

pipeline-creado-aws-codepipeline

Pipeline creado

ejecucion-de-pipeline-aws-codepipeline

Ejecución de Pipeline

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.

apigateway-interfaz

ApiGateway Interfaz

Hacemos clic en prod y posteriormente abrimos el enlace de Invocar URL en nuestro navegador.

aplicacion-ejecutada-apigateway

API Accedida por url de Api Gateway

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.