Jenkins on EC2: Automating Deployment with Terraform
Jenkins is a popular open-source automation server that can be used to implement continuous integration and continuous delivery pipelines for software projects. It allows you to build, test, and deploy your code using a set of instructions called Jenkinsfile.
However, installing and configuring Jenkins on a cloud platform such as AWS can be a tedious and error-prone process. That's why in this article, I will show you how to automate the installation of Jenkins on an AWS EC2 instance using Terraform, an infrastructure as code tool that can create and manage cloud resources declaratively.
By using terraform, you can simplify and standardize the deployment of Jenkins on AWS, as well as leverage the benefits of versioning, modularity, and reusability of your infrastructure code.
Prerequisites
You will need the following prerequisites:
- An AWS account with access to create EC2 instances and other resources. You can sign up for a free tier account on the AWS website
- Terraform installed on your local machine. You can download the appropriate package for your operating system from the Terraform download page and follow the installation instructions or use a package manager such as Homebrew on Mac OS or Chocolatey on Windows.
- AWS CLI configured on your local machine with your AWS credentials and region. You can install the AWS CLI by following the official documentation.
Once you have all the prerequisites ready, you can proceed to the next section where you will set up Jenkins with Terraform on AWS EC2 instance.
Step 1: Create a directory to be used for the project
The first step is to create directory which will be used for this project, let's start with the variables.tf file:
variable "ami" {
type = string
default = "#AMI_OF_UBUNTU"
}
variable "infra_env" {
default = "YOUR_ENV"
}
variable "instace_type" {
type = string
default = "t3.medium"
}
variable "keyname" {
type = string
default = "YOUR_KEY_NAME"
}
variable "vpc_cidr" {
type = string
default = "VPC_CIDR"
}
variable "public_subnets" {
type = list(any)
default = ["subnet-XX1", "subnet-XX2", "subnet-XX3"]
}
variable "private_subnets" {
type = list(any)
default = ["subnet-XX1", "subnet-XX2", "subnet-XX3"]
}
variable "azs" {
type = list(any)
default = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
}
variable "vpc_id" {
type = string
default = "vpc-XX"
}
variable "alb_sg" {
default = "sg-XX"
}
variable "instance_name" {
default = "jenkins"
}
The variables.tf file is where you define the input variables for your terraform configuration. These variables allow you to customize and reuse your code for different environments and scenarios. Here is a small explanation of each variable in your file:
ami
: This variable specifies the Amazon Machine Image (AMI) ID that you want to use for your EC2 instance. You can find the AMI ID of Ubuntu on the [AWS Marketplace] or use the [aws_ami data source] to dynamically fetch the latest AMI ID.infra_env
: This variable defines the name of your infrastructure environment, such as dev, test, or prod. You can use this variable to tag your resources and differentiate them from other environments.instance_type
: This variable determines the type of EC2 instance that you want to launch. You can choose from a variety of instance types that offer different combinations of CPU, memory, storage, and networking capacity. You can find more information about the instance types on the [AWS documentation].keyname
: This variable specifies the name of the key pair that you want to use for SSH access to your EC2 instance. You can create a key pair on the [AWS console] or use the [aws_key_pair resource] to generate one with terraform.vpc_cidr
: This variable defines the CIDR block for your Virtual Private Cloud (VPC), which is a logical network that isolates your AWS resources from the public internet. You can use any valid IPv4 CIDR block that does not overlap with other networks in your account or region.public_subnets
: This variable is a list of subnet IDs that are associated with your VPC and have a route to an internet gateway. These subnets allow your EC2 instance to communicate with the internet and receive traffic from the Application Load Balancer (ALB).private_subnets
: This variable is a list of subnet IDs that are associated with your VPC and do not have a route to an internet gateway. These subnets are used for internal communication between your EC2 instance and other AWS services, such as RDS or S3.azs
: This variable is a list of availability zones (AZs) that are within your region and support your chosen instance type. AZs are physically isolated locations that provide high availability and fault tolerance for your resources. You can use the [aws_availability_zones data source] to get a list of all AZs in your region.vpc_id
: This variable specifies the ID of your VPC. You can use the [aws_vpc data source] to get the ID of an existing VPC or use the [aws_vpc resource] to create a new one with terraform.alb_sg
: This variable specifies the security group ID that is attached to your ALB. A security group is a set of rules that control the inbound and outbound traffic for your resources. You can use the [aws_security_group data source] to get the ID of an existing security group or use the [aws_security_group resource] to create a new one with terraform.instance_name
: This variable defines the name of your EC2 instance. You can use this variable to tag your instance and identify it easily on the AWS console or CLI.
Step 2: Create the security group file
The second step is to create the security-group.tf file, which defines the security group for your EC2 instance. A security group is a set of rules that control the inbound and outbound traffic for your resources.
resource "aws_security_group" "jenkins_sg" {
name = "${var.instance_name}-sg"
description = "Needed rules."
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_groups = ["sg-XX"]
self = true
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
vpc_id = var.vpc_id
tags = {
Name = "${var.instance_name}-SG"
}
}
Here is a small explanation of each block in your file:
resource "aws_security_group" "jenkins_sg"
: This block creates a new security group resource with the name${var.instance_name}-sg
and the descriptionNeeded rules.
. The name and description are optional, but they help you identify your resource easily. Thejenkins_sg
is a local name that you can use to reference this resource in other parts of your configuration.ingress
: This block specifies the rules for the incoming traffic to your EC2 instance. You can have multiple ingress blocks for different types of traffic. In this example, you have two ingress blocks:- The first one allows SSH access (port 22) from any IP address (0.0.0.0/0) using the TCP protocol. This is useful for debugging and troubleshooting purposes, but you may want to restrict the access to a specific IP range or security group for security reasons.
- The second one allows HTTP access (port 8080) from any IP address (0.0.0.0/0) or from the security group with the ID
sg-XX
using the TCP protocol. Theself
attribute is set to true, which means that this rule also applies to the traffic within the same security group. This is necessary for the ALB to communicate with your EC2 instance. egress
: This block specifies the rules for the outgoing traffic from your EC2 instance. You can have multiple egress blocks for different types of traffic. In this example, you have one egress block that allows all traffic (port 0) to any IP address (0.0.0.0/0) using any protocol (-1). This means that your EC2 instance can access any external service or resource on the internet or within your VPC.vpc_id
: This attribute sets the ID of the VPC that your security group belongs to. You can use the variablevar.vpc_id
that you defined in the variables.tf file.tags
: This block allows you to add key-value pairs of metadata to your security group resource. Tags are useful for organizing, filtering, and searching your resources on the AWS console or CLI. In this example, you have one tag with the keyName
and the value${var.instance_name}-SG
.
Step 3: Create the userdata bash script
In this step, you will define the userdata script, which will run when the Terraform is applied. Let's create userdata.sh file:
#!/bin/bash
#########################################################################
## Description: Automate the installation of Jenkins as container.
## Author: Omar XS [Github]
## Date: Sep 2nd 2023
#########################################################################
## Update && upgrade && install docker
apt update
apt -y upgrade
apt update
apt -y install docker.io docker-compose
usermod -aG docker ubuntu
cat > /home/ubuntu/docker-compose.yaml << EOF
version: '2'
services:
jenkins:
image: jenkins/jenkins:lts
privileged: true
user: root
ports:
- 8080:8080
- 50000:50000
container_name: jenkins
volumes:
- ./jenkins_compose/jenkins_configuration:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
EOF
#! START_JENKINS !#
docker-compose -f /home/ubuntu/docker-compose.yaml up -d
The userdata.sh script is a bash script that automates the installation of Jenkins as a container on an EC2 instance. It first updates and upgrades the system packages, then installs docker and docker-compose. It then creates a docker-compose file that defines a Jenkins service with the necessary configuration options, such as ports, volumes, and privileges. Finally, it runs the docker-compose command to start the Jenkins container in the background.
Step 4: Create the main terraform file
The last step is to create the main.tf file, which defines the main terraform configuration for your project. This file will create an EC2 instance with an elastic IP that can be used to access Jenkins web interface.
provider "aws" {
region = "eu-central-1"
default_tags {
tags = {
Terraform = "true"
Environment = "${var.infra_env}"
}
}
}
resource "aws_instance" "jenkins" {
ami = var.ami # Ubuntu
instance_type = var.instace_type
key_name = var.keyname
root_block_device {
volume_size = 25
}
vpc_security_group_ids = ["${aws_security_group.jenkins_sg.id}", "${var.alb_sg}"]
subnet_id = var.public_subnets[0]
tags = {
Name = "${var.instance_name}"
}
user_data = file("userdata.sh")
}
resource "aws_eip" "jenkins_elasticIP" {
vpc = true
instance = aws_instance.jenkins.id
tags = {
Name = "${var.instance_name}-ElasticIP"
}
}
output "jenkins_static_ip" {
value = aws_eip.jenkins_elasticIP.public_ip
}
output "msg" {
value = "Please wait 5-10 mintues until ${var.instance_name} is up and running."
depends_on = [
aws_instance.jenkins
]
}
The main.tf file creates an EC2 instance with an elastic IP using terraform. It uses the AWS provider with the region eu-central-1
and the variables from the variables.tf file. It also uses the security group from the security-group.tf file and the user data script from the userdata.sh file. It outputs the static IP of the instance and a message to wait until Jenkins is ready.
Conclusion
In this article, you have learned to automate the installation of Jenkins on an AWS EC2 instance using terraform.
You have created a terraform project with four files: variables.tf, security-group.tf, userdata.sh, and main.tf. I have shared the files on my GitHub repo as well.
You have used variables to customize our configuration, a security group to control the traffic to our instance, a user data script to install Jenkins as a container using docker and docker-compose, and a main file to create the EC2 instance and an elastic IP. You have also seen how to access Jenkins web interface using the static IP and port 8080.
However, using HTTP is not secure and exposes our Jenkins server to potential attacks. Therefore, you should use HTTPS to encrypt the communication between our browser and our server. There are two ways to achieve this:
- One way is to use an Application Load Balancer (ALB) in front of our EC2 instance and configure it to use HTTPS with a certificate from AWS Certificate Manager (ACM). This way, the ALB will handle the SSL termination and forward the traffic to our instance using HTTP.
- Another way is to install nginx on the same EC2 instance and use it as a reverse proxy to route the traffic to Jenkins. You also need to use certbot to obtain a free certificate from Let's Encrypt and configure nginx to use HTTPS with it. This way, nginx will handle the SSL termination and forward the traffic to Jenkins using HTTP.
I hope you find this tutorial helpful.