A bash script that fully automates the deployment of a two-VM KVM infrastructure using libvirt, virt-install, and cloud-init. A single script run provisions three virtual networks, creates and boots both VMs with correct network configurations, sets up a VxLAN overlay tunnel between them, installs Docker on both machines, and deploys an NGINX reverse proxy (with TLS termination) on vm1 forwarding traffic to an Apache container on vm2.
This task builds on:
- task10_12 — infrastructure creation and container deployment
- task6_7 — NGINX reverse proxy with SSL/TLS termination
Three libvirt networks are created from XML templates:
| Network | Type | DHCP | Notes |
|---|---|---|---|
external |
NAT | Yes | Single static DHCP entry for vm1's MAC address |
internal |
Isolated | No | No IP addresses assigned; used only for VM-to-VM L2 connectivity |
management |
Isolated | No | Host IP assigned to provide L3 connectivity between host and VMs for SSH access |
| VM | Interfaces | Role |
|---|---|---|
| vm1 | external · internal · management |
NGINX reverse proxy, NAT gateway for vm2 |
| vm2 | internal · management |
Apache backend |
Both VMs are configured at first boot via cloud-init ISO drives:
vm1
- Hostname set to
vm1 - Public SSH key injected
externalinterface: DHCPinternalinterface: static IPmanagementinterface: static IP- IP forwarding + iptables NAT so that vm2's traffic reaches the internet through vm1's external interface
- VxLAN tunnel to vm2 created on boot
docker-ceinstalled from the official repository- NGINX container (
nginx:1.13) started via Docker Compose with mountednginx.conf, TLS certificates, and log directory
vm2
- Hostname set to
vm2 - Public SSH key injected
internalinterface: static IP; default gateway = vm1's internal IP; DNS =8.8.8.8managementinterface: static IP- VxLAN tunnel to vm1 created on boot
docker-ceinstalled from the official repository- Apache container (
httpd:2.4) started via Docker Compose, listening on the VxLAN interface
Entry point — run as root from the repository directory. Performs the following in order:
- Sources
configand creates all required working directories - Generates an RSA SSH key pair at the path specified by
SSH_PUB_KEY - Generates an OpenSSL SAN config, a root CA (
root-ca.crt/root.crt), and a signed server certificate (web.crt/web.key) for vm1's external IP - Generates
docker/etc/nginx.confpointing to vm2's VxLAN IP and the configured Apache port - Generates Docker Compose files for both vm1 (NGINX) and vm2 (Apache)
- Generates
user-dataandmeta-datacloud-init files for both VMs - Copies Docker assets into the vm1 config-drive staging directory
- Builds ISO config drives for both VMs with
mkisofs - Creates and starts the three libvirt networks from generated XML files
- Downloads the Ubuntu 16.04 base image, copies and resizes it for each VM
- Provisions vm1 with
virt-install, waits 5 minutes for cloud-init to complete - Provisions vm2 with
virt-install
Teardown script — destroys both VMs and all three networks, then removes their disk image directories so the environment can be rebuilt from scratch:
bash down.shThe following hierarchy must exist after task10_12_3.sh completes:
WORKDIR
├── config # configurable parameters file
├── task10_12_3.sh # main script (entry point)
├── config-drives
│ ├── vm1-config
│ │ ├── meta-data # vm1 cloud-init meta-data
│ │ └── user-data # vm1 cloud-init user-data
│ └── vm2-config
│ ├── meta-data # vm2 cloud-init meta-data
│ └── user-data # vm2 cloud-init user-data
├── docker
│ ├── etc
│ │ └── nginx.conf # NGINX configuration file
│ └── certs
│ ├── root.crt # root CA certificate
│ ├── web.crt # NGINX server certificate (chain)
│ └── web.key # NGINX private key
└── networks
├── external.xml # external network XML definition
├── internal.xml # internal network XML definition
└── management.xml # management network XML definition
Both script-generated and pre-committed files are acceptable. Additional files and directories are permitted.
All deployment parameters are read from the config file in the repository root. Example with default values:
# Libvirt networks
EXTERNAL_NET_NAME=external
EXTERNAL_NET_TYPE=dhcp
EXTERNAL_NET=192.168.123
EXTERNAL_NET_IP=${EXTERNAL_NET}.0
EXTERNAL_NET_MASK=255.255.255.0
EXTERNAL_NET_HOST_IP=${EXTERNAL_NET}.1
VM1_EXTERNAL_IP=${EXTERNAL_NET}.101
INTERNAL_NET_NAME=internal
INTERNAL_NET=192.168.124
INTERNAL_NET_IP=${INTERNAL_NET}.0
INTERNAL_NET_MASK=255.255.255.0
MANAGEMENT_NET_NAME=management
MANAGEMENT_NET=192.168.125
MANAGEMENT_NET_IP=${MANAGEMENT_NET}.0
MANAGEMENT_NET_MASK=255.255.255.0
MANAGEMENT_HOST_IP=${MANAGEMENT_NET}.1
# VMs global parameters
SSH_PUB_KEY=/home/jenkins/.ssh/id_rsa.pub
VM_TYPE=hvm
VM_VIRT_TYPE=kvm
VM_DNS=8.8.8.8
VM_BASE_IMAGE=https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img
# VxLAN overlay
VXLAN_NET=10.255.0
VID=12345
VXLAN_IF=vxlan0
# vm1
VM1_NAME=vm1
VM1_NUM_CPU=1
VM1_MB_RAM=512
VM1_HDD=/var/lib/libvirt/images/vm1/vm1.qcow2
VM1_CONFIG_ISO=/var/lib/libvirt/images/vm1/config-vm1.iso
VM1_EXTERNAL_IF=ens3
VM1_INTERNAL_IF=ens4
VM1_MANAGEMENT_IF=ens5
VM1_INTERNAL_IP=${INTERNAL_NET}.101
VM1_MANAGEMENT_IP=${MANAGEMENT_NET}.101
VM1_VXLAN_IP=${VXLAN_NET}.101
# vm2
VM2_NAME=vm2
VM2_NUM_CPU=1
VM2_MB_RAM=512
VM2_HDD=/var/lib/libvirt/images/vm2/vm2.qcow2
VM2_CONFIG_ISO=/var/lib/libvirt/images/vm2/config-vm2.iso
VM2_INTERNAL_IF=ens3
VM2_MANAGEMENT_IF=ens4
VM2_INTERNAL_IP=${INTERNAL_NET}.102
VM2_MANAGEMENT_IP=${MANAGEMENT_NET}.102
VM2_VXLAN_IP=${VXLAN_NET}.102
# Docker containers
NGINX_IMAGE="nginx:1.13"
APACHE_IMAGE="httpd:2.4"
NGINX_PORT=17080
APACHE_PORT=13254
NGINX_LOG_DIR=/srv/log/nginx- Create libvirt networks from XML using
virsh net-definefollowed byvirsh net-start. - Use
virt-installto provision both VMs. - A unique MAC address is required for the DHCP static entry. Generate one with:
MAC=52:54:00:`(date; cat /proc/interrupts) | md5sum | sed -r 's/^(.{6}).*$/\1/; s/([0-9a-f]{2})/\1:/g; s/:$//;'` - The base image URL is read from
config(VM_BASE_IMAGE) — do not hardcode it. - Resize the VM disk after copying the base image with
qemu-img resize. - Use the
meta-datafile to configure hostname, SSH key, and network interfaces. - Use the
user-datafile for VxLAN tunnel creation, Docker installation, and container deployment. - Build the cloud-init ISO with
mkisofs -V cidata -r -J. - Refer to task6_7 and task10_12_2 for the apache2+nginx configuration.
- A Docker Compose file is used here for convenience but is not strictly required by the assignment.
Prerequisites: Ubuntu 16.04 host (bare-metal or VM) with qemu-kvm, libvirt-bin, virtinst, bridge-utils, and genisoimage installed. All commands run as root.
# 1. Clone the repository
git clone https://github.com/<your-username>/task10_12_3
cd task10_12_3
# 2. Adjust config if needed (e.g. update SSH_PUB_KEY path)
vi config
# 3. Run the deployment script (~10 minutes total)
bash task10_12_3.shThe script prints ###### ALL DONE ###### when finished.
# Source config to get variables
source ./config
# Test the NGINX reverse proxy — response must contain "It works!"
curl --cacert docker/certs/root.crt https://${VM1_EXTERNAL_IP}:${NGINX_PORT}virsh list # vm1 and vm2 must appear as "running"
virsh net-list # external, internal, management must be "active"source ./config
ssh -i $(echo ${SSH_PUB_KEY} | sed 's/\.pub//') ubuntu@${VM1_MANAGEMENT_IP}Inside vm1:
docker ps # nginx container must be running
ip addr show vxlan0 # VxLAN interface with IP 10.255.0.101
ping 10.255.0.102 -c 3 # VxLAN connectivity to vm2
curl http://10.255.0.102:${APACHE_PORT} # Apache page via VxLAN (run after sourcing config)source ./config
ssh -i $(echo ${SSH_PUB_KEY} | sed 's/\.pub//') ubuntu@${VM2_MANAGEMENT_IP}Inside vm2:
docker ps # apache container must be running
curl http://example.com # external access — routed exclusively through vm1bash down.shThis destroys both VMs and all three networks and removes their disk image directories.
| Check | Command (run from project root) | Expected result |
|---|---|---|
| VMs are running | virsh list |
vm1 and vm2 listed as running |
| Networks are active | virsh net-list |
external, internal, management listed as active |
| HTTPS endpoint responds | curl --cacert docker/certs/root.crt https://<VM1_EXTERNAL_IP>:<NGINX_PORT> |
Response body contains It works! |
| vm1 SSH access | ssh ubuntu@<VM1_MANAGEMENT_IP> with configured key |
Shell prompt |
| vm2 SSH access | ssh ubuntu@<VM2_MANAGEMENT_IP> with configured key |
Shell prompt |
| VxLAN tunnel on vm1 | ip addr show vxlan0 (inside vm1) |
IP 10.255.0.101/24 assigned |
| VxLAN tunnel on vm2 | ip addr show vxlan0 (inside vm2) |
IP 10.255.0.102/24 assigned |
| VxLAN connectivity | ping 10.255.0.102 -c 3 (inside vm1) |
0% packet loss |
| docker-ce on vm1 | docker --version (inside vm1) |
Version string printed |
| docker-ce on vm2 | docker --version (inside vm2) |
Version string printed |
| NGINX container on vm1 | docker ps (inside vm1) |
Container from nginx:1.13 running |
| Apache container on vm2 | docker ps (inside vm2) |
Container from httpd:2.4 running |
| NGINX volume mounts | docker inspect <nginx-container> (inside vm1) |
nginx.conf, certs/, log dir all mounted |
| NGINX access log | cat /srv/log/nginx/access.log (inside vm1) |
Entries present after HTTPS request |
| vm2 external routing | curl http://example.com (inside vm2) |
Response received (via vm1 NAT) |
- OS: Ubuntu Xenial 16.04 Server (
xenial-server-cloudimg-amd64-disk1.img) - User:
root - Pre-installed packages:
qemu-kvm,libvirt-bin,virtinst,bridge-utils,genisoimage
- The repository is cloned by name — the repository must be named
task10_12_3. task10_12_3.shis launched from the repository root — a different script name or location results in automatic failure.- The
configfile must be present in the repository root and all parameters read from it correctly.
Network and VM presence
- libvirt networks
external,internal, andmanagementexist and are active - VMs
vm1andvm2exist and are connected to the correct networks
Network interface configuration (verified via SSH over Management network, login ubuntu)
- All interfaces have the correct static or DHCP-assigned IP addresses
- Both VMs have access to the external network
- The VxLAN tunnel is configured and reachable between vm1 and vm2
Docker
docker-cepackage is installed on both VMs- NGINX container (image
nginx:1.13) is running on vm1 - Apache container (image
httpd:2.4) is running on vm2 - NGINX container has correct volume mounts:
nginx.conf, certificates directory, log directory /srv/log/nginx/access.logcontains entries on the Docker host (vm1)
Required files and directories
networks/directory with all three XML filesconfig-drives/directory withvm1-config/andvm2-config/subdirectoriesdocker/etc/nginx.confdocker/certs/directory
End-to-end HTTPS check
An HTTPS request is made to https://<VM1_EXTERNAL_IP>:<NGINX_PORT> using docker/certs/root.crt as the trusted root. The connection must succeed and the response body must contain the Apache2 default page (It works!).
- The completed assignment must be in a public GitHub repository named
task10_12_3(e.g.https://github.com/<username>/task10_12_3). - The grader VM is a freshly installed Ubuntu 16.04 image from
https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img. - Pre-installed packages:
qemu-kvm,libvirt-bin,virtinst,bridge-utils,genisoimage. - Any additional packages required must be installed by the script itself.
- The script is run as
root. - Deadline: 23:59 on 13/05/2018.
