How to deploy an application to AWS EC2 Instance using Terraform and Ansible.
Tags: aws, docker, ansible, beginners
Published: 2021-04-21 22:46:55

I had an opportunity to work on this recently, and had to combine different tutorials and articles before I was able to set this up successfully.

So, I’m writing this article in hopes that it will make someone’s work easier and make things clearer for beginners who wish to learn this.

<p>&nbsp;</p>
##What is Infrastructure as Code?
Infrastructure as Code (IaC) is the process of setting up and managing cloud infrastructure through machine-readable files as opposed to manually setting up the required infrastructure.

After the files have been written, you run them using IaC tools to build and configure the cloud infrastructure. Needless to say, this is a much more palatable and easier process than manual setup.

IaC tools include; Terraform, Ansible, Chef, AWS cloud formation, Puppet, Vagrant, and many others.

<p>&nbsp;</p>
##Benefits of Infrastructure as Code
* With IaC, infrastructure configuration and setup is much faster: all you do is run a script.
* Scripts can be easily reused at any time.
* It is also easier to make small modifications between different but similar setups.
* It increases site reliability.
* It reduces the possibility of errors in infrastructure set up.

In this tutorial, you will learn how to provision a server(AWS EC2) for a React app using Terraform and then configure them with necessary packages using Ansible.

Let’s start by highlighting all the steps needed and then explaining them in detail below:
1. [Configure AWS CLI and AWS vault](#aws)
2. [Create a Key-pair for the server](#key-pair)
3. [Dockerize a React app](#docker)
4. [Provision the server using terraform](#terraform)
5. [Create a security group for the server](#security-group)
6. [Configure the server using Ansible](#ansible)

<p>&nbsp;</p>
##My folder structure
![folder structure](https://user-images.githubusercontent.com/33374159/115626986-2fa61480-a2f6-11eb-985c-4cab8690d897.png)

<p>&nbsp;</p>
##Prerequisites
If you’d like to follow this tutorial, please make sure the following requirements are met before doing so.
* AWS account. You can create one [here](https://aws.amazon.com/) if you haven't already, and follow this [tutorial](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) in setting it up.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) and [AWS vault](https://github.com/99designs/aws-vault#readme).
* [Docker](https://docs.docker.com/get-started/#download-and-install-docker) and [Dockerhub](https://hub.docker.com/) account.
* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) 
* [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#selecting-an-ansible-version-to-install)
* A demo React app. You can create one with this [tutorial](https://reactjs.org/docs/create-a-new-react-app.html#create-react-app).

Now, let’s get to it!

<p>&nbsp;</p>
##Step 1 — Configure AWS CLI and AWS vault <a name="aws"></a>
We’ll need to create a user on our AWS account to be able to configure the AWS CLI.

To do this, login to your AWS account, go to **Services**, then click on **IAM**.  Click on **User**, then **Add a user**. Type in a **Username**, then select **Programmatic access** for AWS access type.

Next, we’ll need to create a group for our user. Click on **Create group** and give it **Administrator access**. You can add **Tags** (optional), click **Next** to review the user details and then click on **Create user**.

![user](https://user-images.githubusercontent.com/33374159/115628260-49e0f200-a2f8-11eb-87dd-429f01b39a8f.png)

After this, a new screen would be displayed to download the user security credentials. Click on **Download.csv**.

Now, go back to the terminal and run the following command:

```
aws configure
```

Add the user **Access key ID** and **Secret Access Key** as prompted. Then enter your preferred **AWS region** and **Output**.

Run the following command and make sure to replace `username` with the user created previously from the AWS console. 

```
aws-vault add <username>
```

Add the user **Access key ID** and **Secret Access Key** as prompted. This is done to store your AWS credentials in your machine local keystore.

Then run the following command to authenticate yourself for a session.

```
aws-vault exec <username>
```

<p>&nbsp;</p>
##Step 2 — Create a Key-pair for the server <a name="key-pair"></a>
We need a key pair to run our instance.

To create one, go to your AWS console, select **EC2** from **Services** drop-down, click on **Key pairs**, then click on **Create key pair** button.

Enter a **Name** for your key, select **pem** for openSSH or **ppk** for Putty and then click on **Create key pair**. 

![key pair](https://user-images.githubusercontent.com/33374159/115628196-2b7af680-a2f8-11eb-8361-775ee38d8420.png)

Download the key and move it to your machine’s **.ssh** folder. For Ubuntu and MacOS, this will most likely be **~/.ssh**

<p>&nbsp;</p>
##Step 3 — Dockerize the app <a name="docker"></a>
In the root directory of the **react-app** folder, create a **Dockerfile** and add the following lines:

```
FROM node:12.18.3

LABEL version="1.0"

LABEL description="This is the base docker image for my React app"

LABEL maintainer="abc@mail.com"

WORKDIR /usr/src/app

COPY ["package.json", "yarn.lock", "./"]

RUN yarn

COPY . .

EXPOSE 3000

CMD ["yarn", "start"]
```

Here is an overview of the commands:
**FROM** defines the image we’re using for our container. In this context, we’re using node:12.18.3
**LABEL** indicates the version, description and maintainer of the Dockerfile.
**WORKDIR** sets the working directory for the app, and create it if the directory did not exists.
**COPY** is used to copy file(s) from one destination to another. The last path is always the destination to copy the file(s) to.
**RUN** defines the command to be run by Docker. I used yarn here because I installed my React app with yarn, you can change yours to npm if it’s more applicable.
**EXPOSE** tells docker which port it should listen to when running the container.
**CMD** defines the command to start the container. I used yarn start as specified from my React app’s start script, you should make sure yours also correlates to your app’s start script.

Now, you need to build a docker image from the Dockerfile. To achieve this, run the following command in the root directory of the **react-app** folder:

```
docker build -t <image-name> .
```

Make sure you replace `image-name` with the app’s image name.

Now, run the following command to spin up a container from the image.

```
docker run -it -p 3000:3000 <image-name>
```

Go to [http://localhost:3000](http://localhost:3000) on your browser to view the app.

![deploy](https://user-images.githubusercontent.com/33374159/115626849-f8cffe80-a2f5-11eb-9f7d-ebbab37120c5.png)

Go back to your terminal and add a tag to the image using:

```
docker tag <image-name> <docker_hub_username>/<repo-name>:<tag-name>
```

Be sure to replace all variables with appropriate values.

Next, login to your DockerHub account from your terminal using:

```
Docker login
```

Add your **username** and **password** as prompted.

![docker login](https://user-images.githubusercontent.com/33374159/115626915-10a78280-a2f6-11eb-9c4f-4db9115999f9.png)

Then run the following command to push the image to Dockerhub.

```
docker push <docker_hub_username>/<repo-name>:<tag-name>
```

Once that is done, you should see your docker image in the repo created.

![dockerhub](https://user-images.githubusercontent.com/33374159/115630118-629ed700-a2fb-11eb-8317-d9f3583f145d.png)

<p>&nbsp;</p>
##Step 4 — Provision the server using terraform <a name="terraform"></a>
In the root directory of the project folder, create a folder called **terraform**. Inside the terraform folder, create a new folder and call it **frontend**. We’ll be adding all the config files for our frontend in here. Next, create a file called **main.tf** inside the frontend folder.

Put the following code inside the **main.tf** file.

```
provider "aws" {
    region = "us-east-1"
}

variable "name" {
    description = "Name the instance on deploy"
}

resource "aws_instance" "admin_frontend" {
    ami = "ami-042e8287309f5df03"
    instance_type = "t2.micro"
    key_name = "admin"

    tags = {
            Name = var.name
    }
}
```

Here, we’re creating an AWS ec2 instance using ubuntu server image (**ami-042e8287309f5df03**), **t2.micro** as our instance type and **admin** as the name of the key-pair we created earlier. Make sure you replace **region**, **ami**, **instance_type**, and **key.name** with values from your setup.

In your terminal, `cd` into the **frontend** folder and run the following command to initialize a working directory containing terraform configuration files.

```
terraform init
```

![terraform init](https://user-images.githubusercontent.com/33374159/115627324-ba870f00-a2f6-11eb-8128-b62a2f15b9f1.png)

After that, run the following command to verify that you've set it up correctly.

```
terraform plan
```

It will later prompt for a value, enter the instance name, `frontend`.

![terraform plan](https://user-images.githubusercontent.com/33374159/115627285-a3e0b800-a2f6-11eb-8de8-b778d95284f0.png)

Next, run the following command to spin up your ec2 instance. 

```
terraform apply
```

This should also prompt for a value, enter the instance name as `frontend` again and at the next prompt, enter `yes` to confirm.

![terraform apply](https://user-images.githubusercontent.com/33374159/115627239-8ad80700-a2f6-11eb-8964-5ec98cd6ea39.png)

Now, if you check your running instances on the AWS console, it should be there.

![running instance](https://user-images.githubusercontent.com/33374159/115627155-6e3bcf00-a2f6-11eb-9611-a9efd9176dfd.png)

<p>&nbsp;</p>
##Step 5 — Create a security group for the server <a name="security-group"></a>
Now we need to create a  security group for our **frontend** instance.

Go to AWS console, select **EC2** from **Services** dropdown, then click on **Security groups**. Click on **Create security group**, then add **Security group name** and **Description**.

Scroll down to the **Inbound rules**, and click **Add rule**. Select **HTTP** for Type and **Anywhere** for Source. Once that’s done, create yet another inbound rule with Type as  **All TCP** and **Anywhere** for Source. Create a last inbound rule with **SSH** as Type and **My IP** for Source. This makes a total of **three** inbound rules

![inbound rules](https://user-images.githubusercontent.com/33374159/115627422-dee2eb80-a2f6-11eb-9ba4-98e3ea49dc3c.png)

Leave the Outbound rule as **All traffic** for Type and **0.0.0.0/0** for Destination. Then click on **Create security group**. You can read this [article](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/authorizing-access-to-an-instance.html) if you’re interested in learning more about security groups.

Now, go back to the **frontend** instance and attach the security group created to it. To do this,  select the **frontend** instance, then  **Actions** > **Security** > **Change security groups** > click on the **search field** and choose the **security group**, remove the **default** security group, then **Save**.

![security groups](https://user-images.githubusercontent.com/33374159/115627468-f326e880-a2f6-11eb-8570-3c73fba171df.png)

Now, you can try and ssh into your server using the following command:

```
ssh -i "~/.ssh/<your_KeyPair>.pem" <ec2-user>@<public_IPv4_DNS>
```

Make sure to replace `ec2-user` with AMI username, `public_IPv4_DNS` with the instance public domain name, and `your_KeyPair` with your key file name.

![login](https://user-images.githubusercontent.com/33374159/115627791-83fdc400-a2f7-11eb-8aaa-9475ac307af2.png)

<p>&nbsp;</p>
##Step 6 — Configure the server using Ansible <a name="ansible"></a>
Now that we have our instance running, we need to install the necessary packages using Ansible. Before we do that, let’s add some settings to our ansible config file.

Go to your terminal and run the following command to open up the config file. I use **vim** but feel free to use any editor of your choice. 

```
sudo vim /home/<username>/.ansible.cfg
```

Be sure to replace `username` with your own username.

Then insert the following, making sure to replace `your_KeyPair` with your key name.

```
[defaults]
host_key_checking = False
private_key_file=~/.ssh/<your_KeyPair>.pem
inventory=/etc/ansible/hosts
remote_user = ubuntu

[ssh_connection]
control_path=%(directory)s/%%h-%%r
control_path_dir=~/.ansible/cp
#pipelining = True
scp_if_ssh = True
```

Now save and exit.

![config](https://user-images.githubusercontent.com/33374159/115627864-9d067500-a2f7-11eb-8af0-e2326a75cdea.png)

We also need to add our host into the ansible host file. To achieve that, open the ansible hosts file by running the following command:

```
sudo vim /etc/ansible/hosts
```

Then add these lines to the file. Make sure to replace `ec2-user` with AMI username, `public_IPv4_DNS` with the instance public domain name, and `your_KeyPair` with your key name.

```
[frontend]                                                                                                                                                                                                                                                                                         ec2-user@public_IPv4_DNS ansible_ssh_private_key_file=~/.ssh/your_KeyPair.pem
```

Save and exit.

![host](https://user-images.githubusercontent.com/33374159/115627966-c3c4ab80-a2f7-11eb-82a3-6e55bfec891e.png)

Now, make sure your managed node can be reached by pinging it  using the following command:

```
sudo ansible all -m ping
```

If it cannot be reached, then you may have connectivity issues that need some debugging. If you get a green blob of json however, then you’re good to go.

![ping](https://user-images.githubusercontent.com/33374159/115628115-04242980-a2f8-11eb-9d10-063e690cedb0.png)

Now, go to the root of your project folder, create a folder and call it **ansible**. Inside this **ansible** folder, create a file and call it **provision_frontend.yaml**

Add the following lines into the **provision_frontend.yaml file**

```
---
- hosts: frontend
  become: yes
  become_method: sudo

  tasks:
  
  - name: Install pip
    apt:
      update_cache: yes
      name: python3-pip

  - name: Install aptitude using apt
    apt: name=aptitude state=latest update_cache=yes force_apt_get=yes

  - name: Install required system packages
    apt: name={{ item }} state=latest update_cache=yes
    loop: [ 'apt-transport-https', 'ca-certificates', 'curl', 'software-properties-common', 'python3-pip', 'virtualenv', 'python3-setuptools']

  - name: Add Docker GPG apt Key
    apt_key:
      url: https://download.docker.com/linux/ubuntu/gpg
      state: present

  - name: Add Docker Repository
    apt_repository:
      repo: deb https://download.docker.com/linux/ubuntu bionic stable
      state: present

  - name: Update apt and install docker-ce
    apt: update_cache=yes name=docker-ce state=latest

  - name: install docker-py
    pip: name=docker-py

  - name: enable Docker services
    service:
      name: "docker"
      state: started
      enabled: yes

  - name: Check if container is running
    shell: docker ps

  - name: run docker image
    shell: docker run -dit --name <repo-name> -p 3000:3000 <docker_hub_username>/<repo-name>:<tag-name>

  - name: show running images
    shell: docker images
```

Make sure to edit the shell command for running docker image, to match your setup. 

Then save.

**hosts** defines the host upon which commands in a playbook operate.
**become_method** sets to root user.
**package** is for installing packages.
**service** is for controlling services on remote hosts.
**shell** is for running commands.

In the terminal, navigate to the **ansible** folder and run the playbook using the following command:

```
ansible-playbook provision_frontend.yaml
```

![ansible](https://user-images.githubusercontent.com/33374159/115628003-d50db800-a2f7-11eb-8cc5-64e37ec0e1eb.png)

Now, open your browser and enter the public address of the instance with **3000** at the end.

Yay! That’s it! This might have been a lot, but I hope it was able to help you along in your journey.

If you would like to set up CI/CD for your app using **Jenkins**, you can check out one of my other articles [here](https://dev.to/mariehposa/achieving-continuous-integration-and-deployment-with-jenkins-4d6k).

If you’ve found this article helpful, please leave a heart or a comment. If you have any questions or constructive feedback, please let me know in the comment section.
Also, don’t forget to follow me for more articles. Thank you!