{"id":150,"date":"2026-08-17T00:48:07","date_gmt":"2026-08-17T00:48:07","guid":{"rendered":"http:\/\/127.0.0.1\/en\/docs\/openstack-projects-quotas-via-terraform\/"},"modified":"2026-08-17T00:56:52","modified_gmt":"2026-08-17T00:56:52","slug":"openstack-projects-quotas-via-terraform","status":"publish","type":"post","link":"https:\/\/wp.radut.info\/en\/2026\/08\/17\/openstack-projects-quotas-via-terraform\/","title":{"rendered":"OpenStack Projects &#038; Quotas via Terraform"},"content":{"rendered":"<p><strong>Scope:<\/strong> this covers how OpenStack tenant projects in this lab are provisioned <em>declaratively<\/em> via Terraform&#8217;s <code>openstack<\/code> provider \u2014 projects, quotas, users\/roles, security-group rules, custom compute flavors, and the per-project provider VLAN network. For the surrounding physical network, BGP, and OpenStack-Ansible install itself, see the <a href=\"https:\/\/wp.radut.info\/en\/docs\/openstack-lab-deployment\/\">OpenStack Lab Deployment<\/a> article \u2014 this one is scoped narrowly to project\/quota management on top of that cloud. Credentials, private keys and real passwords are intentionally omitted or replaced with Terraform variable references \u2014 see the referenced files directly in the repo for those, and rotate anything that&#8217;s ever touched git history.<\/p>\n<h2>1. Why Terraform for tenant projects<\/h2>\n<p>Every OpenStack tenant project in this lab (compute\/storage\/network quotas, the project&#8217;s admin user, its default security-group rules, its custom flavor set, its provider network) is defined as Terraform resources against the <code>openstack<\/code> provider, rather than clicked through Horizon or scripted with one-off <code>openstack<\/code> CLI calls. Same rationale as everything else &#8220;as code&#8221; in this repo: the project&#8217;s exact quota numbers, flavor list, and network layout are reviewable in a diff and reproducible from scratch \u2014 re-running <code>terraform apply<\/code> after a project&#8217;s quotas change is safer than hoping someone remembers what was set by hand in Horizon.<\/p>\n<p>This lab runs two tenant projects side by side \u2014 <strong>project-a<\/strong> and <strong>project-b<\/strong> \u2014 each with its own quotas, admin user, keypair, flavor set, and provider VLAN (VLAN 14 and VLAN 15 respectively; see the network article&#8217;s VLAN table for how these fit into the wider physical network).<\/p>\n<h2>2. Project + quotas<\/h2>\n<p>Each project is a plain <code>openstack_identity_project_v3<\/code>, with three separate quota resources \u2014 Nova (compute), Cinder (block storage), and Neutron (networking) are each their own quota API in OpenStack, so each gets its own Terraform resource pointed at the same <code>project_id<\/code>:<\/p>\n<pre><code>resource \"openstack_identity_project_v3\" \"project_a\" {\n  name        = \"project-a\"\n  description = \"Project A\"\n  domain_id   = \"default\"\n}\n\n# Compute quotas (Nova)\nresource \"openstack_compute_quotaset_v2\" \"project_a_quota\" {\n  project_id = openstack_identity_project_v3.project_a.id\n\n  instances            = 100        # Max instances\n  cores                = 240        # Max vCPUs\n  ram                  = 348 * 1024 # Max RAM (MB)\n  key_pairs            = 10\n  server_groups        = 50\n  server_group_members = 10\n}\n\n# Block storage quotas (Cinder)\nresource \"openstack_blockstorage_quotaset_v3\" \"project_a_quota\" {\n  project_id = openstack_identity_project_v3.project_a.id\n\n  volumes              = 50   # Max volumes\n  snapshots            = 20   # Max snapshots\n  gigabytes            = 3000 # Total storage (GB)\n  backups              = 20\n  backup_gigabytes     = 3000\n  per_volume_gigabytes = 1000\n}\n\n# Network quotas (Neutron)\nresource \"openstack_networking_quota_v2\" \"project_a_quota\" {\n  project_id = openstack_identity_project_v3.project_a.id\n\n  network             = 5\n  subnet              = 10\n  router              = 10\n  port                = 500\n  floatingip          = 30\n  security_group      = 500\n  security_group_rule = 2000\n}<\/code><\/pre>\n<p>project-b is the smaller of the two \u2014 a deliberately tighter quota envelope (15 instances \/ 100 vCPUs \/ 128GB RAM vs. project-a&#8217;s 100 instances \/ 240 vCPUs \/ 348GB RAM), since it hosts a narrower workload than the primary project.<\/p>\n<h2>3. User + role assignment<\/h2>\n<p>Each project gets one admin-scoped user. The password is intentionally shown here as a variable reference rather than a literal \u2014 in the real Terraform it should come from a <code>.tfvars<\/code> file kept out of git (or a proper secret store), never hardcoded in the <code>.tf<\/code> source itself:<\/p>\n<pre><code>resource \"openstack_identity_user_v3\" \"project_a_user\" {\n  name               = \"project-a-admin\"\n  password           = var.project_a_user_password\n  default_project_id = openstack_identity_project_v3.project_a.id\n  domain_id          = \"default\"\n}\n\ndata \"openstack_identity_role_v3\" \"admin_role\" {\n  name = \"admin\"\n}\n\nresource \"openstack_identity_role_assignment_v3\" \"project_a_member\" {\n  user_id    = openstack_identity_user_v3.project_a_user.id\n  project_id = openstack_identity_project_v3.project_a.id\n  role_id    = data.openstack_identity_role_v3.admin_role.id\n}\n\n# SSH keypair for this user -- keypairs in OpenStack are project-scoped, so this\n# is created under the project's own credentials, not the admin\/cloud-admin ones.\nresource \"openstack_compute_keypair_v2\" \"project_a_key\" {\n  name       = \"project-a-cloud\"\n  public_key = \"ssh-rsa AAAA... your-key-here\"\n  user_id    = openstack_identity_user_v3.project_a_user.id\n}<\/code><\/pre>\n<h2>4. Default security group rules<\/h2>\n<p>Every project&#8217;s auto-created <code>default<\/code> security group starts with no ingress rules at all \u2014 Terraform reads it as a data source (it already exists once the project exists) and adds the two baseline rules every instance needs: SSH and ICMP:<\/p>\n<pre><code>data \"openstack_networking_secgroup_v2\" \"default_secgroup_project_a\" {\n  name      = \"default\"\n  tenant_id = openstack_identity_project_v3.project_a.id\n\n  depends_on = [openstack_identity_project_v3.project_a]\n}\n\nresource \"openstack_networking_secgroup_rule_v2\" \"default_ssh_project_a\" {\n  direction         = \"ingress\"\n  ethertype         = \"IPv4\"\n  protocol          = \"tcp\"\n  port_range_min    = 22\n  port_range_max    = 22\n  remote_ip_prefix  = \"0.0.0.0\/0\"\n  security_group_id = data.openstack_networking_secgroup_v2.default_secgroup_project_a.id\n}\n\nresource \"openstack_networking_secgroup_rule_v2\" \"default_icmp_project_a\" {\n  direction         = \"ingress\"\n  ethertype         = \"IPv4\"\n  protocol          = \"icmp\"\n  remote_ip_prefix  = \"0.0.0.0\/0\"\n  security_group_id = data.openstack_networking_secgroup_v2.default_secgroup_project_a.id\n}<\/code><\/pre>\n<h2>5. Custom compute flavors<\/h2>\n<p>Flavors are defined once as a <code>locals<\/code> list and expanded with <code>for_each<\/code>, rather than one resource block per flavor. A representative slice:<\/p>\n<pre><code>locals {\n  base_flavors = [\n    { name = \"4xCPU_8GB\",  vcpus = 4, ram = 8192,  disk = 0, extra_specs = {} },\n    { name = \"8xCPU_24GB\", vcpus = 8, ram = 24576, disk = 0, extra_specs = {} },\n    { name = \"c.4xCPU_24GB\", vcpus = 4, ram = 24576, disk = 0, extra_specs = {\n      \"hw:cpu_policy\"            = \"dedicated\"\n      \"hw:cpu_mode\"              = \"host-passthrough\"\n      \"hw:cpu_model_extra_flags\" = \"pcid,pdpe1gb\"\n      \"hw:cpu_thread_policy\"     = \"prefer\"\n      \"hw:cpu_sockets\"           = \"1\"\n      \"hw:cpu_cores\"             = \"4\"\n      \"hw:cpu_threads\"           = \"1\"\n    } },\n  ]\n}\n\nresource \"openstack_compute_flavor_v2\" \"flavors_project_a\" {\n  for_each = { for f in local.base_flavors : f.name => f }\n\n  name      = each.value.name\n  vcpus     = each.value.vcpus\n  ram       = each.value.ram\n  disk      = each.value.disk\n  is_public = false\n\n  # Present all vCPUs as cores inside a single socket so Windows guests don't\n  # ignore CPUs. Windows client editions only recognize a limited number of\n  # physical sockets (Pro = 2, Home = 1); without an explicit topology, libvirt\n  # exposes 1 socket per vCPU, so an 8-vCPU VM would only show 2 usable CPUs.\n  extra_specs = merge(\n    {\n      \"hw:cpu_sockets\" = \"1\"\n      \"hw:cpu_cores\"   = tostring(each.value.vcpus)\n      \"hw:cpu_threads\" = \"1\"\n    },\n    each.value.extra_specs,\n  )\n}\n\nresource \"openstack_compute_flavor_access_v2\" \"flavors_project_a_access\" {\n  for_each  = openstack_compute_flavor_v2.flavors_project_a\n  flavor_id = each.value.id\n  tenant_id = openstack_identity_project_v3.project_a.id\n}<\/code><\/pre>\n<p><code>is_public = false<\/code> + the explicit <code>openstack_compute_flavor_access_v2<\/code> grant keeps each project&#8217;s custom flavors private to that project instead of visible cloud-wide \u2014 every project in this lab defines its own flavor set this way rather than sharing one global list.<\/p>\n<h2>6. Provider VLAN network + subnet<\/h2>\n<p>Each project&#8217;s tenant network rides its own dedicated VLAN on the provider trunk (see the Lab Deployment article&#8217;s VLAN table \u2014 VLAN 14 for project-a, VLAN 15 for project-b):<\/p>\n<pre><code>resource \"openstack_networking_network_v2\" \"vlan14\" {\n  name           = \"vlan-14\"\n  admin_state_up = true\n  external       = true\n  shared         = false\n  mtu            = 1500\n  tenant_id      = openstack_identity_project_v3.project_a.id\n  segments {\n    network_type     = \"vlan\"\n    physical_network = \"provider\"\n    segmentation_id  = 14\n  }\n}\n\nresource \"openstack_networking_subnet_v2\" \"vlan14_subnet\" {\n  name            = \"vlan-14\"\n  network_id      = openstack_networking_network_v2.vlan14.id\n  tenant_id       = openstack_identity_project_v3.project_a.id\n  cidr            = \"10.14.0.0\/24\"\n  gateway_ip      = \"10.14.0.1\"\n  ip_version      = 4\n  dns_nameservers = [\"1.1.1.1\"]\n\n  allocation_pool {\n    start = \"10.14.0.10\"\n    end   = \"10.14.0.200\"\n  }\n\n  enable_dhcp = true\n}<\/code><\/pre>\n<h2>Summary<\/h2>\n<ol>\n<li>Project (<code>openstack_identity_project_v3<\/code>) + three separate quota resources (compute\/block-storage\/network) \u2014 \u00a72<\/li>\n<li>Admin user + role assignment, password sourced from a Terraform variable, never hardcoded \u2014 \u00a73<\/li>\n<li>Baseline SSH\/ICMP security-group rules on the project&#8217;s default security group \u2014 \u00a74<\/li>\n<li>Private, project-scoped custom compute flavors via <code>for_each<\/code> over a <code>locals<\/code> list \u2014 \u00a75<\/li>\n<li>Provider VLAN network + subnet tying the project into the physical network from the Lab Deployment article \u2014 \u00a76<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>Scope: this covers how OpenStack tenant projects in this lab are provisioned declaratively via Terraform&#8217;s openstack provider \u2014 projects, quotas, users\/roles, security-group rules, custom compute&#8230;<\/p>\n<div class=\"more-link-wrapper\"><a class=\"more-link\" href=\"https:\/\/wp.radut.info\/en\/2026\/08\/17\/openstack-projects-quotas-via-terraform\/\">Continue reading<span class=\"screen-reader-text\">OpenStack Projects &#038; Quotas via Terraform<\/span><\/a><\/div>\n","protected":false},"author":0,"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-150","post","type-post","status-publish","format-standard","hentry","category-openstack","entry"],"qubely_featured_image_url":null,"qubely_author":{"display_name":"","author_link":"https:\/\/wp.radut.info\/en\/author\/"},"qubely_comment":0,"qubely_category":"<a href=\"https:\/\/wp.radut.info\/en\/category\/openstack\/\" rel=\"category tag\">OpenStack<\/a>","qubely_excerpt":"Scope: this covers how OpenStack tenant projects in this lab are provisioned declaratively via Terraform&#8217;s openstack provider \u2014 projects, quotas, users\/roles, security-group rules, custom compute&#8230;Continue readingOpenStack Projects &#038; Quotas via Terraform","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":"","author_link":"https:\/\/wp.radut.info\/en\/author\/"},"uagb_comment_info":0,"uagb_excerpt":"Scope: this covers how OpenStack tenant projects in this lab are provisioned declaratively via Terraform&#8217;s openstack provider \u2014 projects, quotas, users\/roles, security-group rules, custom compute&#8230;Continue readingOpenStack Projects &#038; Quotas via Terraform","_links":{"self":[{"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/posts\/150","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/comments?post=150"}],"version-history":[{"count":1,"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/posts\/150\/revisions"}],"predecessor-version":[{"id":168,"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/posts\/150\/revisions\/168"}],"wp:attachment":[{"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/media?parent=150"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/categories?post=150"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wp.radut.info\/en\/wp-json\/wp\/v2\/tags?post=150"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}