Infrastructure as Code Challenge - Terraform with GitLab CI/CD
Published: 2023-08-31 15:53:13

Full Project link on Github: **_https://github.com/users/tenongene/projects/14_**

## Summary of Challenge and Plan

I recently came across an infrastructure as code challenge on Github called "DocNetwork's DevOps Challenge" and I decided to star it and give it a try. I am very passionate about furthering my skills and knowledge in all things cloud and devops, and this challenge seemed like a good one to test my skills.

A description of the challenge:

Challenge Link: https://github.com/docnetwork/Infra-as-code-challenge


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4mky59ggkztwqpo90ra.png)


PLANNING


-Design and code a Quote Generator single page Application with NodeJS and Express, that will fetch a
random quote from a free quote API, and also display a random inspirational image of nature also
fetched from a free photo collection API.

-Containerize the application with Docker, and pushing the image to the DockerHub registry.

-Write and code terraform manifest files for deployment on AWS via an AutoScaling Group of EC2
instances, configured with a load balancer.

-Build a CI/CD pipeline with GitLab, configuring a Webhook in DockerHub and writing a Lambda function
for updating the instances.

-Write and code terraform manifest files for deployment on AWS via an AutoScaling Group of Task
definitions in an ECS cluster, configured with a load balancer.

=================================

## Initial Quote Generator application design with NodeJS and Express

The challenge said it had provided an already containerized application. That did not seem to be available as it's an old repository.
I decided to build my own from scratch using NodeJS, Express and some free APIs.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg89ucxbz4ootzeq0iqt.png)

Wrote a single page quote generator application using NodeJs and the Express framework. The page is intended to generate random inspiring quotes from a free quote API called Zenquotes (https://zenquotes.io).
Along with the quote is displayed a random photograph of nature from a free photo collection API called Pexels (https://www.pexels.com).

A user will click on a "Generate" interactive button that will fetch a new picture along with a new quote with each call of the APIs.

Configured clicking of the "Generate" button to hit the "GET" route in express which will make sequential calls to the quote API, then the photo API, returning and resolving promises at each step to return the "picture", "quote" and "author" variables for injection into the page.

Masked the API key and Port Number using the dotenv module for security.

Made use of a random number function that generates a random number which I then attached as a variable to the API query string to generate a random quote/image.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2v5hhdzgpf45s8p5yk8a.png)


Used the EJS templating engine to pass in the variables "picture", "quote" and "author" from the API that would be received from the APIs.
I used simple styling from materialize css.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z53ndlxnbol87za1qdjf.png)

Initial pageview from localhost:7272 during development.

==============================

## 
Containerization of application with Docker

-I wrote the Dockerfile for building the application image. I used a slim node base image available in
docker hub.

-Copied the current directory to the image working directory which I named as "app".
Ran "npm install" so that all the application dependencies from the package.json file got included with
the image.

-Exposed port 7272 which is the application port as written in Express.

-The command was "node index.js" which will start the application in the container.

-Built the image by running "docker build -t tenongene/quotegen ." in the current directory.
Ran "docker push tenongene/quotegen" to push the image to docker hub.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9if8u7wg2c4jg0krutdq.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g3ou0o4ic4f05vatz6c2.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2tg2tlkeerdsesw7e29e.png)

Application image now in Docker Hub.


=========================

## Writing Terraform manifests for deployment on AWS


Wrote manifest files to deploy application on AWS via an autoscaling group of EC2 instances.

Configured the VPC using the Terraform provider VPC module.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kx9h1z2tk079g38h0vdv.png)


Entered security group rules for load balancer access, SSH and metrics export for possible observability.
Created 4 public subnets, 1 in each of 4 availability zones which will each host the application, for high availability.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dxt2b3u3v3i5qtel3oow.png)


Entered the autoscaling group launch templates as required by the module. As this is a simple exercise, did not go into much detail as to configuring launch configurations, scaling rules and instance lifecycles as may be required in production settings.
Specified the desired capacity to 4 with the intent on placing an instance in each subnet per availability zone.

Obtained an ami id number from the console to use in the manifest.

Registered the autoscaling group as targets to the load balancer target group by referencing the target group arn.

Entered a base64 encoded user data script which will bootstrap the EC2 instances on launch with instructions to install Docker onto the instance, login to the docker registry, run a docker container by pulling the image that was built and pushed to docker hub. Mapped the application port 7272 to the instance port.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w2g5lcr76lwanf2i3sis.png)


Defined the target groups for subsequent registering of autoscaling group.
Associated the VPC by it's id.
Configure an http listener for the load balancer, with ingress at port 80 with a backend target port of 7272 for the instances/application via the autoscaling group.

Initially had access to application from both the instances and load balancer as I had configured the security group to allow access to port 7272 from everywhere ("0.0.0.0/0"). However I later realized this did not satisfy the requirement of access only via the load balancer. Thus created a second security group resource unique to the load balancer. Configured the default security group in the ASG to accepting incoming traffic into port 7272 only from the load balancer security group.

Wrote an output file for important infrastructure resources identification.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt8912npge010e9voed1.png)



=============================

## 
Infrastructure deployment results

Ran Terraform Init, plan, and apply and infrastructure was deployed.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7e5st9ityf1qbp88d07n.png)

i)- The 4 EC2 instances launched.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l02v0d23m3nst8vtvp84.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwjdb55dab782l1s2lm3.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1mogrbaz4k5d782n5wjf.png)

ii)- The security groups (default and load balancer) and associations/rules.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ua88w4gctonbhauic7t1.png)

iii)-The deployed network load balancer



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zcgirc7r2ewil0u2lzpv.png)

iv)- The target group, showing one instance per availability zone in a subnet.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1vmd26pgwldkdord9yb5.png)

v)- The deployed autoscaling group.

-----------------------------

View from 4 different web browsers (Firefox, Opera, Edge, Chrome) when application is accessed via the load balancer.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3cbdb05x5vbnbv9anql2.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfbs0vuaz962nv91krte.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nsris89ku44j4jfsimko.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pc3c0n9ae04zrbrq7jyz.png)

----------------------------

Accessing each instance via it's public IP address at port 7272 does not reach the application, thus proof that application is only accessible via the load balancer.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5f2rmel55a9i1r8y5fc.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jmbfg7dt4ev5j3yfzs99.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2h2gtvkorp7b73nvifi.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j1o7jl6ahj3dzng4etsr.png)


===============================

## Building a CI/CD Pipeline in GitLab, with a DockerHub Webhook configured for Lambda Function

I migrated the application code to GitLab as my preferred platform for continuous integration and continuous deployment.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjotakdn5c54evvdqs78.png)

I configured environment variables in the GitLab pipeline with credentials to access the DockerHub registry for automatically pushing any image updates.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgvztepgnppwun566eo4.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gablfr1wabfjgporzwrr.png)

Wrote the .gitlab-ci.yaml file definition for the pipeline that will propagate changes in the application to the image upon a push to the code repository.
I used a Docker in Docker runner for the pipeline.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nl31gdqfxjmwjxtbez34.png)



Ran initial pipeline with a test change to the application code to verify successful build of application docker image.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4a2hqxynjq9t71ok7vhw.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pcakba1polrr85taaakp.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3e394sau554e4mec5zl.png)

-------------------------------


I wrote a lambda function in AWS named "quotegen-update" that will update the application image in the EC2 instances using the NodeJS SDK for AWS. The function URL will be used to configure a Webhook in docker hub. The Webhook will send an HTTP POST request to the function URL with each push of a new image to docker hub.

The idea is for the function to send instructions to terminate the currently running instances, at which point the Autoscaling group will fire up new instances to replace the terminated ones. However, based on the embedded user data script, the new instances will have the latest application image pulled from docker hub when creating the containers.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq75mswnf93cmv2ob8xm.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1sn06fq9d88pnsrkkjvv.png)


The lambda function handler makes use of the DescribeInstancesCommand and TerminateInstancesCommand
methods of the EC2 Client Module.

The DescribeInstancesCommand was used as a method to filter out the instances by the resource Tags of
Name and Instance Running Sate. The instance ID was not ideal because each new instance will have a
new instance ID and a new public IP address when fired up by the autoscaling group, so it wouldn't be
possible to code which instances to stop.
The resource Tags are the most efficient method since that is common to all instances created.

The result of the filtered instances provided the running instances, which I transformed into an array to be used in the TerminateInstancesCommand method.


Created function with function URL: _**https://aicu5rzgtfpqmwhaua4z7sijyi0snlsk.lambda-url.us-east-1.on.aws/
**_


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ujtvumx7i51rsl3lsb4o.png)



I configured a webhook for the application image repository in docker hub with the URL of the lambda function for which a POST request will be sent for image update.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ax9coukw9t9m4x7xj4ka.png)


===========================

## Demonstration of application CI/CD with Results

With the complete pipeline now in place from the GitLab code repository through to the instances in AWS,
I made a change to the application page by adding a greeting in the html page.
I then committed the changes and pushed them to the GitLab code repository.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k0eic1toqkml80s6g94d.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/guixgjbwlh10640fz1cv.png)


Push to the repository triggered the CI/CD pipeline which immediately began building the image.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fjcs5j6hh573ro6iglr.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qupc0zsntotghjqm13pz.png)



Successful completion of the pipeline resulted in a push of the new image to DockerHub, which then triggered the Webhook to send a post request to the lambda function URL, and thus invoking the lambda function.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udotuorjzqgj16k69ir1.png)


The lambda function executed by terminating the running EC2 instances.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p5jak02dlqej8osmne5a.png)


The Autoscaling group then began initializing new instances to replace the terminated ones, but the new instances will initialize by pulling the newly updated image.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v115s4i20yxsnftigwv4.png)



The updated application is now available when accessed via the load balancer as before.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2wz4shawbszry0e64w8.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6fzu390kfsdujemds5t.png)


============================

## Writing Terraform manifests for deployment on AWS via an Autoscaling group associated ECS Cluster

To deploy the application via ECS, I had to modify the autoscaling group to be a capacity provider for a new ECS cluster and write new resources for the creation of a cluster, a service and a task definition to run the containers.

The load balancer and target groups had to be modified as well.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q8wc6k5evoxu8u5qvc2f.png)



I decided to go with an application load balancer this time around, since I garnered several insights from the first phase of the project. I changed the application port in the application to serve at port 80, and added a health check route at "/health" in the application, and that is how the load balancer would report instances as healthy.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmz6ohycb76segyac9ry.png)


Created a launch template resource with the required EC2 configuration. The ami used thi time was an ECS optimized Amazon Linux ami.
Saved the cluster name to the environment variable and inserted into the ECS agent config file.
Created and IAM instance profile for the instances for authorization.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1klycmjqy9opw8fbkdut.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0bwnwr4xg7a6l2m8ahg.png)


Created the cluster, capacity provider and attached the capacity provider to the cluster.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf4sf3vqxiu2sr4ptc1y.png)


Wrote the service definition file, referencing the capacity provider strategy for autoscaling, the load balancer, the task definition (below) to use and I attached an IAM role that I created on the console to be used by the service load balancer.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a5pzjvu2hrcv0yee5gvx.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9303ugrkr8izxm8v3b3.png)


The task definition for the container parameters with the container name, the image to pull form docker hub, the port mapping, and attached the execution role created on the console for authorization.
The task is to use the "bridge" network mode.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/149htjfei50fxy91icmc.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45jdbziua4t10x8f76ko.png)


I created the lambda function for the CI/CD process, whose URL will be used as a Webhook in docker hub.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vplby3r559s7lfd1v3p.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zplkrt49bhds0wgz8wrw.png)


The idea is to use the "Update Service Command" of the ECS client Nodejs SDK. It has a method called "ForceNewDeployment", and when set to "true" and executes on an event, it simply instructs the service to create a new deployment. The service will then create new deployment of the same quantity, but in a ROLLING UPDATE strategy where one container at a time is terminated then updated. This prevents downtime for the service and creates a smoother transition to any image updates. The new containers will then have to pull the newest image in docker hub.




![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6rpbmidey6em6rxgf0ou.png)


The function URL was:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxpw65634eddqirhtk6w.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eveo8jlz3a4m2edxlwyt.png)

=============================

## 
Infrastructure deployment Results with ECS autoscaling


The deployed ECS Cluster


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpeqzc3burr41n343l52.png)



The service



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/07dqyts8o572tely7gsc.png)


Cluster infrastructure with containers in each availability zone


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/had9tn5ikzursaautmgd.png)

Load balancer and target group


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d3ojqb69ovinf4fa7uwv.png)

The autoscaling group


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1dqk0lbea4yvpx4nekkz.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3vujqp57qvivpp4ss4s9.png)

----------------------

Accessing application


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcbmh4zoy935fjlpzayc.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jz8kz2wt3hpnhjgoflo3.png)


The CI/CD process and making a change to the application.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/otn8r1yk07du468qxkfx.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vvi35i3uo593um8t28r.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6k14mu6cjoqcken7wap.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6hdy6vng0q9jpmqschw.png)



Updated image in DockerHub


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4nzqwsrwqntsl668ly79.png)


Cloudwatch logs showing function invocation by the webhook


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1poth6198v2mdb1cy1n6.png)


Rolling update of the service in the cluster with new containers being spun up in turn, over about 15 minutes.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rwjkapwmxflxe1bqd0m5.png)


---------------------

Updated application is now accessible with the newly updated image



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1250my0x27s9atqfhq99.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87rbs3v8y78fq5t9drls.png)


## Project challenges, insights and lessons learned

The with the initial phase of the project with EC2 instances, my user data script was not bootstrapping the EC2 instances with docker in order to pull images and then run the container:
Further research pointed out that there should be no indentation to the right of the user data script, in the terraform manifest file. When I took out the indentation, the EC2 instances then bootstrapped correctly with docker running.
I also learned, only after completing the 2nd phase of the project with ECS that I also had the option of using an Amazon Linux ECS optimized AMI, which already comes with docker pre-installed.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bomlg023vdojnazoadq7.png)

Indentations in user data script


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9h03t1mj8mppy673d00h.png)

Do not indent.

------------------------------
I initially could not reach the application although everything seemed to have been configured correctly.
My debugging method was to access the instances via SSH and check the docker logs. I noticed that docker could not load the express module in the application during the image pull step. That means I forgot to run "NPM install" to install the dependencies from the package.json during image the building step. I added this, and rebuilt the image and then the image pulled correctly.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iu91pv3sb2stea8quhkn.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xup86zjix0ew66yg35zh.png)


----------------------------

During the ECS deployment phase of the project, at first, although things seemed to be configured correctly, the tasks could not start and none of the containers could be deployed for some reason. I couldn't find any issues and the EC2 instances where running and passing system checks.

Upon further research, I learned I could check the logs of the ECS agent at /var/log/ecs/ecs-agent.log to get a clue of what could be wrong. I checked the logs and saw that the ECS agent did not have the proper credentials and permissions to pull images. My understanding was that an instance profile was required to do so. So I created an instance profile and attached to the ASG definition and that got resolved.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/maiab8650496rykomq1e.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlqswm6z6m71qit29ms1.png)

------------------------
After that resolution the new issue was an error that the containers could not be registered because the cluster was inactive, although I could clearly see on the console that the cluster was active.
Further research with an answer from stack overflow suggested that the ECS agent only creates a "default" cluster, unless the cluster is specifically added as an environment variable (ECS_CLUSTER) and added to the ECS agent's configuration. I added it via a user data script and everything got resolved.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8weu8gs8a0dwpv3bny3.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/etiifkceyjrlue2uq7kv.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/85r5u5td2nxd9gs6pn3x.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxv6iybk44kqan2qjzmu.png)