🚀 A Basic Guide to Automate Deployment on OpenStack (Part 1)

🚀 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:

  • Running Jenkins, our CI/CD orchestrator

You’ll see how:

  • Terraform can automate everything from network setup to security groups and VMs
  • Ansible can configure those machines with the tools they need — all in one go
  • This forms the backbone of a full CI/CD pipeline, ready for expansion

🔧 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:

  • openstack to interact with the cloud infrastructure
  • null for post-creation checks (via SSH)
  • region is passed via a variable and typically comes from the OpenStack environment

🔐 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:

  • Port 22 (SSH)
  • Port 80 (HTTP)
  • Port 443 (HTTPS)
  • Port 8080 (Jenkins web interface)


🌐 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:

  • 100GB SSD volume booted from the image
  • Uses the security group we defined
  • Attached to the internal network

🌍 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

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:

  • The public IP of your Jenkins server
  • The SSH command to access it immediately

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:

  1. Update and upgrade the OS
  2. Install Java 21
  3. Add the Jenkins repository and key
  4. Install Jenkins
  5. Ensure the service is up and running


📄 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:

  • Connect via SSH
  • Set up the full Jenkins stack
  • Ensure the service is up and running on port 8080

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:


Article content


And the infrastructure, should be something like this:

Article content

✅ Wrapping Up – Jenkins Infrastructure Ready

At this point, we’ve:

  • Used Terraform to provision a full OpenStack environment for Jenkins
  • Created networks, routers, floating IPs, and security groups
  • Launched a virtual machine with a 100GB boot volume
  • Used Ansible to install Jenkins, Java 21, and set it up to run automatically

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:

  • Provision a second VM running NGINX
  • Deploy a simple static site from GitHub
  • Use Jenkins pipelines to automatically update the web server when code changes — completing the CI/CD loop

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! 🚀

Like
Reply

Agradeço por compartilhar isso, Gabriel

To view or add a comment, sign in

More articles by Gabriel Souza

Others also viewed

Explore content categories