# Create Terraform vars template Create project directory. ```sh mkdir /home/openstack/stack ``` Create vars template. ```sh nano -cw /home/openstack/stack/vars.tf.envsubst ## vars variable "provider_config" { type = map(string) default = { auth_url = "${AUTH_URL}" auth_user = "${ACCOUNT}" auth_pass = "${ACCOUNT_PASSWORD}" project = "${PROJECT}" } } variable "extnetid" { type = string default = "${PROVIDER_NET_ID}" } variable "image" { type = string default = "${IMAGE}" } variable "flavor" { type = string default = "${FLAVOR}" } locals { project = "${var.provider_config["project"]}" pubkey = "${PUB_KEY}" } ``` # Initial cluster configuration This script configures: - Creates Provider network - Creates a project - Creates project based quotas - Creates a user with RBAC - Uploads instance disk images - Creates flavours - Renders the Terraform vars file ```sh touch /home/openstack/stack/configure_cluster.sh chmod +x /home/openstack/stack/configure_cluster.sh nano -cw /home/openstack/stack/configure_cluster.sh #!/usr/bin/env bash # load venv and credentials source /home/openstack/kolla_zed/bin/activate source /etc/kolla/admin-openrc.sh # vars OPENSTACK_CLI=openstack EXT_NET_CIDR='192.168.140.0/24' EXT_NET_RANGE='start=192.168.140.200,end=192.168.140.254' EXT_NET_GATEWAY='192.168.140.1' PROJECT='test' ACCOUNT='tseed' ACCOUNT_PASSWORD='Password0' ACCOUNT_EMAIL='toby.n.seed@gmail.com' # check cluster $OPENSTACK_CLI host list $OPENSTACK_CLI hypervisor list $OPENSTACK_CLI user list # provider shared network $OPENSTACK_CLI network create --external --share --provider-physical-network physnet1 --provider-network-type flat provider_network $OPENSTACK_CLI subnet create --dhcp --network provider_network --subnet-range ${EXT_NET_CIDR} --gateway ${EXT_NET_GATEWAY} --allocation-pool ${EXT_NET_RANGE} provider_subnet # create project $OPENSTACK_CLI project create --domain default --description "guest project" $PROJECT # set quota on project $OPENSTACK_CLI quota set --instances 10 $PROJECT $OPENSTACK_CLI quota set --cores 4 $PROJECT $OPENSTACK_CLI quota set --ram 6144 $PROJECT $OPENSTACK_CLI quota set --gigabytes 30 $PROJECT $OPENSTACK_CLI quota set --volumes 10 $PROJECT $OPENSTACK_CLI quota set --backups 0 $PROJECT $OPENSTACK_CLI quota set --snapshots 0 $PROJECT $OPENSTACK_CLI quota set --key-pairs 20 $PROJECT $OPENSTACK_CLI quota set --floating-ips 20 $PROJECT $OPENSTACK_CLI quota set --networks 10 $PROJECT $OPENSTACK_CLI quota set --routers 10 $PROJECT $OPENSTACK_CLI quota set --subnets 10 $PROJECT $OPENSTACK_CLI quota set --secgroups 20 $PROJECT $OPENSTACK_CLI quota set --secgroup-rules 100 $PROJECT # create user $OPENSTACK_CLI user create --password ${ACCOUNT_PASSWORD} --email ${ACCOUNT_EMAIL} $ACCOUNT # set the default project in the web console for user $OPENSTACK_CLI user set --project $PROJECT $ACCOUNT $OPENSTACK_CLI project show $(openstack user show $ACCOUNT --domain default -f json | jq -r .default_project_id) -f json | jq -r .description # set RBAC for guest project $OPENSTACK_CLI role add --project $PROJECT --user $ACCOUNT admin # download the cirros test image for admin project wget http://download.cirros-cloud.net/0.5.1/cirros-0.5.1-x86_64-disk.img $OPENSTACK_CLI image create --disk-format qcow2 --container-format bare --private --project admin --property os_type=linux --file ./cirros-0.5.1-x86_64-disk.img cirros-0.5.1 # download the ubuntu image for all projects wget https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img $OPENSTACK_CLI image create --disk-format qcow2 --container-format bare --public --property os_type=linux --file ./bionic-server-cloudimg-amd64.img ubuntu_18.04 # create a flavour for the admin project $OPENSTACK_CLI flavor create admin.tiny --ram 1048 --disk 1 --vcpus 2 --private --project admin # create flavours for the guest project $OPENSTACK_CLI flavor create m1.tiny --ram 512 --disk 5 --vcpus 1 --private --project $PROJECT $OPENSTACK_CLI flavor create m1.smaller --ram 1024 --disk 10 --vcpus 1 --private --project $PROJECT # collect vars export PROJECT=$PROJECT export ACCOUNT=$ACCOUNT export ACCOUNT_PASSWORD=$ACCOUNT_PASSWORD export AUTH_URL=$(openstack endpoint list -f json | jq -r '.[] | select(."Service Name" == "keystone" and ."Interface" == "public") | .URL') export PROVIDER_NET_ID=$(openstack network list -f json | jq -r '.[] | select(."Name" == "provider_network") | .ID') export IMAGE=$(openstack image list -f json | jq -r '.[] | select(."Name" == "ubuntu_18.04") | .ID') export FLAVOR=$(openstack flavor list --all -f json | jq -r '.[] | select(."Name" == "m1.tiny") | .ID') export PUB_KEY=$(cat /home/openstack/.ssh/id_rsa.pub) # render terraform vars.tf envsubst < /home/openstack/stack/vars.tf.envsubst > /home/openstack/stack/vars.tf ``` # Install Terraform ```sh wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt update && sudo apt install terraform ``` # Create remaining project config and templates ## Create user data template Salted hash password generated with `openssl passwd -6 -salt xyz Password0`. This could be automated in Terraform on a per instance basis to resulting in a different hash for the same password to deter anyone who maybe able to intercept or check cloud-init on instantiation (maybe you can see it at the metadata endpoint in Openstack?). ```sh nano -cw /home/openstack/stack/user_data.sh #cloud-config ssh_pwauth: true groups: - admingroup: [root,sys] - openstack users: - name: openstack primary_group: openstack lock_passwd: false passwd: $6$xyz$4tTWyuHIT6gXRuzotBZn/9xZBikUp0O2X6rOZ7MDJo26aax.Ok5P4rWYyzdgFkjArIIyB8z8LKVW1wARbcBzn/ sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/bash ssh_authorized_keys: - ssh-rsa ${pubkey} ``` ## Create Ansible inventory template Gets rendered by Terraform. Ansible will also work with cloud-init seeded ssh pub key. ```sh nano -cw user_data.sh [nodes] %{ for index, name in subnet1_instance_name ~} ${name} ansible_host=${subnet1_instance_address[index]} ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ansible_user=${user} ansible_password=${password} ansible_become=true %{ endfor ~} %{ for index, name in subnet2_instance_name ~} ${name} ansible_host=${subnet2_instance_address[index]} ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ansible_user=${user} ansible_password=${password} ansible_become=true %{ endfor ~} [subnet1_instances] %{ for index, name in subnet1_instance_name ~} ${name} %{ endfor ~} [subnet2_instances] %{ for index, name in subnet2_instance_name ~} ${name} %{ endfor ~} # when rendered this should look a little like the following, notice the provider network IPs provided by floating ip [nodes] subnet1_test0 ansible_host=192.168.140.230 ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ansible_user=openstack ansible_password=Password0 ansible_become=true subnet1_test1 ansible_host=192.168.140.223 ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ansible_user=openstack ansible_password=Password0 ansible_become=true subnet2_test0 ansible_host=192.168.140.217 ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ansible_user=openstack ansible_password=Password0 ansible_become=true [subnet1_instances] subnet1_test0 subnet1_test1 [subnet2_instances] subnet2_test0 ``` ## Create Ansible ping test playbook ```yaml nano -cw ansible_inventory ping_test.yml --- - name: build ping_map hosts: localhost become: no gather_facts: false tasks: - name: build ping_map ansible.builtin.set_fact: _ping_map: "{{ _ping_map | default({}) | combine ({entry: []}, recursive=True) }}" loop: "{{ inventory_hosts }}" loop_control: loop_var: entry vars: inventory_hosts: "{{ hostvars[inventory_hostname]['groups']['all'] }}" # - ansible.builtin.debug: # msg: # - "{{ _ping_map }}" - name: populate ping_map ansible.builtin.set_fact: _ping_map: "{{ _ping_map | default({}) | combine ({source: destination_list_append}, recursive=True) }}" loop: "{{ target_hosts|product(target_hosts) }}" loop_control: loop_var: entry vars: target_hosts: "{{ hostvars[inventory_hostname]['groups']['all'] }}" source: "{{ entry[0] }}" destination: "{{ entry[1] }}" destination_list: "{{ _ping_map[source] }}" destination_list_append: "{{ destination_list + [destination] }}" when: not entry[0] == entry[1] # - ansible.builtin.debug: # msg: # - "{{ _ping_map }}" - name: write global ping_map set_fact: _global_ping_map: "{{ _ping_map }}" delegate_to: localhost delegate_facts: true - name: ping test hosts: all become: yes gather_facts: True tasks: - name: load global ping_map set_fact: _ping_map: "{{ hostvars['localhost']['_global_ping_map'] }}" when: - hostvars['localhost']['_global_ping_map'] is defined # - debug: # msg: # - "{{ _ping_map }}" - name: ping neighbours shell: | echo SOURCE {{ inventory_hostname }} echo DESTINATION {{ destination_target }} echo ping -Rn -c 1 {{ destination_ip }} loop: "{{ destination_targets }}" loop_control: loop_var: entry vars: destination_targets: "{{ _ping_map[inventory_hostname] }}" destination_target: "{{ entry }}" destination_ip: "{{ hostvars[destination_target]['ansible_default_ipv4']['address'] }}" source: "{{ inventory_hostname }}" register: _ping_results - name: print results debug: msg: - "{{ output }}" loop: "{{ _ping_results['results'] }}" loop_control: loop_var: idx label: "{{ destination }}" vars: destination: "{{ idx['entry'] }}" output: "{{ idx['stdout_lines'] }}" ``` ## Create Terraform configuration ```sh nano -cw /home/openstack/stack/stack.tf ## load provider terraform { required_version = ">= 0.14.0" required_providers { openstack = { source = "terraform-provider-openstack/openstack" version = "~> 1.48.0" } } } ## configure provider provider "openstack" { auth_url = "${var.provider_config["auth_url"]}" user_name = "${var.provider_config["auth_user"]}" password = "${var.provider_config["auth_pass"]}" tenant_name = "${var.provider_config["project"]}" region = "RegionOne" } ## vars variable "dns" { type = list(string) default = ["1.1.1.1", "8.8.8.8"] } variable "subnet1" { type = map(string) default = { subnet_name = "subnet1" cidr = "172.16.10.0/24" instance_count = "2" } } variable "subnet2" { type = map(string) default = { subnet_name = "subnet2" cidr = "172.16.11.0/24" instance_count = "1" } } ## data sources data "openstack_networking_network_v2" "exnetname" { network_id = "${var.extnetid}" } #output "exnet_name" { # value = "${data.openstack_networking_network_v2.exnetname.name}" #} ## resources # router resource "openstack_networking_router_v2" "router" { name = "router_${local.project}" admin_state_up = true external_network_id = var.extnetid } # network1 resource "openstack_networking_network_v2" "network1" { name = "network1_${local.project}" } # network2 resource "openstack_networking_network_v2" "network2" { name = "network2_${local.project}" } # subnet1 resource "openstack_networking_subnet_v2" "subnet1" { name = "${var.subnet1["subnet_name"]}_${local.project}" network_id = openstack_networking_network_v2.network1.id cidr = var.subnet1["cidr"] dns_nameservers = var.dns } # subnet2 resource "openstack_networking_subnet_v2" "subnet2" { name = "${var.subnet2["subnet_name"]}_${local.project}" network_id = openstack_networking_network_v2.network2.id cidr = var.subnet2["cidr"] dns_nameservers = var.dns } # router interface subnet1 resource "openstack_networking_router_interface_v2" "interface1" { router_id = openstack_networking_router_v2.router.id subnet_id = openstack_networking_subnet_v2.subnet1.id } # router interface subnet2 resource "openstack_networking_router_interface_v2" "interface2" { router_id = openstack_networking_router_v2.router.id subnet_id = openstack_networking_subnet_v2.subnet2.id } # security group resource "openstack_compute_secgroup_v2" "ingress" { name = "${local.project}" description = "ingress rules" rule { from_port = 22 to_port = 22 ip_protocol = "tcp" cidr = "192.168.140.0/24" } rule { from_port = -1 to_port = -1 ip_protocol = "icmp" cidr = "192.168.140.0/24" } rule { from_port = 22 to_port = 22 ip_protocol = "tcp" self = true } rule { from_port = -1 to_port = -1 ip_protocol = "icmp" self = true } } # floating ip instance_subnet1 resource "openstack_compute_floatingip_v2" "instance_subnet1_fip" { count = "${var.subnet1["instance_count"]}" pool = "${data.openstack_networking_network_v2.exnetname.name}" #depends_on = ["openstack_networking_router_interface_v2.router"] } # floating ip instance_subnet2 resource "openstack_compute_floatingip_v2" "instance_subnet2_fip" { count = "${var.subnet2["instance_count"]}" pool = "${data.openstack_networking_network_v2.exnetname.name}" #depends_on = ["openstack_networking_router_interface_v2.router"] } # subnet1 instances resource "openstack_compute_instance_v2" "instance_subnet1" { count = "${var.subnet1["instance_count"]}" name = "${var.subnet1["subnet_name"]}_${local.project}${count.index}" image_id = var.image flavor_id = var.flavor user_data = templatefile("user_data.sh", { pubkey = local.pubkey } ) #network { # uuid = var.extnetid #} network { uuid = openstack_networking_network_v2.network1.id } security_groups = [ "${openstack_compute_secgroup_v2.ingress.name}" ] depends_on = [ openstack_networking_subnet_v2.subnet1 ] } # subnet2 instances resource "openstack_compute_instance_v2" "instance_subnet2" { count = "${var.subnet2["instance_count"]}" name = "${var.subnet2["subnet_name"]}_${local.project}${count.index}" image_id = var.image flavor_id = var.flavor user_data = templatefile("user_data.sh", { pubkey = local.pubkey } ) network { uuid = openstack_networking_network_v2.network2.id } security_groups = [ "${openstack_compute_secgroup_v2.ingress.name}" ] depends_on = [ openstack_networking_subnet_v2.subnet2 ] } # subnet1 floating ips resource "openstack_compute_floatingip_associate_v2" "fip_subnet1" { count = "${var.subnet1["instance_count"]}" floating_ip = "${openstack_compute_floatingip_v2.instance_subnet1_fip[count.index].address}" instance_id = "${openstack_compute_instance_v2.instance_subnet1[count.index].id}" } # subnet2 floating ips resource "openstack_compute_floatingip_associate_v2" "fip_subnet2" { count = "${var.subnet2["instance_count"]}" floating_ip = "${openstack_compute_floatingip_v2.instance_subnet2_fip[count.index].address}" instance_id = "${openstack_compute_instance_v2.instance_subnet2[count.index].id}" } # ansible inventory resource "local_file" "ansible_inventory" { content = templatefile("inventory.tmpl", { user = "openstack" password = "Password0" subnet1_instance_name = openstack_compute_instance_v2.instance_subnet1[*].name subnet1_instance_address = openstack_compute_floatingip_v2.instance_subnet1_fip[*].address subnet2_instance_name = openstack_compute_instance_v2.instance_subnet2[*].name subnet2_instance_address = openstack_compute_floatingip_v2.instance_subnet2_fip[*].address } ) filename = "ansible_inventory" } # cheat, no until connection - wait for nodes to boot and start ssh resource "time_sleep" "loitering" { create_duration = "120s" } # check ansible instance connectivity resource "null_resource" "ansible_floating_ip_ping" { provisioner "local-exec" { command = "ansible -i ansible_inventory all -m ping" } depends_on = [ time_sleep.loitering ] } # check ansible inter-instance connectivity resource "null_resource" "ansible_private_net_ping" { provisioner "local-exec" { command = "ansible-playbook -i ansible_inventory ping_test.yml" } depends_on = [ null_resource.ansible_floating_ip_ping ] } ``` # Run ```sh cd /home/openstack/stack terraform init terraform plan terraform apply -auto-approve terraform destroy -auto-approve ```