{"id":138,"date":"2026-08-17T00:26:06","date_gmt":"2026-08-17T00:26:06","guid":{"rendered":"http:\/\/127.0.0.1\/en\/docs\/how-openstack-was-installed-kampos-network-bgp-osa-haproxy-k8s-cert-manager\/"},"modified":"2026-08-17T00:56:50","modified_gmt":"2026-08-17T00:56:50","slug":"openstack-lab-deployment","status":"publish","type":"post","link":"https:\/\/wp-nv.radut.info\/ro\/2026\/08\/17\/openstack-lab-deployment\/","title":{"rendered":"OpenStack Lab Deployment"},"content":{"rendered":"<p><strong>Scope:<\/strong> this walks through how this lab&#8217;s OpenStack cloud was actually built, end to end \u2014 physical network \u2192 MikroTik BGP \u2192 OpenStack-Ansible install \u2192 internal HAProxy. Real values from this deployment (VLAN IDs, AS numbers, CIDRs, commands) are included throughout; credentials, private keys and tokens are intentionally omitted \u2014 see the referenced files directly in the repo for those, and rotate anything you find that&#8217;s ever touched git history.<\/p>\n<h2>1. Architecture at a glance<\/h2>\n<p>One physical trunk carries every VLAN into a single MikroTik RB5009, which does two jobs: it&#8217;s the L3 gateway for each VLAN, and it&#8217;s a BGP router peering with every cluster&#8217;s CNI (Calico) so pod\/service CIDRs get real routes instead of NAT tricks. OpenStack itself is a 3-node OpenStack-Ansible (OSA) deployment where each of the 3 physical hosts plays every infrastructure role at once (no dedicated controller\/compute split).<\/p>\n<h2>2. The physical network: one trunk, many VLANs<\/h2>\n<p>Every VLAN rides the same trunk interface into the router&#8217;s bridge. From the live RouterOS config:<\/p>\n<pre><code>vlan-id  8   lab-int             (management uplink to the switch)\nvlan-id  9   bmc                 (server BMC\/IPMI network)\nvlan-id  11  mgm                 (OSA management network, br-mgmt)\nvlan-id  12  vxlan               (OSA tunnel network, VXLAN overlay)\nvlan-id  13  k8s-a               (k8s cluster A VLAN)\nvlan-id  14  project-a           (OpenStack tenant network A)\nvlan-id  15  project-b           (OpenStack tenant network B)\nvlan-id  16  k8s-b               (k8s cluster B VLAN)\nvlan-id  18  os-net-bgp          (OpenStack provider network used for BGP peering)\nvlan-id  19  lbaas               (reserved for Octavia LBaaS, not yet enabled)\n<\/code><\/pre>\n<p>On the OpenStack-Ansible side, the provider VLAN range for tenant networks is configured as a contiguous block rather than the whole 4094-VLAN space \u2014 deliberately narrow, matching only what&#8217;s actually wired to the switch:<\/p>\n<pre><code># openstack_user_config.yml\n- network:\n    container_bridge: \"br-vlan\"\n    type: \"vlan\"\n    range: \"14:18\"   # tenant VLANs; 19 is reserved for lbaas, 20 storage, 21 osd\n    net_name: \"provider\"<\/code><\/pre>\n<h2>3. BGP: how the router talks to each cluster&#8217;s CNI<\/h2>\n<p>Every Kubernetes cluster and the OpenStack tenant network peers with the router over eBGP, not static routes. The AS numbering follows one rule consistently: <strong>router-side ASN = node-side ASN + 2<\/strong>:<\/p>\n<table>\n<tr>\n<th>Peering<\/th>\n<th>Router AS<\/th>\n<th>Node AS<\/th>\n<\/tr>\n<tr>\n<td>OpenStack tenant (compute hosts)<\/td>\n<td>65014<\/td>\n<td>65015<\/td>\n<\/tr>\n<tr>\n<td>k8s project a CNI<\/td>\n<td>65023<\/td>\n<td>65021<\/td>\n<\/tr>\n<tr>\n<td>k8s project b CNI<\/td>\n<td>65027<\/td>\n<td>65025<\/td>\n<\/tr>\n<\/table>\n<p>Each BGP instance gets its own router-id and its own accept-only-this-prefix filter chain \u2014 nothing is accepted by default, each peering can only advertise the one CIDR block it&#8217;s actually supposed to own:<\/p>\n<pre><code>\/routing bgp instance add as=65023 name=\"k8s project a cni\" router-id=10.14.0.1 routing-table=main\n\/routing bgp instance add as=65014 name=\"OS tenant\"     router-id=10.12.0.1 routing-table=main\n\/routing bgp instance add as=65027 name=\"k8s project b cni\" router-id=10.15.0.1 routing-table=main\n\n# one connection per node \u2014 example, k8s-a masters\/nodes:\n\/routing bgp connection add as=65023 instance=\"k8s project a cni\" local.address=10.14.0.1 \\\n  remote.address=10.14.0.21 .as=65021 templates=k8s-a-cni-template name=k8s-a-master-1\n# ... repeated per node (10.14.0.21-23 masters, .31-.36 workers)\n\n# the filter is the real security boundary \u2014 only this cluster's own service\/pod CIDRs are accepted:\n\/routing filter rule add chain=k8s-a-in rule=\"if (dst in 10.13.0.0\/18 || dst in 10.13.64.0\/18 || dst in 10.13.128.0\/18) { accept }\"\n\/routing filter rule add chain=k8s-a-out rule=reject   # never advertise anything back<\/code><\/pre>\n<p>The OpenStack tenant peering additionally uses <code>tcp-md5-key<\/code> on the BGP session (the compute hosts sit across VLANs from the router, <code>multihop=yes<\/code>) \u2014 the k8s CNI peerings don&#8217;t need it since Calico&#8217;s own BGP password support via a Kubernetes Secret covers that layer instead.<\/p>\n<h2>4. Installing OpenStack (OpenStack-Ansible)<\/h2>\n<p>Standard 3-node OSA, deployed from a bootstrap\/deploy host with SSH access to all 3 physical servers.<\/p>\n<h3>4.1 Base OS prep<\/h3>\n<p>Before OSA touches anything, every target host needs to match OpenStack-Ansible&#8217;s own &#8220;prepare the target hosts&#8221; prerequisites: short (non-FQDN) hostnames \u2014 long names blow past LXC\/SSH naming limits \u2014 the <code>en_US.UTF-8<\/code> locale, SSH key-based auth with passwordless sudo, and the base package set OSA&#8217;s docs call for (<code>bridge-utils<\/code>, <code>lvm2<\/code>, <code>openssh-server<\/code>, <code>tcpdump<\/code>, <code>python3<\/code>). The docs also call for four network bridges: <code>br-mgmt<\/code> on every host (OSA&#8217;s own management\/API traffic), <code>br-vxlan<\/code> for the tunnel network, <code>br-vlan<\/code> for VLAN\/flat provider networks, and <code>br-storage<\/code> for Cinder\/Ceph traffic.<\/p>\n<p>On top of that baseline, this lab&#8217;s own host-prep adds: <code>openvswitch-switch<\/code> (the bridges above are OVS bridges here, not Linux bridges \u2014 see the <code>lxc_net_bridge_type<\/code> override in \u00a75), <code>ceph-volume<\/code> (these hosts double as Ceph OSD nodes), KVM kernel modules auto-detected from CPU vendor, and persistent iptables rules.<\/p>\n<h3>4.2 Bootstrap Ansible itself, then generate secrets<\/h3>\n<pre><code>export LC_ALL=en_US.UTF-8\nexport LANG=en_US.UTF-8\nscripts\/bootstrap-ansible.sh\n\n# auto-generate every OpenStack service password\/token \u2014 never hand-write these\ncd \/opt\/openstack-ansible\/scripts\/\npython pw-token-gen.py --file \/etc\/openstack_deploy\/user_secrets.yml<\/code><\/pre>\n<h3>4.3 The 3 real install playbooks<\/h3>\n<pre><code>cd \/opt\/openstack-ansible\/playbooks\nopenstack-ansible setup-hosts.yml           # 1. prepare hosts + LXC containers\nopenstack-ansible setup-infrastructure.yml  # 2. galera, rabbitmq, memcached, internal haproxy\nopenstack-ansible setup-openstack.yml       # 3. keystone, nova, neutron, cinder, glance, horizon...<\/code><\/pre>\n<p><strong>A real gotcha hit during this install:<\/strong> instances had no internet connectivity until this was set:<\/p>\n<pre><code>sed -i 's|is_metal:.*|is_metal: false|g' \/opt\/openstack-ansible\/inventory\/env.d\/neutron.yml<\/code><\/pre>\n<h2>5. Non-default configuration (openstack_user_config.yml \/ user_variables.yml)<\/h2>\n<p>What actually deviates from OSA&#8217;s defaults in this deployment:<\/p>\n<ul>\n<li><strong>Network CIDRs<\/strong> \u2014 management <code>10.11.0.0\/24<\/code>, tunnel <code>10.12.0.0\/24<\/code>, storage <code>10.80.0.0\/24<\/code> (LBaaS reserved at <code>10.19.0.0\/24<\/code> but not enabled yet)<\/li>\n<li><strong>HAProxy VIPs<\/strong> \u2014 internal <code>10.11.0.10<\/code>, external <code>10.11.0.9<\/code> \u2014 these must resolve to an address inside <code>haproxy_keepalived_external_vip_cidr<\/code><\/li>\n<li><strong>Combined host roles<\/strong> \u2014 all 3 physical hosts (<code>infra1\/2\/3<\/code>) serve as infrastructure, compute, identity, storage-infra, load-balancer and repo hosts simultaneously \u2014 there&#8217;s no dedicated controller tier, deliberate for a 3-node cloud<\/li>\n<li><strong>Ceph as the Cinder\/Glance backend<\/strong>, not LVM \u2014 the LVM backend block exists in the config but is commented out<\/li>\n<li><strong>VXLAN tunnel range<\/strong> <code>1:1000<\/code>, tenant VLAN range <code>14:18<\/code> (see \u00a72)<\/li>\n<li><strong>Tuning<\/strong>: <code>galera_innodb_buffer_pool_size: 4G<\/code>, <code>galera_max_connections: 1000<\/code>, <code>haproxy_maxconn: 4096<\/code>, LXC bridge on Open vSwitch (<code>lxc_net_bridge_type: openvswitch<\/code>) rather than the Linux-bridge default<\/li>\n<\/ul>\n<h2>6. HAProxy \u2014 fronting the OpenStack APIs<\/h2>\n<p>This is <strong>OSA&#8217;s own internal HAProxy<\/strong> (deployed by <code>setup-infrastructure.yml<\/code>&#8216;s <code>haproxy-install.yml<\/code>, running on the same 3 <code>load_balancer_hosts<\/code>). It fronts Keystone, Nova, Neutron, Cinder, Glance, Horizon etc. behind the two VIPs from \u00a75, load-balancing across the 3 infra containers running each service.<\/p>\n<h2>Summary of the full chain<\/h2>\n<ol>\n<li>Physical trunk + MikroTik VLANs (\u00a72)<\/li>\n<li>BGP peering per cluster, filtered to exactly its own CIDR (\u00a73)<\/li>\n<li>OpenStack-Ansible: bootstrap \u2192 setup-hosts \u2192 setup-infrastructure \u2192 setup-openstack (\u00a74)<\/li>\n<li>OSA&#8217;s internal HAProxy fronts the OpenStack API VIPs, and the cloud is ready for workloads (\u00a76)<\/li>\n<\/ol>","protected":false},"excerpt":{"rendered":"<p>Scope: this walks through how this lab&#8217;s OpenStack cloud was actually built, end to end \u2014 physical network \u2192 MikroTik BGP \u2192 OpenStack-Ansible install \u2192&#8230;<\/p>\n<div class=\"more-link-wrapper\"><a class=\"more-link\" href=\"https:\/\/wp-nv.radut.info\/ro\/2026\/08\/17\/openstack-lab-deployment\/\">Continue reading<span class=\"screen-reader-text\">OpenStack Lab Deployment<\/span><\/a><\/div>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"qubely_global_settings":"","qubely_interactions":"","_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","_uag_custom_page_level_css":"","footnotes":""},"categories":[21],"tags":[],"class_list":["post-138","post","type-post","status-publish","format-standard","hentry","category-openstack","entry"],"qubely_featured_image_url":null,"qubely_author":{"display_name":"radu","author_link":"https:\/\/wp-nv.radut.info\/ro\/author\/radu\/"},"qubely_comment":0,"qubely_category":"<a href=\"https:\/\/wp-nv.radut.info\/ro\/category\/openstack\/\" rel=\"category tag\">OpenStack<\/a>","qubely_excerpt":"Scope: this walks through how this lab&#8217;s OpenStack cloud was actually built, end to end \u2014 physical network \u2192 MikroTik BGP \u2192 OpenStack-Ansible install \u2192&#8230;Continue readingOpenStack Lab Deployment","uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false,"trp-custom-language-flag":false,"qubely_landscape":false,"qubely_portrait":false,"qubely_thumbnail":false},"uagb_author_info":{"display_name":"radu","author_link":"https:\/\/wp-nv.radut.info\/ro\/author\/radu\/"},"uagb_comment_info":0,"uagb_excerpt":"Scope: this walks through how this lab&#8217;s OpenStack cloud was actually built, end to end \u2014 physical network \u2192 MikroTik BGP \u2192 OpenStack-Ansible install \u2192&#8230;Continue readingOpenStack Lab Deployment","_links":{"self":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts\/138","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/comments?post=138"}],"version-history":[{"count":2,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts\/138\/revisions"}],"predecessor-version":[{"id":151,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts\/138\/revisions\/151"}],"wp:attachment":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/media?parent=138"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/categories?post=138"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/tags?post=138"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}