ICM / how-to-do / Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate. Parte 2.

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate. Parte 2.

30 septiembre 2021 | Carlos Calvo

En anteriores entradas os comentábamos que presentaríamos una guía de cómo realizar el despliegue de una aplicación desarrollada en ASP.NET MVC 5 en un clúster de AWS Fargate mediante el uso de Terraform. En esa publicación, nos adentramos en explicar cómo preparar el entorno para tal ejecución. Tanto la suscripción de AWS, la configuración de nuestro entorno para poder ejecutar Terraform con nuestras credenciales, como la configuración del entorno de Terraform, la disponibilidad de nuestro repositorio Git y la primera subida de código al repositorio de Git.

En este segundo capítulo, vamos a adentrarnos en cómo preparar la infraestructura de AWS para que en el futuro podamos desplegar nuestro proyecto a través de una Pipeline CI/CD de AWS.

Preparando la infraestructura

Los primeros pasos que ejecutar son darnos dar de alta y configurar una serie de servicios que necesitaremos porque los mecanismos de CI/CD se apoyan en ellos. A continuación, explicaremos cada uno de los componentes que debemos generar para que las Pipelines funcionen correctamente. Empezaremos por los componentes más básicos y seguiremos por los componentes más complejos y con más dependencias sobre servicios más simples.

Antes de empezar con la definición de los objetos, queremos aclarar que no vamos a entrar en la definición de las políticas de permisos de AWS. Se trata de ficheros en formato .json que definen los permisos que cada servicio tiene de cara al resto de servicios.

Por ejemplo, si queremos que un servicio concreto guarde un fichero en un bucket S3, debemos otorgar permisos al servicio concreto para que pueda realizar esta acción. De lo contrario, no será capaz de guardar el fichero cuando lo necesite. Si profundizamos en los permisos el artículo será interminable y la intención es que sea introductorio, por lo que si tiene cualquier duda con permisos en AWS no dude en consultarnos.

S3

Lo primero que vamos a crear es una cuenta de almacenamiento S3. La usaremos en varios puntos, así que será el primer elemento que crear en nuestra infraestructura de AWS. Para establecer una cuenta de almacenamiento S3 en AWS con Terraform, podemos hacerlo de la siguiente manera:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Recuerda que para dar acceso al resto de servicios al bucket S3, deberás configurar los permisos de dichos servicios. Puede consultar la ayuda oficial acerca de permisos S3 en este enlace.

Cloudwatch

Daremos de alta una cuenta del servicio CloudWatch con el fin de tener un punto único en el que consultar los logs que genere tanto nuestra aplicación como la infraestructura. Para hacerlo, es tan sencillo como dar de alta el servicio con este código Terraform:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Networking

El siguiente paso con el que lidiaremos será la creación de la red que soportará todo el montaje. Para ello, necesitamos algunos objetos: 1 VPC, 1 tabla de enrutamiento, un Internet Gateway, tantas subredes como necesitemos (una en este tutorial) y un Security Group. Crearemos todos los elementos con configuraciones básicas y empezaremos por el tipo de recurso principal, la VPC.

VPC

Para configurar una VPC, el elemento de red de nivel inferior sobre el que se apoyan el resto de elementos de red, necesitamos definir un sencillo bloque de código en Terraform que establezca la configuración básica. Estableceremos un bloque CIDR por el que pasará todo el tráfico de nuestra plataforma, le indicamos que queremos resolución de nombres y le asignamos un nombre al recurso:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Internet Gateway

Ahora creamos un Internet Gateway que de acceso desde nuestra infraestructura o VPC al exterior, lo relacionamos con la VPC y le asignamos un nombre:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Tabla de Enrutamiento

Creamos una tabla de enrutamiento dentro de la VPC para indicarle a esta por dónde debe ir para salir a internet. Lo hacemos del siguiente modo:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Subredes

Ahora debemos generar una subred dentro de nuestra VPC. Podríamos usar más para crear servicios que fuesen tolerantes a la caída de una de las subredes, pero en este laboratorio formaremos una única subred que contendrá todo el tráfico:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Tablas de enrutamiento

Como hemos creado una tabla de enrutamiento y una subred sobre nuestra VPC, debemos indicarle a AWS la relación que hay entre estos dos objetos. Lo haremos con un bloque de código Terraform como el siguiente, en el que indicaremos la relación entre subred y tabla de enrutamiento:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Security Group

Y para finalizar crearemos un Security Group. Este elemento es muy importante porque es el que marcará qué elementos de nuestra infraestructura serán visibles o accesibles desde el exterior y qué elementos del interior de nuestra infraestructura tendrán visibilidad hacia el exterior. Se trata de un conjunto de reglas, como si de un firewall se tratase, que marcan las reglas de acceso o ACL desde/hacia nuestra VPC. Para nuestro proyecto de laboratorio generaremos un Security Group sencillo como este:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Como podemos ver, primero de todo asociamos el Security Group con nuestra VPC y posteriormente creamos 2 reglas de entrada; una para HTTP y otra para HTTPS y una regla de salida en la que permitimos todo el tráfico saliente. Esta configuración será la mínima necesaria para posteriormente poder acceder desde Internet a nuestro proyecto Web y también permitir que este pueda acceder a cualquier recurso externo.

Aplicar los cambios

Ahora que ya tenemos los ficheros de Terraform referentes al networking escritos, podemos aplicarlos para que Terraform genere la infraestructura que hemos definido en los ficheros. Nuevamente, lo haremos así:

RDS

Es posible que nuestro proyecto necesite acceder a datos persistentes en un motor de base de datos como MySQL (o cualquier otro). Nuestro proyecto de demo no accederá a base de datos, pero aún así consideramos que sería interesante explicar cómo lo haríamos. Puedes consultar aquí la lista de gestores de bases de datos que podemos configurar bajo un servicio RDS de AWS.

Como se trata de un punto meramente didáctico vamos a utilizar la configuración más sencilla posible para desplegar un MySQL. Para crear una instancia RDS/MySQL, debemos definir un bloque de código en Terraform como el siguiente:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

En este bloque estamos definiendo varias características del servicio como el tamaño de disco asignado a la instancia, el motor de base de datos a utilizar, que en nuestro caso será MySQL, la versión del motor y el tamaño de instancia que ejecutará nuestra instancia de base de datos.

Además, asignamos un usuario y password de administración de la instancia, le asignamos la subred en la que residirá el servicio y un periodo de retención y una ventana de actuación para realizar los backups de la instancia. Además, asociamos la instancia de base de datos con el security group.

Balanceadores de carga

Llegados a este punto, necesitamos crear balanceadores de carga, dispositivos que se encargan de recibir tráfico desde un extremo, generalmente Internet, para después hacérselo llegar a otro elemento llamado Target.

Los targets son los extremos de la conexión que en último término reciben una petición de conexión, como por ejemplo nuestra aplicación. En nuestro laboratorio, las peticiones para cargar nuestra aplicación llegarán desde Internet, pasarán por los balanceadores de carga para finalmente ser redirigidas al clúster de Fargate en el que residirá nuestra aplicación.

Para conseguir este funcionamiento, debemos generar un balanceador de carga, uno o varios listeners que atenderán las peticiones entrantes y un grupo de objetivos o targets que serán los receptores de la petición.

Application Load Balancer

Para empezar, crearemos un balanceador de carga, utilizando el siguiente bloque de código en el que definiremos las características principales del servicio:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Como podemos ver, estamos ejecutando un balanceador de carga de tipo aplicación que, además, está asociado con nuestro Security Group y nuestra subred. Además, habilitamos el log de acceso sobre el bucket S3 que creamos previamente. Con esto conseguimos que cada petición que pase por los balanceadores quede registrada en un log que podremos consultar desde la consola de AWS dentro del bucket S3.

Listener

El balanceador de carga por sí sólo no haría nada si no le insertamos algún Listener. Un elemento que se queda a la escucha en un puerto y que se encarga de recibir dichas peticiones a través de un puerto o protocolo definido.

En nuestro caso crearemos dos listeners ya que, como recordaréis, en la definición de nuestro Security Group vimos que aceptaríamos peticiones HTTP por los puertos 80 y 443.

Las peticiones que entren por el puerto 80 serán redirigidas automáticamente al puerto 443 ya que, aunque escuchamos por ambos puertos, sólo queremos ofrecer el contenido de nuestra Web a través del puerto seguro 443. Para lograr este comportamiento, utilizaremos bloques de código Terraform como los siguientes:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

En este bloque de código creamos un listener que escuchará en el puerto 80 y que sencillamente cogerá la petición y la redirigirá al puerto 443 del mismo balanceador de carga.

Ahora creamos el listener sobre el puerto 443 con el siguiente bloque de código:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Este listener sobre el puerto 443 está configurado para utilizar un certificado digital para encriptar el tráfico. Para relacionar el listener con el certificado necesitamos el ARN del certificado en AWS y sustituiremos «arn:aws:acm:zone:account:cert« por el ARN real de nuestro certificado. Además, definimos una default_action, que será lo que se le retornará al cliente si el listener no sabe a qué target group enviar la petición.

Target Groups

Como comentábamos anteriormente, los target groups son los destinos de las peticiones. Se les envía tráfico bajo ciertas premisas, por lo que, si no se cumple ninguna, se retornará al cliente la acción definida en el bloque default_action que vimos anteriormente. Para crear un target group utilizaremos un bloque de código para definir el target y otro bloque para definir bajo qué condiciones queremos enviar el tráfico a ese target group.

Empezamos definiendo el target group del siguiente modo:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Como podemos ver, le estamos asignando un nombre y lo estamos relacionando con nuestra VPC. Además, le especificamos el protocolo con el que se debe conectar el listener al target group -HTTP- y el puerto de destino. Esto es importante porque más adelante deberemos asegurarnos de que nuestra aplicación está escuchando por ese puerto. De lo contrario, el balancer no será capaz de enviar tráfico al target group al darlo como caído.

Asimismo, definimos un health check. Esto es un mecanismo para que el balancer pueda determinar si el extremo remoto está caído o no. No es muy útil en nuestro escenario. No obstante, en escenarios en los que hay múltiples targets groups, definir health checks puede ser muy útil para descartar backends caídos, enviando tráfico sólo a aquellos que estén respondiendo de la manera esperada y evitar así conexiones muertas contra un backend que no va a responder. El health check se hace sobre una URI definida en path y con unos umbrales y respuestas esperadas definidas en el propio bloque. Puedes consultar la documentación oficial sobre los Target Groups aquí.

Ahora que tenemos el Target Group determinado, nos quedará definir las reglas que indicarán a qué backends y bajo qué condiciones se debe enviar una petición. Para definir la regla, lo haremos con un bloque de código Terraform como el siguiente:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Estamos definiendo una regla que asociamos al listener que creamos y vinculamos en su momento a nuestro balanceador de carga. Le concretamos la acción que debe cumplir, que es enviar el tráfico al Target Group y la condición bajo la cual ha de hacerlo, que es que el Host Header de la petición sea igual a “terra-lab.icm.es”. Si esa condición no se cumple, se ejecutará la default_action que definimos previamente en el listener asociado a nuestro balanceador de carga.

Aplicar los cambios

Ahora que ya tenemos los ficheros de Terraform referentes al balanceador de carga escritos, podemos aplicarlos para que Terraform genere la infraestructura que hemos definido en los ficheros. Nuevamente, lo haremos así:

Clúster Fargate

Llegados a este punto, tendremos definido todo el circuito de una petición desde el cliente hasta los Target Groups del balanceador de carga, pero nos falta colocar un servicio en ese extremo para hacer algo con las peticiones. Ese servicio es el clúster de Fargate, que se encargará de ejecutar nuestra aplicación contenida en un contenedor de Docker y que Fargate cargará y ejecutará por nosotros.

Para definir el clúster necesitaremos básicamente 4 componentes; un Container Registry que guarde nuestra imagen tras compilar nuestro código, un clúster ECS, un servicio ECS asociado con el clúster y una definición de tarea que le indique al servicio del clúster con qué capacidades o propiedades ha de levantar el clúster.

Container Registry

Empezaremos con el componente más sencillo, que es el Container Registry. Se trata de un almacén donde guardaremos cada versión compilada de nuestro contenedor con la imagen de nuestra aplicación. Para hacerlo, sencillamente definiremos un bloque de código como el siguiente:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Simplemente estamos definiendo un nombre para nuestro Registry, definimos la propiedad image_tag_mutability como “MUTABLE, lo cual quiere decir que los tags de una imagen pueden ser alterados con la inclusión de nuevas imágenes.

Además, asignamos la propiedad image_scanning_configuration.scan_on_push” a “true” para indicar a AWS que debe escanear nuestra imagen en busca de vulnerabilidades. Cuando publiquemos una imagen, AWS las escaneará y nos mostrará un informe como este, si encuentra alguna vulnerabilidad:

ECS Clúster

Tenemos listo el lugar en el que guardaremos nuestras imágenes, pero ahora nos queda dar de alta los servicios que leerán esas imágenes y las ejecutará en un clúster de Fargate. Lo primero que debemos hacer es dar de alta el clúster, mediante un sencillo código como el siguiente:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

ECS Task Definition

Ahora que hemos creado el clúster de tipo Fargate, debemos generar una definición de tarea para que el clúster sepa con qué parámetros y características ha de ejecutar nuestra imagen. Para esto necesitamos crear un bloque de código en el que definiremos las características del servicio de ejecución de contenedores del siguiente modo:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Configuramos el nombre de la definición de tarea, el tipo de tarea que será Fargate en nuestro laboratorio, el modo de la red que debe ser “awsvpc” para usar las capacidades de nuestra VPC, una cantidad de CPU y de RAM y los permisos que la tarea tendrá sobre el resto de los componentes de AWS. Puedes leer la documentación oficial sobre definición de tareas aquí  para entender cómo configurar todos estos componentes.

Además, tenemos un json con la definición de la imagen a desplegar y de sus características. En nuestro laboratorio la definición en Json de la tarea especifica el nombre de la aplicación y de la imagen a cargar, la CPU y la RAM, la configuración de networking, la configuración del registro de logs y algunas características más. No olvides consultar el enlace de documentación anterior para conocer todas las posibilidades que ofrece la configuración de una definición de tarea.

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Como dato a destacar, hay que decir que el valor de la propiedad “entryPoint” debe coincidir con el ENTRYPOINT definido en Dockerfile; de otro modo el arranque de nuestra aplicación fallará. Para el resto de los parámetros, recuerda consultar el enlace oficial con la documentación.

ECS Service

Para terminar de configurar nuestro clúster sólo nos falta definir el servicio que correrá en el clúster de Fargate con las condiciones definidas por la task definition. Lo haremos con el siguiente código:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Hay varios puntos a comentar en este fragmento de código.

Para empezar, vemos que relacionamos el servicio con el clúster. Especificamos la propiedad “desired_count” para decirle al clúster cuántas instancias de nuestra imagen debe levantar. Le especificamos que se trata de ejecución bajo entorno Fargate y que el tipo de deployment será ECS. Además, configuramos la red del servicio asociándolo con nuestro security group y nuestra subred y también, le pedimos a AWS que cuando levante el servicio le asigne una IP pública. Por último, relacionamos el servicio con el balanceador de carga. De este modo, el servicio ECS es capaz de saber desde qué Target Group le deben llegar las peticiones.

Puedes consultar la documentación sobre la definición de servicios ECS en el siguiente enlace.

Aplicar los cambios

Ahora que ya tenemos los ficheros de Terraform referentes al clúster ECS escritos, podemos aplicarlos para que Terraform genere la infraestructura que hemos definido en los ficheros. Nuevamente, lo haremos así:

DNS

Una vez configurados los servicios básicos como S3 o Cloudwatch, que hemos configurado la red, una instancia RDS, los balanceadores de carga y el clúster ECS con ejecución sobre Fargate, sólo nos falta hacer que todo eso sea accesible desde el exterior.

Lo haremos dando de alta una zona DNS con los registros adecuados para que el host que nos interese resuelva la IP final del Target Group o aplicación que corre tras el Target Group. Lo primero que debemos hacer es crear el registro de nuestro dominio. Lo haremos así:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Con este bloque de código daremos de alta nuestra zona, pero ahora nos falta establecer un registro que apunte a nuestra aplicación ubicada tras los balanceadores de carga:

Desplegar una aplicación .Net con Pipeline CI/CD en AWS ECS y Fargate.

Generamos el registro “terra-lab.icm.es”, con CNAME como tipo de registro y apuntando al nombre de host que tenga en ese momento nuestro balanceador de carga. Así conseguiremos que una petición a dicho host acabe entrando desde Internet a través de nuestra VPC hacia el balanceador, después hacia el Target Group concreto del balanceador y luego hacia nuestra aplicación desplegada en el clúster de Fargate.

Aplicar los cambios

Ahora que ya tenemos los ficheros de Terraform referentes al DNS escritos, podemos aplicarlos para que Terraform genere la infraestructura que hemos definido en los ficheros. Nuevamente, lo haremos así:

Siguientes pasos

En la siguiente entrega del artículo veremos cómo configurar y desplegar infraestructura en AWS con Terraform para que actúe exactamente como esperamos cuando un desarrollador haga un push de código. Y definiremos con Terraform cómo coger ese código subido y compilarlo, publicar la imagen resultante y cómo publicar la aplicación en un clúster de AWS Fargate.