🏗️ OpenStack in One Page: What It Is and How the Services Fit Together

OpenStack is software you install to build your own cloud. This page shows what the pieces are, how they talk to each other, and where the state lives, using a real test cloud and a real instance followed through its logs

💡 How this page was made. Everything shown as output was run for real on 8 October 2026 on a test cloud built with DevStack (branch stable/2026.1) in a throwaway virtual machine: Ubuntu 24.04, 4 CPUs, 8 GB of RAM, with nested KVM. Release names, dates and what each project is for come from the OpenStack documentation, read the same day (releases.openstack.org, docs.openstack.org). The comparisons with AWS and Azure are my own rough ones, not from the docs. The next page builds this test cloud step by step.

What OpenStack is, and what it is not

AWS, Azure, Google Cloud and Oracle Cloud are services you rent. OpenStack is software you install. It turns a rack of ordinary servers into a private cloud: you log in (or call an API), ask for a virtual machine, a network, a disk, and the software finds a server with room, builds it, wires it up and hands it to you. It is the same idea as the big clouds, with you running the datacentre, the upgrades and the pager.

That makes it infrastructure as a service (IaaS): machines, networks and storage. It is not:

⚠️ Be honest about the cost. The software is free. Running it is a job: upgrades every six months, a database and a message queue to keep healthy, networking that has to be designed, and users who will break things in new ways. For a handful of VMs on one server, a simpler tool is usually the right answer. OpenStack pays off when many people need to create and destroy infrastructure on their own.

Releases and names

From releases.openstack.org (read 8 October 2026): OpenStack is "developed and released around 6-month cycles". Each release has an alphabetical code name and, since 2023, a year-based version number:

Some releases are marked SLURP (Skip Level Upgrade Release Process): you may upgrade from one SLURP release straight to the next, skipping the one between. For a cloud you cannot afford to rebuild, that matters more than the names.

The versions you actually see are per project. On the test cloud:

nova 33.0.3.dev9   neutron 28.0.3.dev30   keystone 29.1.0   glance 32.0.1.dev20
cinder 28.0.1.dev11   horizon 25.7.4.dev30   python-openstackclient 9.0.0

Each project has its own version number; the release (Gazpacho) is the set of them that were tested together. The .devN suffix means "this many commits after the release tag": DevStack takes the latest of the stable branch, not an exact tag.

The services at a glance

These roles are the ones the OpenStack documentation gives. The "like" in brackets is my rough comparison, only a hint for people who know another cloud: the services do not match one to one.

And the ones you add later, as the cloud grows:

The core set for a first cloud is Keystone, Nova, Placement, Glance, Neutron and Cinder, with Horizon as the face. That is exactly what DevStack installs by default (the DevStack docs list keystone, glance, nova, placement, cinder, neutron and horizon), and it is what this series uses.

The test cloud, as it really is

First, how a client finds anything. Keystone keeps a service catalogue: a list of services and their URLs. Every command starts by asking it.

$ openstack service list
+----------------------------------+-------------+----------------+
| ID                               | Name        | Type           |
+----------------------------------+-------------+----------------+
| 2cb2beba0e1343e2908633794a650428 | nova        | compute        |
| 371f8dbca9ba44158ae9e1f7af0b0886 | nova_legacy | compute_legacy |
| 6ec3213027bf41c48a39670494fab4dd | keystone    | identity       |
| b148c02452fb4238aa66161e9e3952fd | cinder      | block-storage  |
| c8d22d548eb34116a6be71d3fe5f9579 | glance      | image          |
| d71f9edeada244d2880e31744959aed9 | placement   | placement      |
| eb3373922b0e42b29ea7899d7cfd25ae | neutron     | network        |
+----------------------------------+-------------+----------------+
$ openstack endpoint list --interface public -c "Service Name" -c "Service Type" -c URL
+--------------+----------------+--------------------------------------------+
| Service Name | Service Type   | URL                                        |
+--------------+----------------+--------------------------------------------+
| cinder       | block-storage  | http://10.0.2.15/volume/v3                 |
| neutron      | network        | http://10.0.2.15/networking                |
| glance       | image          | http://10.0.2.15/image                     |
| keystone     | identity       | http://10.0.2.15/identity                  |
| placement    | placement      | http://10.0.2.15/placement                 |
| nova         | compute        | http://10.0.2.15/compute/v2.1              |
| nova_legacy  | compute_legacy | http://10.0.2.15/compute/v2/$(project_id)s |
+--------------+----------------+--------------------------------------------+

Seven services. Two things worth a look:

Behind that front door the services run as systemd units. DevStack names them devstack@...:

$ systemctl list-units "devstack@*" --no-legend | awk '{print $1, $3, $4}' | sed 's/.service//'
devstack@c-api active running
devstack@c-sch active running
devstack@c-vol active running
devstack@dstat active running
devstack@etcd active running
devstack@g-api active running
devstack@keystone active running
devstack@n-api-meta active running
devstack@n-api active running
devstack@n-cond-cell1 active running
devstack@n-cpu active running
devstack@n-novnc-cell1 active running
devstack@n-sch active running
devstack@n-super-cond active running
devstack@neutron-api active running
devstack@neutron-ovn-maintenance-worker active running
devstack@neutron-periodic-workers active running
devstack@neutron-rpc-server active running
devstack@placement-api active running
devstack@q-ovn-agent active running
$ systemctl list-units "devstack@*" --no-legend | wc -l
20
$ systemctl list-units --type=service --state=running | grep -E "apache2|mysql|rabbitmq|libvirt|ovn|memcached"
apache2  libvirtd  memcached  mysql  ovn-controller  ovn-controller-vtep  ovn-northd  ovn-ovsdb-server-nb  ovn-ovsdb-server-sb  rabbitmq-server

Twenty units, and the names tell you the story: n- is Nova (n-api, n-sch the scheduler, n-cpu the compute process, n-super-cond and n-cond-cell1 the conductors, n-novnc-cell1 the console proxy, n-api-meta the metadata service that instances ask for their settings), g-api Glance, c- Cinder (API, scheduler, volume), neutron-... and q-ovn-agent Neutron with its OVN networking, keystone, placement-api, and etcd for coordination. Underneath are the ordinary services: MySQL, RabbitMQ (the message queue), memcached, libvirt (which drives the hypervisor), Open vSwitch/OVN (the virtual network), and Apache.

Why the VM had to be 8 GB: after the install, with one tiny instance running, free reported 5.4 GB used. The biggest single process is the database (mysqld, about 690 MB), then the web workers, then the instance itself (qemu, 209 MB for a 256 MB guest), then Nova and Neutron processes at 160 to 180 MB each. A cloud is many small processes, not one big one.

Everything is a REST call

The openstack command is a client. It turns your command into HTTP requests to those URLs. You can watch it with --debug:

$ openstack --debug server list      (only the request lines, trimmed)
REQ: curl -g -i -X GET http://10.0.2.15/identity -H "Accept: application/json" -H "User-Agent: openstacksdk/4.10.0 keystoneauth1/5.13.1 ..."
Making authentication request to http://10.0.2.15/identity/v3/auth/tokens
REQ: curl -g -i -X GET http://10.0.2.15/image -H "Accept: application/json" ...
REQ: curl -g -i -X GET http://10.0.2.15/compute/v2.1 -H "Accept: application/json" ...
REQ: curl -g -i -X GET "http://10.0.2.15/compute/v2.1/servers/detail?flavor=None&image=None&name=None&..." ...
RESP BODY: {"servers": []}

Three steps, in order: (1) find the identity service and ask it to authenticate (/identity/v3/auth/tokens), (2) with the token and the catalogue it gets back, find the compute service, (3) call the compute API (GET /compute/v2.1/servers/detail). The answer is plain JSON ({"servers": []}: no instances yet).

Because it is just HTTP, you can do the same with curl:

$ openstack token issue -c expires -c project_id -c user_id
+------------+----------------------------------+
| Field      | Value                            |
+------------+----------------------------------+
| expires    | 2026-10-08T22:05:28+0000         |
| project_id | 2b380a2be5ad443e92cabeae7d9909b4 |
| user_id    | 8da6246745c84127a7d28b5c9810a1f6 |
+------------+----------------------------------+
$ TOKEN=$(openstack token issue -f value -c id)
$ curl -s -H "X-Auth-Token: $TOKEN" http://10.0.2.15/compute/v2.1/flavors | python3 -m json.tool | head -12
{
    "flavors": [
        {
            "id": "1",
            "name": "m1.tiny",
            "links": [
                {
                    "rel": "self",
                    "href": "http://10.0.2.15/compute/v2.1/flavors/1"
                },
                {
                    "rel": "bookmark",
$ curl -s -w "\nHTTP %{http_code}\n" http://10.0.2.15/compute/v2.1/flavors
{"error": {"code": 401, "title": "Unauthorized", "message": "The request you have made requires authentication."}}
HTTP 401
$ curl -s -H "X-Auth-Token: $TOKEN" http://10.0.2.15/image/v2/images | python3 -m json.tool | grep -E '"name"|"status"|"disk_format"|"size"'
            "name": "cirros-0.6.3-x86_64-disk",
            "disk_format": "qcow2",
            "size": 21692416,
            "status": "active",

A token is a string with an expiry date, tied to one user and one project. Send it in the X-Auth-Token header and the call works; leave it out and you get 401 Unauthorized. Every service validates the token against Keystone before it does anything. When a call fails, check the three things in this order: do I have a valid token, for the right project, with a role that allows this?

Who are you: users, projects and roles

Keystone's vocabulary is short:

$ openstack project list
+----------------------------------+--------------------+
| ID                               | Name               |
+----------------------------------+--------------------+
| 287f29e0beda4bebadcf93b156b68e44 | demo               |
| 2b380a2be5ad443e92cabeae7d9909b4 | admin              |
| a88278127f3b4c6c849839b457095734 | alt_demo           |
| d1c848b414e24933ac30fdeb6e6858c9 | invisible_to_admin |
| f9316cdb10da402a96f49f35809b3b74 | service            |
+----------------------------------+--------------------+
$ openstack role assignment list --names --user demo --project demo
+-------------+--------------+-------+--------------+--------+--------+-----------+
| Role        | User         | Group | Project      | Domain | System | Inherited |
+-------------+--------------+-------+--------------+--------+--------+-----------+
| anotherrole | demo@Default |       | demo@Default |        |        | False     |
| member      | demo@Default |       | demo@Default |        |        | False     |
+-------------+--------------+-------+--------------+--------+--------+-----------+

demo is a member of the demo project: it may create and delete things there, and by the project model does not see admin's resources (that is what projects are for; not tested here). The service project is where the services' own users live. Rights are always a role in a project, never just "a user".

Inside Nova: four kinds of process and a queue

Nova is where you can see the pattern that most of OpenStack follows. The Nova documentation describes the parts:

They do not call each other directly. They talk through a message queue (RabbitMQ here, through a library called oslo.messaging) using RPC. And one rule, from the docs: compute nodes cannot access the database directly. "Services running on the compute node proxy database requests over RPC to a central manager called the conductor." In a big cloud with thousands of compute nodes, that keeps the database safe from them.

$ openstack compute service list
+--------------------------------------+----------------+--------+----------+---------+-------+----------------------------+
| ID                                   | Binary         | Host   | Zone     | Status  | State | Updated At                 |
+--------------------------------------+----------------+--------+----------+---------+-------+----------------------------+
| 72156c93-1751-4a43-8f9e-d41cd2b0d9ce | nova-scheduler | cloud1 | internal | enabled | up    | 2026-10-08T21:08:58.000000 |
| 599f5860-4ac5-422a-bf25-a863d1f5a3bf | nova-conductor | cloud1 | internal | enabled | up    | 2026-10-08T21:09:03.000000 |
| d234e40f-be71-42dd-aebf-b51de43db27e | nova-conductor | cloud1 | internal | enabled | up    | 2026-10-08T21:09:24.000000 |
| 39ea8eda-99fe-4f3a-a551-1b8242d4bd7b | nova-compute   | cloud1 | nova     | enabled | up    | 2026-10-08T21:09:29.000000 |
+--------------------------------------+----------------+--------+----------+---------+-------+----------------------------+
$ openstack volume service list
+------------------+--------------------+------+---------+-------+----------------------------+---------+---------------+
| Binary           | Host               | Zone | Status  | State | Updated At                 | Cluster | Backend State |
+------------------+--------------------+------+---------+-------+----------------------------+---------+---------------+
| cinder-scheduler | cloud1             | nova | enabled | up    | 2026-10-08T21:09:26.000000 | None    | None          |
| cinder-volume    | cloud1@lvmdriver-1 | nova | enabled | up    | 2026-10-08T21:09:27.000000 | None    | None          |
+------------------+--------------------+------+---------+-------+----------------------------+---------+---------------+
$ openstack network agent list
+--------------------------------------+------------------------------+--------+-------------------+-------+-------+-------------------+
| ID                                   | Agent Type                   | Host   | Availability Zone | Alive | State | Binary            |
+--------------------------------------+------------------------------+--------+-------------------+-------+-------+-------------------+
| b20d8aa6-d887-4256-9d7d-d0d1c847024d | OVN Controller Gateway agent | cloud1 |                   | :-)   | UP    | ovn-controller    |
| ae1fa8db-c466-5cc9-87c7-cd8485e25a9a | OVN Neutron agent            | cloud1 |                   | :-)   | UP    | neutron-ovn-agent |
+--------------------------------------+------------------------------+--------+-------------------+-------+-------+-------------------+

The first list is the Nova services as the database sees them: a scheduler, two conductors (one for the whole cloud, one for cell1) and one nova-compute. Each service reports in regularly, and State: up means its last report was recent. The same idea in the other lists: Cinder has a scheduler and a volume service (cloud1@lvmdriver-1: the host plus the storage backend, here plain LVM), and Neutron has OVN agents. If an instance will not start, this is the first place to look: is every service up?

Nova calls a deployment shard a cell. Every cloud has at least one. The databases below show two, nova_cell0 (a special cell for instances that could not be scheduled; that role is from Nova's cell design, and no failure was provoked here) and nova_cell1 (the working one).

Where the state lives: databases and the queue

Each service owns its own database and nobody reaches into somebody else's. On the test cloud:

$ mysql -uroot -p... -e "show databases"      (system databases left out)
cinder
glance
keystone
neutron
nova_api
nova_cell0
nova_cell1
placement
stats
$ mysql -uroot -p... -N -e "select table_schema, count(*) from information_schema.tables where table_schema in (...) group by table_schema"
keystone	49
glance	16
placement	13
cinder	36
neutron	135
nova_cell1	111
nova_cell0	111
nova_api	32
$ rabbitmqctl list_queues name messages | head -6
scheduler_fanout_92e6263afe3746889944228d380758f4	0
q-plugin.cloud1	0
cinder-scheduler.cloud1	0
cinder-volume.cloud1@lvmdriver-1	0
cinder-scheduler	0
$ rabbitmqctl -q list_connections name | wc -l
37

Eight databases (plus a stats one DevStack adds), one per service or cell. Neutron's 135 tables are the biggest: networks, ports, routers and security groups are a lot of rows. The queues carry the messages between processes, such as "scheduler, find a host", "volume service, create a volume", and 37 connections are open to RabbitMQ (counted with rabbitmqctl -q list_connections name).

This is also where a cloud hurts when it is run badly:

(These three consequences follow from the design described above; this page did not break the lab to test them.)

How the cloud knows what hardware it has: Placement

When Nova must pick a host, it needs to know what each host has and what is already used. That is the Placement service: "resource providers" (here, the compute host), their inventory and usage, in resource classes such as VCPU, MEMORY_MB and DISK_GB.

$ openstack image list
+--------------------------------------+--------------------------+--------+
| ID                                   | Name                     | Status |
+--------------------------------------+--------------------------+--------+
| 0a909d81-1440-4430-a266-930399e6e8b5 | cirros-0.6.3-x86_64-disk | active |
+--------------------------------------+--------------------------+--------+
$ openstack flavor list
+----+-----------+-------+------+-----------+-------+-----------+
| ID | Name      |   RAM | Disk | Ephemeral | VCPUs | Is Public |
+----+-----------+-------+------+-----------+-------+-----------+
| 1  | m1.tiny   |   512 |    1 |         0 |     1 | True      |
| 2  | m1.small  |  2048 |   20 |         0 |     1 | True      |
| 3  | m1.medium |  4096 |   40 |         0 |     2 | True      |
| 4  | m1.large  |  8192 |   80 |         0 |     4 | True      |
| 5  | m1.xlarge | 16384 |  160 |         0 |     8 | True      |
| c1 | cirros256 |   256 |    1 |         0 |     1 | True      |
| d1 | ds512M    |   512 |    5 |         0 |     1 | True      |
| d2 | ds1G      |  1024 |   10 |         0 |     1 | True      |
| d3 | ds2G      |  2048 |   10 |         0 |     2 | True      |
| d4 | ds4G      |  4096 |   20 |         0 |     4 | True      |
+----+-----------+-------+------+-----------+-------+-----------+
$ openstack network list -c Name
+---------+
| Name    |
+---------+
| private |
| public  |
+---------+
$ openstack hypervisor list
+--------------------------------------+---------------------+-----------------+-----------+-------+
| ID                                   | Hypervisor Hostname | Hypervisor Type | Host IP   | State |
+--------------------------------------+---------------------+-----------------+-----------+-------+
| d880925a-07e4-4bbf-a0ab-1112fc9723e6 | cloud1              | QEMU            | 10.0.2.15 | up    |
+--------------------------------------+---------------------+-----------------+-----------+-------+
$ openstack resource provider inventory list $(openstack resource provider list -f value -c uuid | head -1)
+----------------+------------------+----------+----------+----------+-----------+-------+------+
| resource_class | allocation_ratio | min_unit | max_unit | reserved | step_size | total | used |
+----------------+------------------+----------+----------+----------+-----------+-------+------+
| VCPU           |              4.0 |        1 |        4 |        0 |         1 |     4 |    1 |
| MEMORY_MB      |              1.0 |        1 |     7941 |      512 |         1 |  7941 |  256 |
| DISK_GB        |              1.0 |        1 |       57 |        0 |         1 |    57 |    1 |
+----------------+------------------+----------+----------+----------+-----------+-------+------+

Read the inventory of the one hypervisor, cloud1:

Also visible: flavors (the sizes you pick: m1.tiny is 512 MB RAM and 1 disk GB, cirros256 is an extra-small flavor for CirrOS), one image (cirros-0.6.3, a 21 MB test image), two networks (private for instances and public for the outside), and the hypervisor (Type: QEMU: libvirt reports the type as QEMU even when it uses KVM, and the config says virt_type = kvm).

One instance, followed through the logs

To see the services work together, the test cloud booted one instance (CirrOS, the tiny 256 MB flavor, on the private network):

$ openstack server create --flavor cirros256 --image cirros-0.6.3-x86_64-disk --network private --wait flow-test
$ openstack server list -c Name -c Status -c Networks -c Flavor
+-----------+--------+---------------------------------------------------------+-----------+
| Name      | Status | Networks                                                | Flavor    |
+-----------+--------+---------------------------------------------------------+-----------+
| flow-test | ACTIVE | private=10.0.0.46, fd28:9fa8:c0f3:0:f816:3eff:fec4:ac14 | cirros256 |
+-----------+--------+---------------------------------------------------------+-----------+
$ journalctl -u 'devstack@*' | grep req-9b23ddd7-14b5-4570-9751-c7cd4ce87128      (log lines per service)
     51 devstack@neutron-api
     30 devstack@g-api
     26 devstack@n-api
     23 devstack@placement-api
(plus the scheduler, the conductor and nova-compute, which log under their own names)

Every API call gets a request ID, and the services pass it along when they call each other. So one grep over the logs of all services finds the whole story of one request. The server was ACTIVE in 14 seconds. What happened, in order (shortened from the real log lines):

n-api          POST /compute/v2.1/servers  status: 202  len: 376  microversion: 2.100  time: 0.951358
placement-api  GET /placement/allocation_candidates?limit=1000&resource...
placement-api  GET /placement/allocations/5207c12a-d569-4456-8f1c-c998a...
placement-api  PUT /placement/allocations/5207c12a-d569-4456-8f1c-c998a...
nova-conductor INFO dbcounter  Registered counter for database nova_cell1
nova-compute   INFO nova.compute.claims  [instance: 5207c12a-...] Claim successful on node cloud1
neutron-api    INFO neutron.db.ovn_revision_numbers_db  Successfully bumped revision number for resource 1c17a238-...
nova-compute   INFO nova.virt.libvirt.driver  [instance: 5207c12a-...] Creating image(s)
nova-compute   INFO os_vif  Successfully plugged vif VIFOpenVSwitch(active=False,address=fa:16:3e:c4:ac:14,bridge_name='br-int',...
nova-compute   INFO nova.compute.manager  [instance: 5207c12a-...] Took 8.89 seconds to spawn the instance on the hypervisor.
nova-compute   INFO nova.compute.manager  [instance: 5207c12a-...] Took 14.14 seconds to build instance.
$ sudo virsh list --all
 Id   Name                State
-----------------------------------
 1    instance-00000001   running

Read it as a story:

  1. nova-api accepts the request and answers 202 Accepted in under a second. The build continues in the background: this is why server create returns before the server is ready, and why --wait exists.
  2. The scheduler asks Placement for allocation_candidates ("which hosts have room for this flavor?"), picks one and writes the allocation back (PUT .../allocations/...): the resources are now claimed.
  3. The conductor coordinates the build with the cell's database.
  4. nova-compute on the chosen host confirms the claim ("Claim successful on node cloud1") and starts building: Glance supplies the image (30 log lines), Neutron creates and binds the network port (51 lines), then compute writes the disk ("Creating image(s)") and plugs the virtual network card into the Open vSwitch bridge br-int.
  5. libvirt starts the guest: virsh list shows instance-00000001, running. "Took 8.89 seconds to spawn" and "Took 14.14 seconds to build".

Five of the services above cooperated on one command, each doing its one job. The compute node never touched a database itself: it asked the conductor, as the Nova documentation says. When you troubleshoot a failed boot, you follow the same path: API, scheduler (no valid host?), Placement, compute, image, network port.

Regions, zones and cells

The words look like the big clouds' and mean something a little different:

Mistakes people make early

What this page did not cover

The cheat sheet

openstack catalog list                         what services exist, and their URLs
openstack endpoint list --interface public     the URL of each
openstack token issue                          who and which project am I
openstack project list / user list             who exists (admin)
openstack role assignment list --names         who may do what, where
openstack compute service list                 Nova processes, up or down
openstack volume service list                 Cinder processes
openstack network agent list                   Neutron agents
openstack hypervisor list                      where instances can run
openstack resource provider inventory list ID  what the host has (Placement)
openstack image list / flavor list / network list    what you can launch with
openstack --debug COMMAND                      show the real HTTP requests
openstack server list --all-projects           (admin) every instance in the cloud
journalctl -u 'devstack@*' | grep req-...      follow a request through the logs (DevStack)

↑ All tutorials · Build a Test Cloud with DevStack: From a Blank Ubuntu to a Working Cloud →