Diagram

Objective

  • Setup Jenkins for automate Terraform process
  • Provision highly available 3-tier architecture with Terraform

 

Pre-requisite :

  • AWS & Github Account
  • Configured AWS Credentials on local
  • AWS CLI Installed on local

 

First | Create and Setup Jenkins Server

Run EC2 Instance for Jenkins Server and fill the parameter what do you want.

aws ec2 run-instances \
--image-id ami-abcd1234 \
--instance-type t2.medium \
--key-name keypair-name \
--subnet-id subnet-abcd1234 \
--security-group-ids sg-abcd1234 \
--user-data file://setup.sh

This is a content of setup.sh :

#!/bin/bash

#Ubuntu/Debian

# Install Jenkins
sudo apt update && sudo apt install git fontconfig unzip openjdk-17-jre -y
curl -fsSL https://pkg.jenkins.io/debian/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update && sudo apt-get install jenkins -y
sudo systemctl enable jenkins

# Install Terraform
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

After instance running and Jenkins successfuly installed, ensure you have open port 8080 on Security Group because its Jenkins default port. And now setting up Jenkins with access your-instance-ip-or-domain:8080 via browser. Unlock-Jenkins

To unlock Jenkins, open terminal and use command below to see the password.

> ssh -i keypair-name user@domain/public-ip-instance 'sudo cat /var/lib/jenkins/secrets/initialAdminPassword' 

0587d04623434423a1532785eaf9692a

 

Second | Generate AWS Credentials

You can skip this step, if you already have Access & Secret Key.

aws iam create-access-key --user-name your-user

output : 
{
    "AccessKey": {
        "UserName": "your-user",
        "AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
        "Status": "Active",
        "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "CreateDate": "2023-10-054T17:34:16Z"
    }
}

 

Third | Configure AWS Credentials on Jenkins Server

Use command below to configure AWS Credentials.

ssh -i keypair-name jenkins@domain/public-ip-instance '
aws configure set aws_access_key_id "YOUR-ACCESS-KEY" --profile jenkins && \
aws configure set aws_secret_access_key "YOUR-SECRET-KEY" --profile jenkins && \
aws configure set region "us-east-1" --profile jenkins
'

 

Fourth | Setup Jenkins Pipeline

Click New Item > Enter Pipeline Name. 3

In Definition choose ‘Pipeline script for SCM’, then fill the repository URL & credentials box with your own and uncheck Lightweight checkout. 5

And this is a code of Jenkinsfile.

#Jenkinsfile
pipeline {
    agent any
    stages {

        stage("Checkout"){
            steps{
        		echo pwd()
                git branch: 'main', 
                  credentialsId: 'auth-jenkins-tf',
                  url: 'https://github.com/defloriooryn/jenkins-terraform.git'
            }
        }

        stage("TF Init"){
            steps{
                sh 'export AWS_PROFILE=jenkins'
                sh 'terraform init'
            }
        }

        stage("TF Validate"){
            steps{
                sh 'terraform validate'
            }
        }

        stage("TF Plan"){
            steps{
                sh 'terraform plan'
            }
        }

        stage("TF Apply"){
            steps{
                sh 'terraform apply -auto-approve'
            }
        }
    }
}

 

Fifth | Deploy 3-Tier Architecture

4

Below is the terraform code for the 3 tier architecture topology above or you can find the complete code in my GitHub account.

version.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
  profile = "jenkins"
}

backend.tf

terraform {
  backend "s3" {
    region  = "us-east-1"
    bucket  = "nizamzam"
    key     = "terraform.tfstate"
    encrypt = true
  }
}

networking.tf

resource "aws_vpc" "vpc_example" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc_example.id
}

resource "aws_subnet" "public" {
  for_each                = var.subnet_public
  vpc_id                  = aws_vpc.vpc_example.id
  cidr_block              = each.value.cidr
  availability_zone       = each.value.az
  map_public_ip_on_launch = true
}

resource "aws_subnet" "private" {
  for_each          = var.subnet_private
  vpc_id            = aws_vpc.vpc_example.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
}

resource "aws_subnet" "database" {
  for_each          = var.subnet_database
  vpc_id            = aws_vpc.vpc_example.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
}

resource "aws_eip" "eip_natgw" {
  domain = "vpc"
}

resource "aws_nat_gateway" "nat_gw" {
  allocation_id = aws_eip.eip_natgw.id
  subnet_id     = values(aws_subnet.public)[0].id
}

resource "aws_route_table" "route_public" {
  vpc_id = aws_vpc.vpc_example.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}

resource "aws_route_table" "route_private" {
  vpc_id = aws_vpc.vpc_example.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_nat_gateway.nat_gw.id
  }
}

resource "aws_route_table_association" "public" {
  for_each       = aws_subnet.public
  subnet_id      = each.value.id
  route_table_id = aws_route_table.route_public.id
}

resource "aws_route_table_association" "private" {
  for_each       = aws_subnet.private
  subnet_id      = each.value.id
  route_table_id = aws_route_table.route_private.id

}

resources.tf

# Security Group Application Load Balancer
resource "aws_security_group" "sg-alb" {
  name        = "sg_alb"
  vpc_id      = aws_vpc.vpc_example.id
  description = "allow http and https"
  dynamic "ingress" {
    for_each = var.sg_alb_port
    iterator = port
    content {
      from_port   = port.value
      to_port     = port.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Security Group Instance
resource "aws_security_group" "sg-instance" {
  name        = "sg_ec2"
  vpc_id      = aws_vpc.vpc_example.id
  description = "allow http from sg-alb"
  dynamic "ingress" {
    for_each = var.sg_ec2_port
    iterator = port
    content {
      from_port       = port.value
      to_port         = port.value
      protocol        = "tcp"
      security_groups = [aws_security_group.sg-alb.id]
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

#Security Group RDS
resource "aws_security_group" "sg-rds" {
  name        = "sg_rds"
  vpc_id      = aws_vpc.vpc_example.id
  description = "allow port 3306 from sg-instance"

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.sg-instance.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_launch_template" "launch_template" {
  name          = "template"
  image_id      = var.instance_template.ami
  instance_type = var.instance_template.type
  user_data = base64encode(file("${path.module}/script/nginx.sh"))
  
  network_interfaces {
    device_index    = 0
    security_groups = [aws_security_group.sg-instance.id]
  }
}

resource "aws_autoscaling_group" "asg" {
  desired_capacity    = var.asg.desired
  min_size            = var.asg.min
  max_size            = var.asg.max
  target_group_arns   = [aws_lb_target_group.tg_instance.arn]
  vpc_zone_identifier = values(aws_subnet.private)[*].id

  launch_template {
    id      = aws_launch_template.launch_template.id
    version = "$Latest"
  }
}

resource "aws_lb" "alb" {
  name               = "external-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.sg-alb.id]

  dynamic "subnet_mapping" {
    for_each = aws_subnet.public
    content {
      subnet_id = subnet_mapping.value.id
    }
  }

}
resource "aws_lb_target_group" "tg_instance" {
  name     = "nginx"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.vpc_example.id

  health_check {
    path    = "/"
    matcher = 200
  }
}

resource "aws_lb_listener" "lb_listener_http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    target_group_arn = aws_lb_target_group.tg_instance.id
    type             = "forward"
  }
}

resource "aws_db_instance" "db" {
  allocated_storage    = var.db.allocated_storage
  db_name              = var.db.db_name
  db_subnet_group_name = aws_db_subnet_group.sg_db.name
  engine               = var.db.engine
  engine_version       = var.db.engine_version
  instance_class       = var.db.instance_class
  username             = var.db.username
  password             = var.db.password
  parameter_group_name = var.db.parameter_group_name
  skip_final_snapshot  = var.db.skip_final_snapshot
  availability_zone    = var.db.availability_zone
  multi_az             = var.db.multi_az
}

output.tf

output "alb_dns" {
  value = aws_lb.alb.dns_name
}

variables.tf

variable "region" {
  type    = string
  default = "us-east-1"
}

# VPC & Subnet 
variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "subnet_public" {
  type = any
  default = {
    "public-1" = { cidr = "10.0.0.0/24", az = "us-east-1a" }
    "public-2" = { cidr = "10.0.1.0/24", az = "us-east-1b" }
  }
}

variable "subnet_private" {
  type = map(object({
    cidr = string
    az   = string
  }))
  default = {
    "private-1" = { cidr = "10.0.2.0/24", az = "us-east-1a" }
    "private-2" = { cidr = "10.0.3.0/24", az = "us-east-1b" }
  }
}

variable "subnet_database" {
  type = map(object({
    cidr = string
    az   = string
  }))
  default = {
    "private-3" = { cidr = "10.0.4.0/24", az = "us-east-1a" }
    "private-4" = { cidr = "10.0.5.0/24", az = "us-east-1b" }
  }
}

# Security Group
variable "sg_alb_port" {
  type    = list(any)
  default = [80, 443]
}

variable "sg_ec2_port" {
  type    = list(any)
  default = [80, 22]
}

variable "sg_rds_port" {
  type    = number
  default = 3306
}

# Auto Scalling Group
variable "instance_template" {
  type = map(string)
  default = {
    type = "t2.micro"
    ami  = "ami-067d1e60475437da2" #Amazon Linux 2023
  }
}

# ASG
variable "asg" {
  type = any
  default = {
    az      = ["us-east-1a", "us-east-1b"]
    desired = 2
    min     = 2
    max     = 4
  }
}

# Database
variable "db" {
  type = map(string)
  default = {
    allocated_storage    = 10
    db_name              = "mydb"
    engine               = "mysql"
    engine_version       = "5.7"
    instance_class       = "db.t3.micro"
    username             = "admin"
    password             = "admin"
    parameter_group_name = "default.mysql5.7"
    skip_final_snapshot  = true
    availability_zone    = "us-east-1a"
    multi_az             = false
  }
}

Sixth | Test Pipeline

After the Jenkins setup is complete and you have pushed the code to your repository. Then in your Job, click Build Now to trigger the pipeline. 6

 

Additional :

Just like that for notes today, hopefully I can still exist to update this website