# The cloud resume challenge - AWS

---

### Proyecto finalizado

Si desea avanzar y comprobar cómo se ve el proyecto finalizado, puede hacerlo a través del siguiente enlace:

* [luispastor.dev](https://luispastor.dev)
    

### Introducción

Descubrí el [*Cloud Resume Challenge*](https://cloudresumechallenge.dev/) 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](https://www.credly.com/badges/981b3ed3-adfd-4e47-a7a9-0eb5f0be20ff/public_url)), 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.

[![](https://cdn.hashnode.com/res/hashnode/image/upload/v1682867312969/6169d8e9-21e3-4318-8bd6-e851c603f919.png align="center")](https://github.com/lpastor75/cloud-resume-challenge-backend)

Esta arquitectura trabajará del siguiente modo:

1. Los usuarios solicitan la página web al navegador.
    
2. 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.
    
3. La distribución de *CloudFront* reenvía la solicitud al *bucket* de *S3* que contiene los archivos del sitio web y recupera su contenido.
    
4. El *bucket* de *S3* con el código *HTML* está protegido con *Origin Access Identity* (*OAI*) que impide el acceso directo al *bucket*.
    
5. 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.
    
6. *AWS API Gateway*, al recibir la solicitud, dispara una función *lambda*.
    
7. 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.
    
8. El recuento de visitantes se muestra en el sitio web.
    
9. Un repositorio de *git* en *Github* proporciona control de versión de código y *CI/CD* a través de acciones de *Github*.
    
10. *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*".
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680746989511/48bc8e05-7010-4763-bfc3-4c9ce11658a3.png align="center")
    
* 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:

```python
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](https://github.com/lpastor75/cloud-resume-challenge-backend/blob/main/test/test_handler.py))

### 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](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:

```yaml
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:

```yaml
# 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*:
    

```yaml
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.
