Proyecto finalizado
Si desea avanzar y comprobar cómo se ve el proyecto finalizado, puede hacerlo a través del siguiente enlace:
Introducción
Descubrí el Cloud Resume Challenge de Forrest Brazeal por casualidad, a través de Twitter, y pensé que sería divertido dedicar un tiempo a completarlo, ya que me veía capacitado para ello. Veía ese reto como una oportunidad de obtener experiencia en la nube de AWS, así que me puse manos a la obra.
Para conseguir este reto se tiene que crear un portfolio personal cumpliendo una serie de requisitos, utilizando para ello varios servicios de AWS. Una condición para poder completar el desafío es tener una certificación en la nube de AWS. Como había conseguido una certificación unos mese atrás (AWS Cloud Practitioner), decidí seguir hacia delante. La cronología del proyecto fue la siguiente:
Arquitectura del website
Inicialmente se planificó la infraestructura que tendría el proyecto. El siguiente diagrama de arquitectura muestra los servicios en la nube que se usarán. Dicho diagrama enlaza al repositorio de Github del back-end del proyecto. Haga click en él para acceder al repositorio.
Esta arquitectura trabajará del siguiente modo:
Los usuarios solicitan la página web al navegador.
El navegador envía una solicitud a Route53, que resuelve el DNS para llegar a la ubicación de borde de AWS más cercana a la localización del usuario.
La distribución de CloudFront reenvía la solicitud al bucket de S3 que contiene los archivos del sitio web y recupera su contenido.
El bucket de S3 con el código HTML está protegido con Origin Access Identity (OAI) que impide el acceso directo al bucket.
El código javascript embebido en el HTML envía una solicitud GET a AWS API Gateway para recuperar el número de visitantes almacenados en la base de datos.
AWS API Gateway, al recibir la solicitud, dispara una función lambda.
La función lambda aumenta en una unidad el número de visitantes almacenados en una tabla de DynamoDB, devolviendo, a través de un objeto JSON, el número de visitantes actualizado.
El recuento de visitantes se muestra en el sitio web.
Un repositorio de git en Github proporciona control de versión de código y CI/CD a través de acciones de Github.
Serverless framework implementa la infraestructura de AWS (IaC).
Ahora "sólo" falta implementar todo esto y hacer que funcione. A por ello.
Front end: HTML, CSS y Javascript
El portfolio tiene que estar escrito en código HTML, dándole formato con una hoja de estilos CSS. Lo que se hizo fue fue crear un documento HTML desde cero, sin usar ninguna plantilla prediseñada. Es preferible crear una página simple que ayude a afianzar conceptos sobre diseño web que usar una plantilla, aunque el resultado sea menos vistoso. Para los estilos se utilizó el framework Bootstrap, además de CSS básico.
La página HTML también incluye un fragmento de JavaScript que actualiza y obtiene el recuento de visitantes desde el back-end. Esto se consigue utilizando el método fetch()
de la Fetch API , pasando como parámetro la URL de una API creada principalmente para obtener, de forma asíncrona, el número de visitantes del sitio web (se explicará más adelante cómo funciona esta API).
Amazon Static Website
El sitio web tiene que ser desplegado en un bucket de S3 de AWS. Para crear un sitio web estático en un bucket de Amazon S3, se fueron dando los siguientes pasos:
Se crea un bucket de S3: En la consola de Amazon S3, haciendo clic en el botón "Crear bucket". Se le da un nombre al bucket (tiene que ser único globalmente) y se selecciona la región donde se alojará (se dejó la que viene por defecto, Virginia del Norte, us-east-1).
Se habilita el alojamiento de sitios web: Una vez creado el bucket, seleccionando la pestaña "Propiedades" y habilitando la opción "Sitio web estático".
A continuación se procede a configurar la ruta de acceso al documento de índice ("index.html"), y la ruta de acceso al documento de error personalizado ("error.html", siendo éste opcional).
Una vez hecho esto, ya se pueden cargar los archivos del sitio web estático en el bucket de S3.
Se configuran los permisos de acceso. Se hace clic en la pestaña "Permisos" del bucket, asegurándose de que los permisos son los adecuados para que los usuarios puedan acceder al sitio web (más adelante se modificará esto para que sólo tenga acceso una distribución de CloudFront determinada).
Ya sólo faltaría configurar el DNS, aunque eso se hará más adelante, utilizando el servicio Route 53 de AWS.
Así se haría manualmente, más adelante se hará todo este proceso ejecutando unas simples líneas de código, con Infrastructure as Code (IaC).
CloudFront
Se usará una distribución de AWS CloudFront para que sólo se pueda acceder al contenido del bucket a través de dicha distribución. Para crearla se deben seguir los siguientes pasos:
Se accede, a través de la consola de AWS, al servicio CloudFront. Una vez allí se hace click en el botón de "Crear distribución".
Se define el origen de la distribución, en este caso el bucket de S3 con el contenido web creado anteriormente.
Se establecen las opciones de caché y de seguridad de la distribución. Se marca la opción de "Redirigir HTTP a HTTPs".
Se especifique el certificado SSL que desea utilizar. Se puede cargar un certificado SSL existente o crear uno nuevo utilizando el servicio AWS Certificate Manager.
Se guarda el DNS de la distribución de CloudFront.
Se vuelve a acceder al servicio de S3 a través de la consola. Se bloquea todo acceso público al bucket, creando además una política por la cual sólo se permite el acceso al bucket a la distribución de CloudFront.
DNS
El siguiente paso consiste en apuntar un nombre de dominio DNS personalizado a la distribución de CloudFront creada anteriormente. Para ello se decició comprar un nombre de dominio a través de Google Domains. Se trata de un servicio de alojamiento de dominios que permite registrar un dominio sin necesidad de un alojamiento web. En el momento de la compra costaba unos 12€ anuales. Para el hosting se decidió utilizar las Hosted Zones de AWS Route 53. El precio mensual que factura AWS por el alojamiento web es de 0,5€ + IVA.
Para configurar el dominio adquirido se siguieron los siguientes pasos:
Se accede, a través de la consola de AWS, al servicio Route 53.
Se crea una nueva hosted zone, si es que no se dispone de ninguna.
En la consola de Google Domains, se inicia sesión en la cuenta y se selecciona el dominio que se desea configurar.
En la página de configuración del dominio, se busca la sección "Custom resource records" o "Registro de recursos personalizados" y se hace clic en "Editar".
Se añade un registro de tipo NS (Nombre de Servidor o Name Server) y en las casillas de texto que aparecen, se escriben los nombres de servidor que aparecen en la sección "NS records" en la página de detalles de la zona hospedada (hosted zone) en AWS Route 53.
Se guardan los cambios y se espera a que la configuración se propague. Suele tardar poco tiempo, aunque te avisan de que puede tardar hasta 48 horas en propagarse completamente.
Una vez que se han completado estos pasos, ya se puede utilizar AWS Route 53 para administrar el dominio de Google Domains. Se pueden añadir registros de recursos DNS (Domain Name System) para el dominio, como registros A, CNAME, MX y otros, y utilizar las herramientas de AWS Route 53 para monitorear y administrar la zona hospedada.
Concretamente, se añadió un A Record que apuntara el dominio a la distribución de AWS Cloudfront, que a su vez apunta al bucket que contiene el sitio web. También se añadió un CNAME Record para apuntar al sitio web dónde se aloja esta entrada de blog que está actualmente leyendo.
DynamoDB
Para crear la tabla de DynamoDB que almacenará el número de visitantes se seguirán los siguientes pasos:
Se accede, a través de la consola de AWS, al servicio de DynamoDB. Una vez allí, se hace click en el botón de "Crear tabla" y se la da un nombre.
Se crea una clave de partición de tipo string, asignándola un nombre ("id", por ejemplo). No es necesaria ninguna clave de ordenación ni ningún tipo de índice, ni locales ni globales.
Se pincha en las opciones avanzadas y se selecciona "on demand" en el apartado de las características de lectura y/o escritura.
Las demás opciones se dejhan por defecto, haciendo click a continuación en el botón "Crear tabla".
Esta tabla almacenará un elemento compuesto por un "id" (clave de partición) y un atributo de tipo "number" que almacenará el número total de visitantes del sitio web. Este atributo es el que se incremetará en una unidad cuando alguien acceda al sitio web a través de un navegador. Sencillo, verdad?
Función lambda
Se creará una función lambda, con código Python, para extraer de la base de datos el registro que lleva la cuenta del número de visitantes del sitio web. Los pasos para crear esta función son los siguientes:
Se accede, a través de la consola de AWS, al servicio de Lambda. Una vez allí, se hace click en el botón de "Crear función".
Se da un nombre a la función, se selecciona Python 3.9 como lenguaje, se selecciona un Role (si no existe se crea en AWS IAM) al que se le permita el acceso a la tabla de DynamoDB creada anteriormente y se pincha en el botón inferior de "Crear función".
Ahora toca implementar el código de la función que permita recuperar el dato requerido de la tabla de DynamoDB.
El código sería el siguiente:
import boto3
client = boto3.client('dynamodb', region_name='us-east-1')
MY_DYNAMO_TABLE = 'counterTable'
def lambda_handler(event, context):
response = client.update_item(
TableName = MY_DYNAMO_TABLE,
Key = {
'id': {'S': 'my_website'}
},
UpdateExpression = 'ADD visitors :inc',
ExpressionAttributeValues = {':inc' : {'N': '1'}},
ReturnValues = 'UPDATED_NEW'
)
value = response['Attributes']['visitors']['N']
return {
'headers': {'Access-Control-Allow-Origin': '*'},
'statusCode': 200,
'body': {'visits':f'{value}'}
}
API Gateway
Es hora de crear una API que, al invocarla, dispare una función lambda que recupere el número de visitas a la web. Los pasos a seguir serán los siguientes:
Se accede, a través de la consola de AWS, al servicio de API Gateway. Una vez allí, se hace click en el botón de "Crear API" .
Como tipo de API se selecciona REST API. Se le da un nombre y se pincha en "Crear API".
Se crea un recursos y los métodos necesarios para la API (con uno de tipo GET es suficiente), a través del editor visual que provee AWS API Gateway.
Como punto de integración se escoje la función lambda creada anteriormente.
En la pestaña de acciones , se habilita el CORS si es necesario para que la API pueda ser consumida desde otros dominios.
Desde esa misma pestaña de acciones, se procede al deploy, para que la API esté disponible en internet. Se guarda el endpoint, que se colocará en el código HTML del sitio web.
¡Listo! Ya se dispone de una API de tipo REST en AWS API Gateway que dispara una función lambda.
Pruebas unitarias en Python
Como era una parte de las condiciones del desafío, se creo un test unitario escrito en Python para comprobar que la función lambda se comportaba de manera correcta, arrojando los resultados esperados.
Se decidió usar un test mock, que viene a ser un método que prueba una parte estructural del programa. El código del test (y el de todo el proyecto) se puede consultar en el repositorio del backend del proyecto en Github. (Code)
Infraestructura como código (IaC) - Serverless
Hasta ahora se ha explicado cómo crear cada uno de los servicios necesarios a través de la consola de AWS. Pero uno de los requisitos del desafío era crear todos los servicios haciendo uso de la llamada 'infraestructura como código' (IaC por sus iniciales). Así que hubo que ponerse manos a la obra y cumplir con este requerimiento.
CloudFormation es un servicio de AWS que permite aprovisionar todos los recursos de la infraestructura en la nube, utilizando un lenguaje común mediante plantillas JSON o YAML. Con CloudFormation se puede especificar cualquier nivel de detalle sobre los servicios de AWS y sus relaciones, siendo recomendado para diseñar aplicaciones en producción.
Se decidió usar Serverless framework, que permite crear y desplegar aplicaciones basadas en funciones. Es un framework multi proveedor, aunque suele usarse comúnmente en AWS. Permite definir recursos de AWS como lambda, IAM, buckets de S3, tablas de DynamoDB, distribuciones de CloudFront, etc...
Serverless framework depende de NodeJS para funcionar. Por ello, en primer lugar se debe instalar nodeJS y después se instala el framework (en Windows y Mac OS simplemente hay que descargarse el instalador desde: https://nodejs.org/es/download
Se parte de un fichero serverless.yml, en el que se define la aplicación, con todas las funciones, recursos y su configuración. Se tiene que incluir el código fuente y las dependencias que utilicen las funciones.
Serverless proporciona una herramienta CLI, que permite desplegar el código en S3 y crear la infraestructura de manera sencilla, simplemente con el comando:
serverless deploy
Por debajo, serverless convierte la aplicación en una plantilla de cloudformation y lo sube a la nube (S3) junto con el código, para desplegarlo.
El fichero de configuración de una aplicación serverless se compone de tres secciones:
service: Se define el nombre del servicio. Este nombre se usará como prefijo para el nombre de las funciones y para el stack de cloudformation.
provider: Se define la configuración común a toda la aplicación y las funciones, así como los permisos básicos.
functions: Se definen las funciones lambda a desplegar, identificando dónde se encuentra el código, qué eventos las disparan y su configuración particular. En este caso sólo se utilizará una función lambda.
Otras secciones utilizadas son:
custom: Se definen variables auxiliares que se usarán en el resto de la plantilla.
package: Se define la forma de empaquetar las funciones.
resources: Se definen otros recursos de AWS que se crearán en la aplicación. Esta sección usa el lenguaje de CloudFormation.
El fichero de configuración serverless.yml quedaría del siguiente modo:
# The name of the project
service: cloud-resume-challenge
frameworkVersion: '3'
custom:
siteName: lpastor-website-bucket
customDomain:
domainName: luispastor.dev
certificateName: 'luispastor.dev'
createRoute53Record: true
customCertificate:
certificateName: 'luispastor.dev'
hostedZoneName: 'luispastor.dev.' # don't forget the dot on the end - is required by Route53
hostedZoneId: Z2FDTNDATAQYW2 # For CloudFront distributions, the value is always Z2FDTNDATAQYW2
# Configuration for AWS
provider:
name: aws
endpointType: REGIONAL
region: ${opt:reginon, 'us-east-1'}
# This enables us to use the default stage definition, but override it from the command line
stage: ${opt:stage, 'dev'}
runtime: python3.9
iam:
role:
name: crc-${sls:stage}-role
statements:
- Effect: Allow
Action:
- 'dynamodb:UpdateItem'
Resource:
- "Fn::GetAtt": [myDynamoDBTable, Arn]
environment:
ENV_MYTABLE: counterTable
functions:
# Lambda function with API Gateway trigger
get-visitors:
handler: lambda_function/handler.lambda_handler
events:
- http:
path: /counter
method: get
cors: true
integration: lambda
resources:
- ${file(resources/resources.yml)}
- ${file(resources/outputs.yml)}
# Plugins for additional Serverless functionality
plugins:
- serverless-certificate-creator
CI/CD
Otro requisito era utilizar repositorios de GitHub para almacenar el código del front-end y del back-end y hacer uso de GitHub Actions para lograr una integración e implementación continuas (CI/CD).
Se podían haber utilizado servicios de AWS como CodeDeploy y CodePipeline para el CI/CD, pero se decidió escoger la vía de GitHub Actions, que nunca se había usado previamente, para aprender algo nuevo principalmente.
Los pasos que se dieron para crear Github Actions como CI/CD fueron los siguientes:
Se crean los repositorios de Github, tanto del front-end como del back-end.
Se define el flujo de trabajo, creando en la raiz de los repositorios la carpeta llamada .github/workflows. Dentro de este directorio se crea un archivo YAML para definir el flujo de trabajo de GitHub Actions. Se le llamó
main.yml
.En este archivo yml se comfiguró el flujo de trabajo. A continuación, se muestra el código del flujo de trabajo del front-end:
name: Website CI-CD
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Sync website bucket with AWS CLI
run: aws s3 sync ./ s3://$AWS_S3_BUCKET
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
- name: Invalidate CloudFront
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.DISTRIBUTION_ID }} --paths "/*"
- Importante. Se deben gestionar los secrets en los repositorios. En la página principal del repositorio de Github, en la pestaña de settings, se selecciona "Secrets" en las opciones de "Acciones" y se crea un "New repository secret", donde se agregan las claves secretas de acceso a AWS. Se las puede llamar AWS_SECRET_ACCESS_KEY, por ejemplo. Estos secrets son referenciados en el código yml mostrado anteriormente.
Una vez hecho esto, cada vez que se haga un commit, los cambios aparecerán
inmediatamente reflejados en el proyecto.
Entrada en el blog
Este último paso es, de largo, el más costoso. Se ha de reconocer que da mucha pereza ponerse a escribir los pasos seguidos a lo largo del proyecto. Ha pasado mucho tiempo desde que se finalizó el reto hasta que esta entrada ha sido escrita.
Pero bueno, era algo que había que hacer. Sólo queda agradecer el tiempo empleado a los que han llegado hasta aquí abajo leyendo.
Ha sido un reto muy interesante, aprender cosas nuevas siempre es recomendable. Como se suele decir en estos casos, el saber no ocupa lugar.
Saludos.
Luis Pastor.