Skip to content

epquito/TodoList-S3-Static-Website

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 

Repository files navigation

Deploying 3-Tier Website with AWS/Ansible/Terraform

This repository provides scripts and configurations to deploy a 3-tier web application using AWS, Ansible, and Terraform.

Alt Text

Prerequisites

Before deploying the Flask application "TodoList," ensure you have the following and have a brief understading of aws resources:

  • AWS account
  • VSCode
  • Terraform
  • Ansible

Commands to Execute in Terminal

Execute the following commands in the terminal:

terraform init
terraform plan
terraform apply
ansible-vault create vault.yml
ansible-playbook --ask-vault-pass role.yml -i aws_ec2.yml -u ubuntu --private-key=/path/to/pem/file

Building AWS infrastructure with Terraform

Create two files main.tf/providers.tf within the root directory of the repository

  • provider.tf:
# Enter region associated with your AWS account
provider "aws" {
    region = "Enter Region"
  
}

terraform {
  required_providers {
    aws = {
        source = "hashicorp/aws"
        version = "~> 4.16"
    }
  }
}
# Allows terraform to retrieve data from AWS to get latest ubuntu ami within the specified filters
data "aws_ami" "ubuntu_22" {
    most_recent = true

    filter {
      name = "name"
      values = ["ubuntu/images/hvm-ssd/ubuntu*22*amd64*"]
    }
    filter {
      name = "virtualization-type"
      values = ["hvm"]
    }
    owners = ["amazon"]

}

Now initialize providers.tf

terraform init

Create main.tf and configure your resources

# Refrences the defualt vpc that is given to you by aws
resource "aws_default_vpc" "default" {
    tags = {
      Name = "default"
    }
  
}
# References the default Security Group that is given to you by AWS but also changes the ingress/egress for this deployment.

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id
  ingress {
    description = "http"
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "ssh"
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "myqsl"
    from_port = 3306
    to_port = 3306
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = [ "0.0.0.0/0" ]

  }
  tags = {
    Name = "default"
  }
}

Within the same main.tf file, you also need to create an IAM role/policy and attach it to your EC2 instance to give it full access to S3 buckets.

# Define the IAM Role
resource "aws_iam_role" "ROLENAME" {
 name = "ROLENAME"

 assume_role_policy = jsonencode({
   Version = "2012-10-17",
   Statement = [
     {
       Action = "sts:AssumeRole",
       Effect = "Allow",
       Principal = {
         Service = "ec2.amazonaws.com",
       },
     },
   ],
 })
}

# Define the IAM Role Policy
resource "aws_iam_policy" "POLICYNAME" {
 name        = "POLICYNAME"
 description = "Policy for EC2 to access S3"

 policy = jsonencode({
   Version = "2012-10-17",
   Statement = [
     {
       Action   = "s3:*",  # Adjust the permissions as needed
       Effect   = "Allow",
       Resource = "*",
     },
   ],
 })
}

# Attach the Role Policy to the Role
resource "aws_iam_policy_attachment" "role_policy_attachment" {
 name       = "role-policy-attachment"
 roles      = [aws_iam_role.ROLENAME.name]
 policy_arn = aws_iam_policy.POLICYNAME.arn
}

# Define the IAM Instance Profile
resource "aws_iam_instance_profile" "ec2_profile" {
 name = "ec2_profile"
 role = aws_iam_role.ROLENAME.name
}

Create your backend and database EC2 instances and attach the IAM role.

resource "aws_instance" "backend" {
     ami           = data.aws_ami.ubuntu_22.id
     instance_type = "t2.micro"
     key_name      = "PEMFILE"
     iam_instance_profile = aws_iam_instance_profile.ec2_profile.name  # Use the instance profile
     provisioner "local-exec" {
        command = "echo 'backend: \"${aws_instance.backend.public_ip}\"'>/path/to/project/ansible/backend_role/var/file "
       
     }
     tags = {
        Name = "backend"
        Enviroment = "dev"
        Team = "mobile-app"
        Type = "backend"
        }
}
resource "aws_instance" "database" {
     ami           = data.aws_ami.ubuntu_22.id
     instance_type = "t2.micro"
     key_name      = "PEMFILE"
     provisioner "local-exec" {
        command = "echo 'database: \"${aws_instance.database.public_ip}\"'>/path/to/project/backend_role/default/file"
       
     }

     tags = {
        Name = "database"
        Enviroment = "dev"
        Team = "mobile-app"
        Type = "database"
        }
}

output "TodoList" {
    value = aws_instance.backend.public_ip
  
}

output "TodoList-db" {
    value = aws_instance.database.public_ip
  
}

Create S3 bucket and object, also configure bucket policy.

resource "aws_s3_bucket" "bucket-name" {
  bucket = "bucket-name"
  website {
    index_document = "index.html"
  }
  tags = {
    Type = "frontend"

  }
}
resource "aws_s3_bucket_object" "index" {
  depends_on = [ aws_s3_bucket.bucket-name ]
  bucket       = aws_s3_bucket.bucket-name.bucket
  key          = "index.html"
  content_type = "text/html"
  # For this deployment, I saved the index.html from the web app to my local machine.

  source = "/local/path/to/index.html"
}

resource "aws_s3_bucket_public_access_block" "bucket-name" {
  bucket = aws_s3_bucket.bucket-name.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}
# Define the Bucket Policy
resource "aws_s3_bucket_policy" "bucket-policy" {
  depends_on = [ aws_instance.backend, aws_instance.database]
  bucket = aws_s3_bucket.bucket-name.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect    = "Allow",
        Principal = "*",
        Action    = "s3:GetObject",
        Resource  = "${aws_s3_bucket.bucket-name.arn}/*",
      },
    ],
  })
}

# Define Bucket Ownership Controls
resource "aws_s3_bucket_ownership_controls" "bucket-name" {
  bucket = aws_s3_bucket.bucket-name.id

  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

# Output the Bucket Website Endpoint
output "bucket_website_endpoint" {
  value = aws_s3_bucket.bucket-name.website_endpoint

}

After completing the creation of your AWS architecture structure for your resources, you can now initiate the deployment, and the resources specified in the main.tf file will be provisioned within your AWS account.

terraform plan
terraform apply

Directory should look like this

├── index.html
├── main.tf
├── provider.tf
├── terraform.tfstate
├── terraform.tfstate.backup

With the AWS resources successfully provisioned, we can now proceed to configure the EC2 instances using Ansible.

Within the same directory, we are going to create a subdirectory named 'final' where all the Ansible files are going to be stored, such as:

  • Dynamic Ansible inventory
  • Ansible role
  • Playbooks
  • Ansible vault

Before anything, we need to create all of our subdirectories for the final deployment:

  • Create backend_role and database_role directories within the 'final' directory.
  • Each role directory will have 7 subdirectories that will execute when the main role playbook is executed.
├── backend_role
│   ├── defaults
│   │   └── main.yml
│   ├── files
│   ├── handlers
│   │   └── main.yml
│   ├── meta
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   └── vars
│       ├── main.yml
│       └── vault.yml
├── database_role
│   ├── defaults
│   ├── files
│   ├── handlers
│   │   └── main.yml
│   ├── meta
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   └── vars
│       └── vault.yml


creating dynamic inventory

---
plugin: aws_ec2
regions:
  - Region name

keyed_groups:
  # Create groups based on the value of the 'Type' tag
  - key: tags['Type']
    prefix: tag_Type

# Filter instances to include those tagged as either 'backend' or 'database' or any other tags
filters:
  tag:Type:
    - database
    - backend

Create your main Ansible role playbook that will reference your Ansible vault for secured variables and be able to execute both backend and database roles, automating the configurations of both EC2 instances.

When referencing a role within Ansible, make sure it's the name of the directory where all 7 subdirectories are present for the backend/database role.

role.yml

---
- name: database server
  hosts: tag_Type_database
  become: true
  vars_files:
   - /path/to/database/vars/ansible.vault/file
  roles:
   - { role: database_role}

- name: backend server
  hosts: tag_Type_backend
  become: true
  vars_files:
   - /path/to/database/vars/ansible.vault/file
  roles:
   - { role: backend_role}

Configure the database_role

Within the database role, we will use handlers, tasks, vars subdirectories.

-tasks/main.yml:

---
- name: restarting server
  apt:
    update_cache: yes
  
- name: install MySQL
  apt:
    name: 
      - mysql-server
      - mysql-client
      - python3-mysqldb
      - libmysqlclient-dev
  become: true

- name: start mysql 
  systemd:
    name: mysql
    state: started
    enabled: yes
  become: true

- name: mysql secure installation
  become: true
  command: mysql_secure_installation
  args:
    stdin: |
      SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{{ db_password }}');
      DELETE FROM mysql.user WHERE User='';
      DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost');
      DROP DATABASE IF EXISTS test;
      DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
      FLUSH PRIVILEGES;

- name: create user
  mysql_user:
    name: "{{db_user}}"
    password: "{{db_password}}"
    priv: '*.*:ALL'
    host: '%'
    state: present
  become: true

- name: create Db
  mysql_db:
    name: "{{ db_name }}"
    state: present
  become: true

- name: adjusting mysql config file
  lineinfile:
    path: /etc/mysql/mysql.conf.d/mysqld.cnf
    regexp: '^bind-address'
    line: 'bind-address = 0.0.0.0'
    backup: yes
  become: true
  notify: restart mysql

-handlers/main.yml

---
- name: restart mysql
  systemd: 
   name: mysql
   state: restarted

To create a secure file to store your variables, you would need to create an Ansible Vault file inside the directory where you want to store it.

vars/vault.yml:

# Once inside the file is created, store your sensitive information of the database within this file.
ansible-vault create vault.yml

# If you want to edit the information inside, use this command. Keep in mind it will ask for the password used for the creation of the file you are trying to access.
ansible-vault edit vault.yml

Now configure your backend_role files

We are going to be using the subdirectories handlers, tasks, vars.

  • tasks/main.yml:
--- 
- name: restarting backend ec2
  apt:
   update_cache: yes

- name: installing git 
  apt:
   name: 
    - git
    - python3
    - python3-pip
    - gunicorn
   state: latest

- name: installing boto3
  ansible.builtin.pip:
   name: boto3
   state: present

- name: installing neccesery files from github repository
  ansible.builtin.git:
   repo: "https://github.com/chandradeoarya/todolist-flask.git"
   dest: "/home/ubuntu/todolist-flask"
   single_branch: yes
   version: env

- name: get rid of index.html from the repository
  ansible.builtin.file:
   state: absent
   path: "/home/ubuntu/todolist-flask/index.html"

- name: download s3 bucket index.html
  aws_s3:
   bucket: bucket-name
   mode: get
   object: "index.html"
   dest: /home/ubuntu/todolist-flask/index.html

- name: update api url
  lineinfile:
   path: /home/ubuntu/todolist-flask/index.html
   regexp: "const API_URL ="
   line: "const API_URL = 'http://{{backend}}'"

- name: create env file
  copy:
   dest: "/home/ubuntu/todolist-flask/.env"
   content: |
    MYSQL_DATABASE_HOST = "{{db_host}}"
    MYSQL_DATABASE_USER = "{{db_user}}"
    MYSQL_DATABASE_PASSWORD = "{{db_password}}"
    MYSQL_DATABASE_PORT = "{{db_port}}"
   mode: '0644'

- name: re-upload s3 bucket 
  aws_s3:
   bucket: bucket-name
   mode: put
   object: "index.html"
   src: /home/ubuntu/todolist-flask/index.html

- name: delete index.html file once its re-uploaded to s3 bucket
  ansible.builtin.file:
   state: absent
   path: "/home/ubuntu/todolist-flask/index.html"

- name: install requirements
  command: pip install -r /home/ubuntu/todolist-flask/requirements.txt

- name: create todolist gunicorn.py file
  ansible.builtin.file:
   path: /home/ubuntu/todolist-flask/gunicorn_config.py
   state: touch
   mode: u+rwx

- name: add configurations to gunicorn
  ansible.builtin.blockinfile:
   path: /home/ubuntu/todolist-flask/gunicorn_config.py
   block: |
    preload_app = True
    bind = "0.0.0.0:80"
    workers = 4
   mode: u+rwx
    

- name: create todolist service file
  ansible.builtin.file:
   path: /etc/systemd/system/todolist.service
   state: touch
   mode: u+rwx

- name: Add content to todolist service file
  ansible.builtin.blockinfile:
   path: /etc/systemd/system/todolist.service
   block: |
    # BEGIN ANSIBLE MANAGED BLOCK
    [Unit]
    Description=Gunicorn instance to serve todolist
    Wants=network.target
    After=syslog.target network-online.target
    [Service]
    Type=simple
    WorkingDirectory=/home/ubuntu/todolist-flask
    ExecStart=/usr/bin/gunicorn todo:app -c /home/ubuntu/todolist-flask/gunicorn_config.py
    Restart=always
    RestartSec=10
    [Install]
    WantedBy=multi-user.target
    # END ANSIBLE MANAGED BLOCK

- name: start
  systemd: 
   name: todolist.service
   state: started
   enabled: yes

- name: restart daemon-reload
  systemd: 
   daemon-reload: yes
  notify: restarting todolist

-handlers/main.yml:

---
- name: restarting todolist
  systemd:
   name: todolist.service
   state: restarted

-vars/vault.yml You can either create a new ansible vault file and give it anoter name and password but for this deployment we actually copied the vault.yml from database_role/vars/vault.yml since the information we need is the same from that file.

# Once inside the file, store your sensitive information of the database within this file 
ansible-vault create vault.yml

# If you want to edit the information inside, use this command. Keep in mind it will ask for the password used for the creation of the file you are trying to access
ansible-vault edit vault.yml

-defaults/main.yml:

# Earlier in the instructions, when we created the database EC2 instance, we made sure to forward the key:value once it was executed to this location within the terminal, so it will auto-populate the newest EC2 database IP.
db_host: {{ databse_ec2_IP}}

Now that both Terraform and Ansible are finally created and configured, you can now activate the playbooks, and they will automatically configure the EC2 instances for you

ansible-playbook -ask-vault-pass role.yml -i aws_ec2.yml -u ubuntu -k --private-key:/path/to/pem/file
#or you can execute it in the main directory where terraform is located
ansible-playbook -ask-vault-pass final/role.yml -i final/aws_ec2.yml -u ubuntu -k --private-key:/path/to/pem/file

Final directory structure Overview:

├── todo-list-flask-mysql-app
│   ├── final
│   │   ├── aws_ec2.yml
│   │   ├── backend_role
│   │   │   ├── defaults
│   │   │   │   └── main.yml
│   │   │   ├── files
│   │   │   ├── handlers
│   │   │   │   └── main.yml
│   │   │   ├── meta
│   │   │   ├── tasks
│   │   │   │   └── main.yml
│   │   │   ├── templates
│   │   │   └── vars
│   │   │       ├── main.yml
│   │   │       └── vault.yml
│   │   ├── database_role
│   │   │   ├── defaults
│   │   │   ├── files
│   │   │   ├── handlers
│   │   │   │   └── main.yml
│   │   │   ├── meta
│   │   │   ├── tasks
│   │   │   │   └── main.yml
│   │   │   ├── templates
│   │   │   └── vars
│   │   │       └── vault.yml
│   │   └── role.yml
│   ├── index.html
│   ├── main.tf
│   ├── provider.tf

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors