:::success # LS Lab 3 - Infrastructure as Code (IaC) ::: ## Task 1 - IaC Theory :::info Briefly answer for the following questions what is and for what: ::: 1. <ins> git repository:</ins> * **`/.git`** - contains all the information that the project needs in version control and all the information about commits; * **`/.github`** - cloud-based hosting service that lets you manage Git repositories; * **`.gitignore`** - tracks ignored files, is edited manually, contains templates that are mapped to file names in the repository to determine whether to ignore these files; * **`.gitmodules`** - this is a configuration file that stores the correspondence between the URL of the project and the local one under the directory to which you downloaded it. If you have multiple submodules, then you will have multiple entries in this file; 1. <ins>ansible directory:</ins> * **`ansible.cfg`** - the configuration file in the current directory contains various parameters, for example, on behalf of whom to run Ansible and the location of the inventory file; * **`inventory folder`** - the inventory directory will be the one where the used inventory file is stored; * **`roles folder`** - /etc/ansible/roles :) it's just another way to organize files, tasks, templates, variables and handlers that together serve a specific purpose, for example, to configure a service; * **`tasks`** - the smallest unit of action you can automate using an Ansible playbook; * **`defaults`** - default values, they will be used if these variables are not defined anywhere else for the host in question; * **`files`** - contains files that will be copied to the configured hosts; also - may contain scripts that will be run on the hosts later; * **`handlers`** - these are tasks that run only when you receive a notification that changes are being made on the machine; * **`templates`** - templates of files with variables; * **`playbooks folder`** - all hosts are stored in a local Ansible inventory file called /etc/ansible/hosts. Playbook is a script file that describes a set of actions for one or a group of hosts, the syntax of Yaml. 1. <ins>terraform folder:</ins> * **`main.tf`** - main configuration file where define resource definition; * **`variables.tf`** - file to define variables; * **`outputs.tf`** - file contains output definitions for resources; ## Task 2 - Choose your application/microservices :::info *Base level:* find (much better to write) a simple application with REST interface. For example, it could be a web server that returns "Hello to SNE family! Time is: < current time in Innopolis >". Use whatever programming language that you want (python, golang, C#, java...). ::: I created the simplest Java web server, consisting of several source directories and files. ``` #the project will be contained in the hr_app directory mkdir hr_app && cd hr_app/ #the entry point of the application touch index.js #adding code to that file and the web-server.js(config and services) mkdir config && touch config/web-server.js mkdir controllers && mkdir db_apis && mkdir services touch services/web-server.js ``` <center> ![](https://i.imgur.com/BUiLFyD.png) Figure 1 - File structure </center> This is a simple JavaScript module that provides a single property named `port`. In Node.js the process object has an `env` property that contains the user environment. I use this to set the port value to the value of the `HTTP_PORT` environment variable. If this environment variable is not defined, the default value will be 3000. <center> ![](https://i.imgur.com/t9iNsX7.png) Figure 2 - config/web-server.js </center> Web server module **services/web-server.js**: ``` //several modules are required in. The HTTP module is //included with Node.js but the express module and moment //will need to be installed via npm. const http = require('http'); const express = require('express'); const webServerConfig = require('../config/web-server.js'); const { DateTime } = require("luxon"); var local = DateTime.local(); let httpServer; //initialize immediately returns a promise which is //resolved or rejected depending on whether the web server //is started successfully function initialize() { return new Promise((resolve, reject) => { //A new Express application is created (which is really just //a function) and then used to create an HTTP server via the //HTTP module const app = express(); httpServer = http.createServer(app); //the get method of the application is used to add a //handler for GET requests coming from the root (/) path app.get('/', (req, res) => { //The callback function (also called the intermediate //function) will be called when such a request is received //and will use the "response" (res) parameter to send the //"Hello to SNE family!" message. response to the client res.end('\n' + 'Hello to SNE family! Time is: ' + local.toLocaleString(DateTime.TIME_24_WITH_SHORT_OFFSET) + '\n'); }); //The server's listen method is used to bind to the //specified port and start listening for incoming requests httpServer.listen(webServerConfig.port, err => { if (err) { reject(err); return; } console.log(`Web server listening on localhost:${webServerConfig.port}`); resolve(); }); }); } //The initialize function is exported from the module so it //can be invoked externally module.exports.initialize = initialize; ``` The main module introduces a web server module, and then defines and calls an asynchronous function named `startup`. <center> ![](https://i.imgur.com/09fx1Mq.png) Figure 3 - index.js </center> Since the initialize function of the web server module returns a promise, I can use it with `async/await` and wrap it in a `try-catch block`. If the `initialize` function succeeds, the web server will be started; otherwise, any exceptions will be caught and handled. Now I need to initialize the tpm and install Express and Luxon: ``` npm init -y npm install express -s npm install --save luxon #for running app node . ``` <center> ![](https://i.imgur.com/T2YbUQI.png) ![](https://i.imgur.com/4chjQ9D.png) Figure 4 - Result </center> ## Task 3 - Dockerize your application :::info 1. Build Docker image for your application (make Dockerfile). ::: This part of the lab took me the most time, because I wanted to continue working on my virtual machine, but, unfortunately, there was critically little memory (4GB) for the Docker on it. During this time, I learned how to resize virtual machine storage on the fly, what would happen if I wrote the wrong file system to /fstab for additional storage, which is what the first attempts to increase the root partition of the hard disk lead to (spoiler: data loss). Speaking in general, the whole root of the problem lies in the size of the sparse file that I installed for the future virtual machine image. More space should have been allocated. I add current user to the docker group to make able run the docker command without root privileges: ``` sudo usermod -aG docker ${USER} su - ${USER} ``` First I need to create two files in the directory with my application: ``` #the file itself for the build conditions touch Dockerfile #will contain modules and debug logs that will not be #included in the image touch .dockerignor ``` Here everything is quite simple, we will build from the `node:16` image just for our js-server. Then you need to specify the application code directory inside the image (working directory), I decided to leave the same path that I have now. Next, we install the dependencies using `npm`. And specify `package*.json` (meaning `package.json` and `package-lock.json`). Using `COPY`, I link the source code of the application inside the Docker image. And since my application uses `port 3000`, I use `EXPOSE` to map it to the docker daemon. `CMD` defines the commands to start the server, in my case starting via `node`, and the main file that is displayed in package.json is an `index.js`. And one more point related to the date and time inside the container, I tried to change the timezone first in an already running container (`docker run -e TZ=Europe/Moscow user/hr_app date`) , however, it did not bring me results. Then I decided to add these conditions to the Dockerfile in order to immediately assemble an image with the correct timezone. This required the `TZ` environment variable and the pre-installation of the `tzdata` package. :::spoiler root@st7:/home/user/hr_app# npm init -y Wrote to /home/user/hr_app/package.json: { "name": "hr_app", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.17.2", "moment": "^2.29.1" }, "devDependencies": {}, "description": "" } ::: <center> ![](https://i.imgur.com/kE086jl.png) Figure 5 - Dockerfile </center> In the .docker ignore file, I wrote down the corresponding elements that I wrote about above. Although my server does not have logging, but for the future. ``` node_modules npm-debug.log ``` We can build: ``` docker build . -t user/hr_app ``` <center> ![](https://i.imgur.com/Xw5QJkD.png) Figure 6 - Building Docker image </center> For run hr_app I used following command: ``` #random public port for local port broadcast #and enabling background working container docker run -p 49160:3000 -d user/hr_app ``` <center> ![](https://i.imgur.com/GQwcpVq.png) Figure 7 - All times are correct </center> ## Task 4 - Deliver your app using Software Configuration Management :::info 1. Get your personal cloud account. To avoid some payment troubles, take a look on free tier that e.g. AWS or GCP offers for you (this account type should be fit for a lab). 2. Use Terraform to deploy your required cloud instance. ::: I decided that I would follow this [guide](https://learn.hashicorp.com/collections/terraform/aws-get-started). So first I will install Terraform. To install Terraform, I used the following commands. Despite the fact that the installation from a binary file seemed interesting to me, it seems to me that something is missing in this instruction, because after copying the /terraform directory to /usr/local/bin, the `install` command does not work.: ``` curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - sudo apt-add-repository "deb [arch=$(dpkg --print-architecture)] https://apt.releases.hashicorp.com $(lsb_release -cs) main" sudo apt install terraform ``` <center> ![](https://i.imgur.com/r3SGVUo.png) Figure 8 - Successfully installation </center> To follow this tutorial I will need: > > 1. The AWS CLI installed. > 1. An AWS account. > 1. My AWS credentials. I can need create a new Access Key. To install the AWS console, I used the [official documentation](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) of AWS itself. Therefore, I will not show the unpacking of archives and installation. <center> ![](https://i.imgur.com/8c951cH.png) Figure 9 - AWS CLI </center> Now I need to write a configuration to define a single instance of AWS EC2: ``` #create new directory mkdir terraform-aws-instance && cd terraform-aws-instance #creating a file to define the infrastructure touch main.tf ``` As you can see in the screenshot below, I'm using a standard template for main.tf . This is especially due to the fact that resources are suitable for me. In the configuration, the Ubuntu image is set as the `AMI` identifier, and the instance type is `t2.micro`, which corresponds to the AWS free usage level. <center> ![](https://i.imgur.com/CsHYnyi.png) Figure 10 - main.tf </center> After creating a new configuration, I need to initialize a directory with the terraform init extension. When initializing the configuration directory, the providers defined in the configuration are loaded and installed, in my case this is the aws provider. ``` #initializing terraform init #format the configuration terraform fmt #checkup terraform validate ``` <center> ![](https://i.imgur.com/qvRQRil.png) Figure 11 - Initializing, format, validate </center> Now you need to apply the configuration: ``` terraform apply ``` <center> ![](https://i.imgur.com/hnoSJf2.png) ![](https://i.imgur.com/EJWkp48.png) Figure 12 - Apply configuration </center> <center> ![](https://i.imgur.com/wTCX5VM.png) ![](https://i.imgur.com/ARRFWan.png) Figure 13 - Wow! </center> :::info 3. Choose SCM tool. Suggestion: **Ansible** 4. Using SCM tool, write a playbook tasks to deliver your application to cloud instance. Try to separate your configuration files into inventory/roles/playbooks files. Use Molecula and Ansible Lint to test your application before to deliver to cloud. ::: My computer will act as Ansible management node: ``` sudo apt install ansible ``` Now I need to create a basic things: <center> ![](https://i.imgur.com/yfTmXGK.png) Figure 14 - Ansible host </center> At this stage, I didn't quite understand how to link AS to Ansible via Terraform. But I tried to copy these [two](https://medium.datadriveninvestor.com/devops-using-ansible-to-provision-aws-ec2-instances-3d70a1cb155f) [solutions](https://github.com/dzeban/c10k/tree/master/infrastructure), but I failed. ## References: 1. [Git Documentation](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain) 2. [Guides on Ansible](https://rtfm.co.ua/ansible-scenarii-playbook-i-obrabotchiki-handler/) 3. [Guides on Terraform](https://www.squadcast.com/blog/creating-your-first-module-using-terraform#:~:text=main.tf%3A%20This%20is%20our,output%20definitions%20for%20our%20resources.) 4. [Creating a REST API Part 1: Web Server Basics](https://dzone.com/articles/creating-a-rest-api-web-server-basics) 5. [Github luxon module](https://github.com/moment/luxon/blob/master/docs/formatting.md) 6. [Dockerizing a Node.js web app](https://nodejs.org/de/docs/guides/nodejs-docker-webapp/)