🚀 A Basic Guide to Automate Deployment on OpenStack (Part 1)
Whether you're just getting started with cloud automation or looking to sharpen your DevOps skills, this guide will walk you through the fundamentals of automating infrastructure and application deployment using Terraform, Ansible, and OpenStack.
In this first part, we’ll focus on provisioning the cloud infrastructure and setting up one virtual machine:
You’ll see how:
🔧 Tools Used: Terraform + Ansible + OpenStack 🧠 Skills Covered: Infrastructure as Code, Configuration Management, Automation Best Practices
Let’s get started by provisioning the infrastructure!
✅ Assumption: This guide assumes you’ve already authenticated with your OpenStack environment using a sourced *.openrc file or similar, so environment variables like OS_AUTH_URL, OS_USERNAME, OS_PROJECT_NAME, etc., are already available to Terraform.
📁 Project Folder Structure
We’re organizing the automation into two isolated environments: one for Jenkins (the CI/CD tool) and another for the web server. Each has its own Terraform and Ansible configuration to keep things modular and easy to maintain.
Terraform/
├── Jenkins/ # Jenkins infrastructure and Ansible config
│ ├── main.tf # Terraform definition for Jenkins server
│ ├── variables.tf # Input variables
│ ├── terraform.tfvars # Concrete values for variables
│ └── install_jenkins.yml # Ansible playbook to install Jenkins
├── WebServer/ # Web server infrastructure and Ansible config
│ ├── main.tf # Terraform definition for NGINX web server
│ ├── variables.tf # Input variables
│ ├── terraform.tfvars # Values (image name, flavor, etc.)
│ └── install_nginx.yml # Ansible playbook to install NGINX and deploy site
└── shared/ # (Optional) Shared files like SSH keys, templates, etc.
└── pocssh.pem # SSH key used for both instances
🔨 Jenkins Server Provisioning with Terraform
Let’s begin by provisioning our Jenkins server. This section uses Terraform to automate everything: security groups, private networking, floating IPs, and the virtual machine itself — all hosted on OpenStack.
Below is a breakdown of the main.tf file located in Terraform/POClinkedin/.
✅ Required Providers
terraform {
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 1.49.0"
}
null = {
source = "hashicorp/null"
}
}
}
provider "openstack" {
region = var.os_region
}
This block sets up the required providers. We’re using:
🔐 Security Group and Rules for Jenkins
resource "openstack_networking_secgroup_v2" "jenkins_sg" {
name = "jenkins-secgroup"
description = "Allow SSH, HTTP, HTTPS and 8080"
}
This creates a dedicated security group for the Jenkins VM. Next, we define specific inbound rules:
resource "openstack_networking_secgroup_rule_v2" "allow_ssh" {
security_group_id = openstack_networking_secgroup_v2.jenkins_sg.id
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
}
resource "openstack_networking_secgroup_rule_v2" "allow_https" {
security_group_id = openstack_networking_secgroup_v2.jenkins_sg.id
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
}
resource "openstack_networking_secgroup_rule_v2" "allow_http" {
security_group_id = openstack_networking_secgroup_v2.jenkins_sg.id
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
}
resource "openstack_networking_secgroup_rule_v2" "allow_8080" {
security_group_id = openstack_networking_secgroup_v2.jenkins_sg.id
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 8080
port_range_max = 8080
remote_ip_prefix = "0.0.0.0/0"
}
These rules allow access to:
🌐 Private Network & Router
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private_net" {
name = "private-net"
}
Here we define a new private network and fetch the external one from OpenStack using a data source.
resource "openstack_networking_subnet_v2" "private_subnet" {
name = "private-subnet"
network_id = openstack_networking_network_v2.private_net.id
cidr = "192.168.100.0/24"
gateway_ip = "192.168.100.1"
dns_nameservers = ["8.8.8.8", "1.1.1.1"]
ip_version = 4
}
This subnet will be used by the VM. It assigns private IPs and routes traffic through the following router:
resource "openstack_networking_router_v2" "router" {
name = "jenkins-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "router_interface" {
router_id = openstack_networking_router_v2.router.id
subnet_id = openstack_networking_subnet_v2.private_subnet.id
}
🖥️ Jenkins VM Creation
data "openstack_images_image_v2" "jenkins_image" {
name = var.image_name
}
This fetches the image we'll use to boot the instance.
resource "openstack_compute_instance_v2" "jenkins_vm" {
name = "jenkins-vm"
flavor_name = var.flavor_name
key_pair = var.keypair_name
security_groups = [openstack_networking_secgroup_v2.jenkins_sg.name]
network {
uuid = openstack_networking_network_v2.private_net.id
}
block_device {
uuid = data.openstack_images_image_v2.jenkins_image.id
source_type = "image"
destination_type = "volume"
volume_size = 100
boot_index = 0
delete_on_termination = true
}
}
Here we define the VM:
🌍 Floating IP & SSH Health Check
resource "openstack_networking_floatingip_v2" "fip" {
pool = var.external_network
}
resource "openstack_compute_floatingip_associate_v2" "fip_assoc" {
instance_id = openstack_compute_instance_v2.jenkins_vm.id
floating_ip = openstack_networking_floatingip_v2.fip.address
}
This block allocates and attaches a floating IP to the instance so you can access it publicly.
resource "null_resource" "check_instance_ready" {
depends_on = [openstack_compute_floatingip_associate_v2.fip_assoc]
connection {
type = "ssh"
host = openstack_networking_floatingip_v2.fip.address
user = "ubuntu"
private_key = file(var.private_key_path)
}
provisioner "remote-exec" {
inline = [
"echo VM is ready",
"uptime"
]
}
}
This waits for the VM to become reachable via SSH and executes a simple uptime command to confirm.
🖥️ Output IP and SSH Access
output "floating_ip" {
value = openstack_networking_floatingip_v2.fip.address
}
output "ssh_command" {
value = "ssh -i ${var.private_key_path} ubuntu@${openstack_networking_floatingip_v2.fip.address}"
}
🧩 Defining Variables for Jenkins
To keep the Terraform code clean and reusable, we define all external values in a separate file: variables.tf. These include image names, flavors, keypair, and paths.
📄 variables.tf
Recommended by LinkedIn
variable "os_region" {
default = "RegionOne"
}
variable "image_name" {
default = "Ubuntu-24.04-amd64"
}
variable "flavor_name" {
default = "4_6"
}
variable "keypair_name" {
description = "OpenStack keypair name"
default = "pocssh"
}
variable "external_network" {
description = "External network name for floating IP"
default = "Externa"
}
variable "private_key_path" {
description = "Path to private SSH key used with keypair"
default = "/home/gabriel/Terraform/Shared/pocssh.pem"
}
All values can be overridden via terraform.tfvars or CLI arguments, but keeping them in one file helps you version-control and reuse configs easily.
🧪 Before You Apply: Terraform Basics
Once your Terraform files are ready, it’s time to initialize and deploy.
Here are the three essential commands to run from inside the Terraform/Jenkins/ directory:
terraform init
🔧 Initializes the working directory and downloads provider plugins.
terraform plan
🔍 Shows a preview of what will be created, changed, or destroyed. Always run this before applying.
terraform apply
🚀 Executes the plan and provisions your infrastructure. Confirm with yes when prompted.
Once terraform apply completes, you’ll see two helpful outputs:
Like this:
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
Outputs:
floating_ip = "ExternalIP"
ssh_command = "ssh -i /home/gabriel/Terraform/Shared/pocssh.pem ubuntu@ExternalIP"
Command took 44 seconds
~/Terraform/Jenkins [23-04 10:06] $
✅ Tip: You can run terraform output ssh_command anytime to reprint the access command.
🤖 Configuring Jenkins with Ansible
Now that our Jenkins VM is provisioned and accessible via SSH, it’s time to automate the configuration using Ansible.
The playbook will:
📄 install_jenkins.yml
- name: Install Jenkins on Ubuntu Server
hosts: jenkins
become: yes
vars:
java_version: 21
jenkins_repo_url: https://pkg.jenkins.io/debian-stable
jenkins_repo_key: https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
tasks:
- name: Update APT cache
apt:
update_cache: yes
- name: Upgrade all packages
apt:
upgrade: dist
- name: Reboot the server if required
reboot:
msg: "Reboot initiated by Ansible after system upgrade"
connect_timeout: 5
reboot_timeout: 600
when: ansible_facts['os_family'] == 'Debian'
- name: Install Java {{ java_version }}
apt:
name: openjdk-{{ java_version }}-jre
state: present
- name: Add Jenkins GPG key
get_url:
url: "{{ jenkins_repo_key }}"
dest: /usr/share/keyrings/jenkins-keyring.asc
- name: Add Jenkins APT repository
copy:
content: |
deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] {{ jenkins_repo_url }} binary/
dest: /etc/apt/sources.list.d/jenkins.list
- name: Update APT cache again (for Jenkins)
apt:
update_cache: yes
- name: Install Jenkins
apt:
name: jenkins
state: present
- name: Start and enable Jenkins service
systemd:
name: jenkins
state: started
enabled: yes
📄 inventory.ini
Make sure you have an inventory file like this in the same directory (POClinkedin/ansible/inventory.ini):
[jenkins]
your.jenkins.ip.address ansible_user=ubuntu ansible_ssh_private_key_file=../pocssh.pem
Replace your.jenkins.ip.address with the floating IP printed by Terraform.
▶️ Run the Playbook
From the ansible directory, run:
ansible-playbook -i inventory.ini install_jenkins.yml
This will:
You should see something like this:
~/Terraform/Jenkins $ ansible-playbook -i inventory.ini install_jenkins.yaml
PLAY [Install Jenkins with Java 21 on Ubuntu] ***********************************************************************************************************
TASK [Gathering Facts] **********************************************************************************************************************************
[WARNING]: Platform linux is using the discovered Python interpreter at /usr/bin/python3.12, but future installations may affect this path.
ok: [jenkins-vm]
TASK [Update APT cache] *********************************************************************************************************************************
changed: [jenkins-vm]
TASK [Upgrade all packages] *****************************************************************************************************************************
changed: [jenkins-vm]
TASK [Reboot if required] *******************************************************************************************************************************
changed: [jenkins-vm]
TASK [Install OpenJDK 21] *******************************************************************************************************************************
changed: [jenkins-vm]
TASK [Set JAVA_HOME to JDK 21 in Jenkins config] ********************************************************************************************************
changed: [jenkins-vm]
TASK [Download Jenkins GPG key] *************************************************************************************************************************
changed: [jenkins-vm]
TASK [Add Jenkins apt repository (signed-by method)] ****************************************************************************************************
changed: [jenkins-vm]
TASK [Update APT cache after adding Jenkins repo] *******************************************************************************************************
changed: [jenkins-vm]
TASK [Install Jenkins] **********************************************************************************************************************************
changed: [jenkins-vm]
TASK [Ensure Jenkins is running] ************************************************************************************************************************
ok: [jenkins-vm]
TASK [Allow port 8080 (optional)] ***********************************************************************************************************************
changed: [jenkins-vm]
RUNNING HANDLER [Restart Jenkins] ***********************************************************************************************************************
changed: [jenkins-vm]
PLAY RECAP **********************************************************************************************************************************************
jenkins-vm : ok=13 changed=11 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Command took 353 seconds
After that, the VM with jenkins should be up and running, heres an example:
And the infrastructure, should be something like this:
✅ Wrapping Up – Jenkins Infrastructure Ready
At this point, we’ve:
Jenkins is now up and running on port 8080, ready to become the heart of our CI/CD pipeline.
🔜 Coming Next: CI/CD with GitHub and a Web Server
In Part 2, we’ll:
This next phase will show how infrastructure, configuration management, and CI/CD can work together seamlessly in a real-world scenario.
Thank you for this detailed guide! The step-by-step approach to automating infrastructure with Terraform and Ansible is incredibly clear. The project structure and variable management make it easy to follow and scalable. Excited to apply these principles to future projects! 🚀
Agradeço por compartilhar isso, Gabriel
Great content!
Great post!
Excellent work!!!