Category Archives: Automation

Vagrant: mapping a Virtualbox VM to a Vagrant environment

This is a small post hopefully saving you a few minutes mapping Vagrant and VirtualBox environments.

I typically have lots of Vagrant environments defined. I love Vagrant as a technology, it makes it super easy to spin up Virtual Machines (VMs) and learn about new technologies.

Said Vagrant environments obviously show up as VMs in VirtualBox. To make it more interesting I have a few more VirtualBox VMs that don’t map to a Vagrant environment. Adding in a naming convention that’s been growing organically over time I occasionally find myself at a loss as to which VirtualBox VM maps to a Vagrant environment. Can this be done? Yep, and creating a mapping is quite simple actually. Here is what I found useful.

Directory structure

My Vagrant directory structure is quite simple: I defined ${HOME}/vagrant as top-level directory with a sub-directory containing all my (custom) boxes. Apart from ~/vagrant/boxes I create further sub-directories for each project. For example:

[martin@ryzen: vagrant]$ ls -ld *oracle* boxes
drwxrwxr-x 2 martin martin 4096 Nov 23 16:52 boxes
drwxrwxr-x 3 martin martin   41 Feb 16  2021 oracle_19c_dg
drwxrwxr-x 3 martin martin   41 Nov 19  2020 oracle_19c_ol7
drwxrwxr-x 3 martin martin   41 Jan  6  2021 oracle_19c_ol8
drwxrwxr-x 3 martin martin   41 Nov 25 12:54 oracle_xe

But … which of my VirtualBox VMs belongs to the oracle_xe environment?

Mapping a Vagrant environment to a VirtualBox VM

Vagrant keeps a lot of metadata in the project’s .vagrant directory. Continuing with the oracle_xe example, here is what it stores:

[martin@buildhost: oracle_xe]$ tree .vagrant/
.vagrant/
├── machines
│   └── oraclexe
│       └── virtualbox
│           ├── action_provision
│           ├── action_set_name
│           ├── box_meta
│           ├── creator_uid
│           ├── id
│           ├── index_uuid
│           ├── synced_folders
│           └── vagrant_cwd
├── provisioners
│   └── ansible
│       └── inventory
│           └── vagrant_ansible_inventory
└── rgloader
    └── loader.rb

7 directories, 10 files

Looking at the above output I guess I should look at .vagrant/machines/

The machine name (oraclexe) is derived from the Vagrantfile. I create a config.vm.define section per VM out of habit (even when I create just 1 VM), as you can see here in my shortened Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  
  config.vm.define "oraclexe" do |xe|
    xe.vm.box = "ol7"
    xe.vm.box_url = "file:///home/martin/vagrant/boxes/ol7.json"

    ...

    xe.vm.provision "ansible" do |ansible|
      ansible.playbook = "setup.yml"
    end
  end
end

In case you don’t give your VMs a name you should find a directory named default instead.

As I’m using Vagrant together with VirtualBox I’m not surprised to find a sub-directory named virtualbox.

Finally! You see the VM’s metadata in that directory. The VM’s ID can be found in .vagrant/machines/oraclexe/virtualbox/id. The file contains the internal ID VirtualBox uses to identify VMs. Using that knowledge to my advantage I can create the lookup as shown here:

[martin@buildhost: oracle_xe]$ vboxmanage list vms | grep $(cat .vagrant/machines/oraclexe/virtualbox/id)
"oraclexe" {67031773-bad9-4325-937b-e471d02a56a3}

Voila! This wasn’t particularly hard since the VM name is oracelxe as well. Nevertheless I found this technique works well regardless of how you curated your Vagrantfile.

Happy Automating!

Configuring a VM using Ansible via the OCI Bastion Service

In my previous post I wrote about the creation of a Bastion Service using Terraform. As I’m incredibly lazy I prefer to configure the system pointed at by my Bastion Session with a configuration management tool. If you followed my blog for a bit you might suspect that I’ll use Ansible for that purpose. Of course I do! The question is: how do I configure the VM accessible via a Bastion Session?

Background

Please have a look at my previous post for a description of the resources created. In a nutshell the Terraform code creates a Virtual Cloud Network (VCN). There is only one private subnet in the VCN. A small VM without direct access to the Internet resides in the private subet. Another set of Terraform code creates a bastion session allowing me to connect to the VM.

I wrote this post on Ubuntu 20.04 LTS using ansible 4.8/ansible-core 2.11.6 by the way. From what I can tell these were current at the time of writing.

Connecting to the VM via a Bastion Session

The answer to “how does one connect to a VM via a Bastion Session?” isn’t terribly difficult once you know how to. The clue to my solution is with the SSH connection string as shown by the Terraform output variable. It prints the contents of oci_bastion_session.demo_bastionsession.ssh_metadata.command

$ terraform output
connection_details = "ssh -i <privateKey> -o ProxyCommand=\"ssh -i <privateKey> -W %h:%p -p 22 ocid1.bastionsession.oc1.eu-frankfurt-1.a...@host.bastion.eu-frankfurt-1.oci.oraclecloud.com\" -p 22 opc@10.0.2.39"

If I can connect to the VM via SSH I surely can do so via Ansible. As per the screen output above you can see that the connection to the VM relies on a proxy in form of the bastion session. See man 5 ssh_config for details. Make sure to provide the correct SSH keys in both locations as specified in the Terraform code. I like to think of the proxy session as a Jump Host to my private VM (its internal IP is 10.0.2.39). And yes, I am aware of alternative options to SSH, the one shown above however is the most compatible (to my knowledge).

Creating an Ansible Inventory and running a playbook

Even though it’s not the most flexible option I’m a great fan of using Ansible inventories. The use of an inventory saves me from typing a bunch of options on the command line.

Translating the Terraform output into the inventory format, this is what worked for me:

[blogpost]
privateinst ansible_host=10.0.2.39 ansible_user=opc ansible_ssh_common_args='-o ProxyCommand="ssh -i ~/.oci/oci_rsa -W %h:%p -p 22 ocid1.bastionsession.oc1.eu-frankfurt-1.a...@host.bastion.eu-frankfurt-1.oci.oraclecloud.com"'

Let’s run some Ansible code! Consider this playbook:

- hosts: blogpost
  tasks:
  - name: say hello
    ansible.builtin.debug:
      msg: hello from {{ ansible_hostname }}

With the inventory set, it’s now possible to run the playbook:

$ ansible-playbook -vi inventory.ini blogpost.yml 
Using /tmp/ansible/ansible.cfg as config file

PLAY [blogpost] *********************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************
ok: [privateinst]

TASK [say hello] ********************************************************************************************************
ok: [privateinst] => {}

MSG:

hello from privateinst

PLAY RECAP **************************************************************************************************************
privateinst                : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The playbook is of course very simple, but it can be easily extended. The tricky bit was establishing the connection, once the connection is established the sky is the limit!

Create an OCI bastion service via Terraform

Maintaining bastion hosts (a “jump box” or other network entry point directly exposed to the Internet) is somewhat frowned upon by security conscious architects, for good reasons. In my opinion the only way to connect on-premises systems to the cloud is by means of a dedicated, low-latency/high-bandwidth, and most importantly well-secured link.

I never liked the idea of exposing systems to the Internet – too much can go wrong and you’d be surprised about the number of port-scans you see, followed by attempts at breaking in. Sometimes of course opening a system to the Internet is unavoidable: a website offering services to the public is quite secure if it cannot be reached but won’t generate a lot of revenue that way. Thankfully there are ways to expose such applications safely to the Internet, a topic that’s out of scope of this post though.

My very personal need for the bastion service

I create lots of demos using Oracle Cloud Infrastructure (OCI) and setting up a dedicated link isn’t always practical. The solution for me is to use Oracle’s bastion service. This way I can ensure time-based secure access to my resources in a private subnet. Most importantly there is no need to connect a VM directly to the Internet. And since it’s all fully automated it doesn’t cause any more work than terraform up followed by a terraform destroy when the demo completed.

This blog post describes how I create a VCN with a private subnet containing a VM. The entire infrastructure is intended as a DEMO only. None of the resources will live longer than for the duration of a conference talk. Please don’t follow this approach if you would like to deploy systems in the cloud for > 45 minutes. Also be aware that it’s entirely possible for you to incur cost when calling terraform up on the code. As always, the code will be available on Github.

Creating a Bastion Service

The bastion service is created by Terraform. Following the advice from the excellent Terraform Up and Running (2nd ed) I separated the resource creation into three directories:

  • Network
  • Compute
  • Bastion

To keep things reasonably simple I refrained from creating modules.

Directory layout

Please have a look at the book for more details about the directory structure. You’ll notice that I simplified the example a little.

$ tree .
.
├── bastionsvc
│   ├── main.tf
│   ├── terraform.tfstate
│   └── variables.tf
├── compute
│   ├── compute.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   └── variables.tf
├── network
│   ├── network.tf
│   ├── outputs.tf
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   └── variables.tf
├── readme.md
└── variables.tf

I decided to split the network code into a generic section and the bastion service for reason explained later.

Generic Network Code

The network code is responsible for creating the Virtual Cloud Network (VCN) including subnets, security lists, necessary gateways etc. When I initially used the bastion service I struggled a bit with Network Security Groups (NSG) and went with a security list instead. I guess I should re-visit that decision at some point.

The network must be created first. In addition to creating all the necessary infrastructure it exports an output variable used by the compute and bastion code. These read remote state to get the necessary OCIDs.

Note that the choice of a remote data source has its drawbacks as described in the documentation. These don’t apply for my demos as I’m the only user of the code. And while I’m at it, using local state is acceptable only because I know I’m the only one using the code. Local state doesn’t necessarily work terribly well for team-development.

Here are some key features of the network code. As these tend to go stale over time, have a look at the Github repository for the latest and greatest revision.

resource "oci_core_vcn" "vcn" {

  compartment_id = var.compartment_ocid
  cidr_block     = "10.0.2.0/24"
  defined_tags   = var.network_defined_tags
  display_name   = "demovcn"
  dns_label      = "demo"

}

# --------------------------------------------------------------------- subnet

resource "oci_core_subnet" "private_subnet" {

  cidr_block                 = var.private_sn_cidr_block
  compartment_id             = var.compartment_ocid
  vcn_id                     = oci_core_vcn.vcn.id
  defined_tags               = var.network_defined_tags
  display_name               = "private subnet"
  dns_label                  = "private"
  prohibit_public_ip_on_vnic = true
  prohibit_internet_ingress  = true
  route_table_id             = oci_core_route_table.private_rt.id
  security_list_ids          = [
    oci_core_security_list.private_sl.id
  ]
}

The security list allows SSH only from within the same subnet:

# --------------------------------------------------------------------- security list

resource "oci_core_security_list" "private_sl" {

  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.vcn.id

...

  egress_security_rules {

    destination = var.private_sn_cidr_block
    protocol    = "6"

    description      = "SSH outgoing"
    destination_type = ""

    stateless = false
    tcp_options {

      max = 22
      min = 22

    }
  }

  ingress_security_rules {

    protocol = "6"
    source   = var.private_sn_cidr_block

    description = "SSH inbound"

    source_type = "CIDR_BLOCK"
    tcp_options {

      max = 22
      min = 22

    }

  }
}

The bastion service and its corresponding session are going to be created in the same private subnet as the compute instance for the sake of simplicity.

Compute Instance

The compute instance is created as a VM.Standard.E3.Flex shape with 2 OCPUs. There’s nothing too special about the resource, except maybe that I’m explicitly enabling the bastion plugin agent, a prerequisite for using the service.

resource "oci_core_instance" "private_instance" {
  agent_config {
    is_management_disabled = false
    is_monitoring_disabled = false

...

    plugins_config {
      desired_state = "ENABLED"
      name = "Bastion"
    }
  }

  defined_tags = var.compute_defined_tags

  create_vnic_details {
    
    assign_private_dns_record = true
    assign_public_ip = false
    hostname_label = "privateinst"
    subnet_id = data.terraform_remote_state.network_state.outputs.private_subnet_id
    nsg_ids = []
  }

...

Give it a couple of minutes for all agents to start.

Bastion Service

Once the VM’s bastion agent is up it is possible to create the bastion service:

resource "oci_bastion_bastion" "demo_bastionsrv" {

  bastion_type     = "STANDARD"
  compartment_id   = var.compartment_ocid
  target_subnet_id = data.terraform_remote_state.network_state.outputs.private_subnet_id

  client_cidr_block_allow_list = [
    var.local_laptop_id
  ]

  defined_tags = var.network_defined_tags

  name = "demobastionsrv"
}


resource "oci_bastion_session" "demo_bastionsession" {

  bastion_id = oci_bastion_bastion.demo_bastionsrv.id
  defined_tags = var.network_defined_tags
  
  key_details {
  
    public_key_content = var.ssh_bastion_key
  }

  target_resource_details {

    session_type       = "MANAGED_SSH"
    target_resource_id = data.terraform_remote_state.compute_state.outputs.private_instance_id

    target_resource_operating_system_user_name = "opc"
    target_resource_port                       = "22"
  }

  session_ttl_in_seconds = 3600

  display_name = "bastionsession-private-host"
}

output "connection_details" {
  value = oci_bastion_session.demo_bastionsession.ssh_metadata.command
}

The Bastion is set up in the private subnet created by the network code. Note that I’m defining the session’s client_cidr_block_allow_list specifically to only allow my external IP to access the service. The session is of type Managed SSH, thus requires a Linux host.

And this is all I can say about the creation of a bastion session in Terraform.

Terraform in action

Once all the resources have been created all I need to do is adapt the SSH command provided by my output variable shown here:

connection_details = "ssh -i <privateKey> -o ProxyCommand=\"ssh -i <privateKey> -W %h:%p -p 22 ocid1.bastionsession.oc1.eu-frankfurt-1.am...@host.bastion.eu-frankfurt-1.oci.oraclecloud.com\" -p 22 opc@10.0.2.94"

After adopting the SSH command I can connect to the instance.

$ ssh -i ...
The authenticity of host '10.0.2.94 (<no hostip for proxy command>)' can't be established.
ECDSA key fingerprint is SHA256:Ot...
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.0.2.94' (ECDSA) to the list of known hosts.
Activate the web console with: systemctl enable --now cockpit.socket

[opc@privateinst ~]$ hostname
privateinst
[opc@privateinst ~]$ logout

That’s it! I am connected to the instance and experiment with my demo.

Another reason I love Terraform: when the demo has concluded I can simply tear down all resources with very few commands.

Building a Debian 11 Vagrant Box using Packer and Ansible

Sometimes it might be necessary to create one’s own Vagrant base box for reasons too numerous to mention here. Let’s assume you want to build a new base box for Debian 11 (bullseye) to run on Virtualbox. Previously I would have run through the installation process followed by customising the VM’s installed packages and installing Guest Additions before creating the base box. As it turns out, this repetitive (and boring) process isn’t required as pretty much the whole thing can be automated using Packer.

Debian 11 is still quite new and a few things related to the Guest Additions don’t work yet but it can’t hurt to be prepared.

As I’m notoriously poor at keeping my code in sync between my various computers I created a new repository on Github for sharing my Packer builds. If you are interested head over to https://github.com/martincarstenbach/packer-blogposts. As with every piece of code you find online, it’s always a good idea to vet it first before even considering using it. Kindly take the time to read the license as well as the README associated with the repository in addition to this post.

Please note this is code I wrote for myself, a little more generic than it might have to be but ultimately you’ll have to read the code and adjust it for your own purposes. The preseed and kickstart files are specifically single-purpose only and shouldn’t be used for anything other than what is covered in this post. My Debian 11 base box is true to the word: it’s really basic, apart from SSH and the standard utilities (+ Virtualbox Guest Additions) I decided not to include anything else.

Software Releases

I should have added that I used Packer’s Virtualbox ISO builder. It is documented in great detail at the Packer website. Further software used:

  • Ubuntu 20.04 LTS
  • Ansible 2.9
  • Packer 1.7.4
  • Virtualbox 6.1.26

All of these were current at the time of writing.

Preparing the Packer build JSON and Debian Preseed file

I have missed the opportunity of creating all my computer systems with the same directory structure, hence there are small, subtle differences. To accommodate all of these I created a small shell script, prepare-debian11.sh. This script prompts me for the most important pieces of information and creates both the preseed file as well as the JSON build-file required by Packer.

martin@ubuntu:~/packer-blogposts$ bash prepare-debian11.sh 

INFO: preparing your packer environment for the creation of a Debian 11 Vagrant base box

Enter your local Debian mirror (http://ftp2.de.debian.org): 
Enter the mirror directory (/debian): 

/home/martin/.ssh/id_rsa.pub

Enter the full path to your public SSH key (/home/martin/.ssh/id_rsa.pub): 
Identity added: /home/martin/.ssh/id_rsa (/home/martin/.ssh/id_rsa)
Enter the location of the Debian 11 network installation media (/m/stage/debian-11.0.0-amd64-netinst.iso):
Enter the full path to store the new vagrant box (/home/martin/vagrant/boxes/debian-11-01.box):/home/martin/vagrant/boxes/blogpost.box    

INFO: preparation complete, next run packer validate vagrant-debian-11.json && packer build vagrant-debian-11.json

One of the particularities of my Packer builds is the use of agent authentication. My number 1 rule when coding is to never store authentication details in files if it can be avoided at all. Relying on the SSH agent to connect to the Virtualbox VM while it’s created allows me to do that, at least for Packer. Since I tend to forget adding my Vagrant SSH key to the agent, the prepare-script does that for me.

Sadly I have to store the vagrant user’s password in the preseed file. I can live with that this time as the password should be “vagrant” by convention and I didn’t break with it. Out of habit I encrypted the password anyway, it’s one of these industry best-known-methods worth applying every time.

Building the Vagrant Base Box

Once the build file and its corresponding preseed file are created by the prepare-script, I suggest you review them first before taking any further action. Make any changes you like, then proceed by running a packer validate followed by the packer build command once you understood/agree with what’s happening next. The latter of the 2 commands kicks the build off, and you’ll see the magic of automation for yourself ;)

Here is a sample of one of my sessions:

martin@ubuntu:~/packer-blogposts$ packer build vagrant-debian-11.json
virtualbox-iso: output will be in this color.

==> virtualbox-iso: Retrieving Guest additions
==> virtualbox-iso: Trying /usr/share/virtualbox/VBoxGuestAdditions.iso
==> virtualbox-iso: Trying /usr/share/virtualbox/VBoxGuestAdditions.iso
==> virtualbox-iso: /usr/share/virtualbox/VBoxGuestAdditions.iso => /usr/share/virtualbox/VBoxGuestAdditions.iso
==> virtualbox-iso: Retrieving ISO
==> virtualbox-iso: Trying file:///m/stage/debian-11.0.0-amd64-netinst.iso
==> virtualbox-iso: Trying file:///m/stage/debian-11.0.0-amd64-netinst.iso?checksum=sha256%3Aae6d563d2444665316901fe7091059ac34b8f67ba30f9159f7cef7d2fdc5bf8a
==> virtualbox-iso: file:///m/stage/debian-11.0.0-amd64-netinst.iso?checksum=sha256%3Aae6d563d2444665316901fe7091059ac34b8f67ba30f9159f7cef7d2fdc5bf8a => /m/stage/debian-11.0.0-amd64-netinst.iso
==> virtualbox-iso: Starting HTTP server on port 8765
==> virtualbox-iso: Using local SSH Agent to authenticate connections for the communicator...
==> virtualbox-iso: Creating virtual machine...
==> virtualbox-iso: Creating hard drive output-virtualbox-iso-debian11base/debian11base.vdi with size 20480 MiB...
==> virtualbox-iso: Mounting ISOs...
    virtualbox-iso: Mounting boot ISO...
==> virtualbox-iso: Creating forwarded port mapping for communicator (SSH, WinRM, etc) (host port 2302)
==> virtualbox-iso: Executing custom VBoxManage commands...
    virtualbox-iso: Executing: modifyvm debian11base --memory 2048
    virtualbox-iso: Executing: modifyvm debian11base --cpus 2
==> virtualbox-iso: Starting the virtual machine...
==> virtualbox-iso: Waiting 10s for boot...
==> virtualbox-iso: Typing the boot command...
==> virtualbox-iso: Using SSH communicator to connect: 127.0.0.1
==> virtualbox-iso: Waiting for SSH to become available...
==> virtualbox-iso: Connected to SSH!
==> virtualbox-iso: Uploading VirtualBox version info (6.1.26)
==> virtualbox-iso: Uploading VirtualBox guest additions ISO...
==> virtualbox-iso: Provisioning with Ansible...
    virtualbox-iso: Setting up proxy adapter for Ansible....
==> virtualbox-iso: Executing Ansible: ansible-playbook -e packer_build_name="virtualbox-iso" -e packer_builder_type=virtualbox-iso -e packer_http_addr=10.0.2.2:8765 --ssh-extra-args '-o IdentitiesOnly=yes' -e ansible_ssh_private_key_file=/tmp/ansible-key610730318 -i /tmp/packer-provisioner-ansible461216853 /home/martin/devel/packer-blogposts/ansible/vagrant-debian-11-guest-additions.yml
    virtualbox-iso:
    virtualbox-iso: PLAY [all] *********************************************************************
    virtualbox-iso:
    virtualbox-iso: TASK [Gathering Facts] *********************************************************
    virtualbox-iso: ok: [default]
    virtualbox-iso: [WARNING]: Platform linux on host default is using the discovered Python
    virtualbox-iso: interpreter at /usr/bin/python3, but future installation of another Python
    virtualbox-iso: interpreter could change this. See https://docs.ansible.com/ansible/2.9/referen
    virtualbox-iso: ce_appendices/interpreter_discovery.html for more information.
    virtualbox-iso:
    virtualbox-iso: TASK [install additional useful packages] **************************************
    virtualbox-iso: changed: [default]
    virtualbox-iso:
    virtualbox-iso: TASK [create a temporary mount point for vbox guest additions] *****************
    virtualbox-iso: changed: [default]
    virtualbox-iso:
    virtualbox-iso: TASK [mount guest additions ISO read-only] *************************************
    virtualbox-iso: changed: [default]
    virtualbox-iso:
    virtualbox-iso: TASK [execute guest additions script] ******************************************
    virtualbox-iso: changed: [default]
    virtualbox-iso:
    virtualbox-iso: TASK [unmount guest additions ISO] *********************************************
    virtualbox-iso: changed: [default]
    virtualbox-iso:
    virtualbox-iso: TASK [remove the temporary mount point] ****************************************
    virtualbox-iso: ok: [default]
    virtualbox-iso:
    virtualbox-iso: TASK [upgrade all packages] ****************************************************
    virtualbox-iso: ok: [default]
    virtualbox-iso:
    virtualbox-iso: PLAY RECAP *********************************************************************
    virtualbox-iso: default                    : ok=8    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    virtualbox-iso:
==> virtualbox-iso: Gracefully halting virtual machine...
==> virtualbox-iso: Preparing to export machine...
    virtualbox-iso: Deleting forwarded port mapping for the communicator (SSH, WinRM, etc) (host port 2302)
==> virtualbox-iso: Exporting virtual machine...
    virtualbox-iso: Executing: export debian11base --output output-virtualbox-iso-debian11base/debian11base.ovf
==> virtualbox-iso: Cleaning up floppy disk...
==> virtualbox-iso: Deregistering and deleting VM...
==> virtualbox-iso: Running post-processor: vagrant
==> virtualbox-iso (vagrant): Creating a dummy Vagrant box to ensure the host system can create one correctly
==> virtualbox-iso (vagrant): Creating Vagrant box for 'virtualbox' provider
    virtualbox-iso (vagrant): Copying from artifact: output-virtualbox-iso-debian11base/debian11base-disk001.vmdk
    virtualbox-iso (vagrant): Copying from artifact: output-virtualbox-iso-debian11base/debian11base.ovf
    virtualbox-iso (vagrant): Renaming the OVF to box.ovf...
    virtualbox-iso (vagrant): Compressing: Vagrantfile
    virtualbox-iso (vagrant): Compressing: box.ovf
    virtualbox-iso (vagrant): Compressing: debian11base-disk001.vmdk
    virtualbox-iso (vagrant): Compressing: metadata.json
Build 'virtualbox-iso' finished after 13 minutes 43 seconds.

==> Wait completed after 13 minutes 43 seconds

==> Builds finished. The artifacts of successful builds are:
--> virtualbox-iso: 'virtualbox' provider box: /home/martin/vagrant/boxes/blogpost.box

The operations should complete with the message shown in the output – build complete, box created and added in the directory specified. From that point onward you can add it to your inventory.

Happy Automation!

Install the Oracle Cloud Infrastructure CLI on Ubuntu 20.04 LTS

This is a short post on how to install/configure the Oracle Cloud Infrastructure (OCI) Command Line Interface (CLI) on Ubuntu 20.04 LTS. On a couple of my machines I noticed the default Python3 interpreter to be 3.8.x, so I’ll stick with this version. I used the Manual installation, users with higher security requirements might want to consider the offline installation.

Creating a virtual environment

The first step is to create a virtual environment to prevent the OCI CLI’s dependencies from messing up my python installation.

[martin@ubuntu: python]$ mkdir -p ~/development/python && cd ~/development/python
[martin@ubuntu: python]$ python3 -m venv oracle-cli

If this command throws an error you may have to install the virtual-env module via sudo apt install python3.8-venv

With the venv in place you need to activate it. This is a crucial step! Don’t forget to run it

[martin@ubuntu: python]$ source oracle-cli/bin/activate
(oracle-cli) [martin@ubuntu: python]$ 

As soon as the venv is activated you’ll notice its name has become a part of the prompt.

Downloading the OCI CLI

The next step is to download the latest OCI CLI release from Github. At the time of writing version 3.0.2 was the most current. Ensure you load the vanilla release, eg oci-cli-release.zip, not one of the distribution specific ones. They are to be used with the offline installation.

(oracle-cli) [martin@ubuntu: python]$ curl -L "https://github.com/oracle/oci-cli/releases/download/v3.0.2/oci-cli-3.0.2.zip" -o /tmp/oci-cli-3.0.2.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   623  100   623    0     0   2806      0 --:--:-- --:--:-- --:--:--  2793
100 52.4M  100 52.4M    0     0  5929k      0  0:00:09  0:00:09 --:--:-- 6311k
(oracle-cli) [martin@ubuntu: python]$ 

Unzip the release in a temporary location and begin the installation by invoking pip using the “whl” file in the freshly unzipped directory. Just to make sure I always double-check I’m using the pip executable in the virtual environment before proceeding.

(oracle-cli) [martin@ubuntu: python]$ which pip
/home/martin/development/python/oracle-cli/bin/pip
(oracle-cli) [martin@ubuntu: python]$ pip install /tmp/oci-cli/oci_cli-3.0.2-py3-none-any.whl 
Processing /tmp/oci-cli/oci_cli-3.0.2-py3-none-any.whl
Collecting arrow==0.17.0
  Downloading arrow-0.17.0-py2.py3-none-any.whl (50 kB)
     |████████████████████████████████| 50 kB 2.7 MB/s 
...

You’ll notice additional packages are pulled into the virtual environment by the setup routine. As always, exercise care when using external packages. An offline installation is available as well if your security requirements mandate it.

At the end of the process you have a working installation of the command line interface.

Configuration

Before you can use the CLI you need to provide a configuration file. The default location is ~/.oci, which I’ll use as well.

(oracle-cli) [martin@ubuntu python]$ mkdir ~/.oci && cd ~/.oci

Inside of this directory you need to create a config file; the example below is taken from the documentation and should provide a starting point.

[DEFAULT]
user=ocid1.user.oc1..<unique_ID>
fingerprint=<your_fingerprint>
key_file=~/.oci/oci_api_key.pem
tenancy=ocid1.tenancy.oc1..<unique_ID>
region=us-ashburn-1

Make sure to update the values accordingly. Should you be unsure about the user OCID and/or API signing key to use, have a look at the documentation for instructions. Next time you invoke the CLI the DEFAULT configuration will be used. It is possible to add multiple configurations using the old Windows 3.11 .ini file format.

[DEFAULT]
user=...

[ANOTHERUSER]
user=...

Note that it’s strongly discouraged to store a potential passphrase (used for the API key) in the configuration file!

Happy Automation!

Oracle Cloud Infrastructure: using the CLI to manipulate Network Security Groups

I frequently need to update security rules in one of my Network Security Groups (NSG). Rather than logging into the console and clicking my way through the user interface to eventually change the rule I decided to give it a go and automate the process using the Oracle Cloud Infrastructure (OCI) Command Line Interface (CLI). It took me slightly longer than I thought to get it right, so hopefully this post saves you 5 minutes. And me, later, when I forgot how I did it :)

In my defense I should point out this isn’t one of the terraform controlled environments I use but rather a cloud playground with a single network, a few of subnets, Network Security Groups (NSG) and security lists that have grown organically. If that sounds similar to what you are doing, read on. If not, please use terraform to control the state of your cloud infrastructure, it’s much better suited to the task, especially when working with others. The rule is: “once terraform, always terraform” when making changes to the infrastructure.

I have used Ubuntu 20.04 LTS as a host for version 3.0.0 of the CLI, the current version at the time of writing. It’s assumed you already set the CLI up and have the correct access policies granted to you to make changes to the NSG. I also defined a default compartment in ~/.oci/oci_cli_rc so I don’t have to add a --compartment-id to every call to the CLI.

Listing Network Security Groups

The landing page for NSGs in OCI CLI was my starting point. The list and rules list/rules update verbs are exactly what I need.

Before I can list the security rules for a given NSG I need to find its Oracle Cloud ID (OCID) first:

(oracle-cli) [martin@ubuntu: ~]$ oci network nsg list \
> --query 'data[].{id:id,"display-name":"display-name" }' \
> --output table
+-----------------------+-------------------------------------------------...---+
| display-name          | id                                              ...   |
+-----------------------+-------------------------------------------------...---+
| NSG1                  | ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aa...vq |
| NSG2                  | ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aa...5q |
...
| NSG5                  | ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aa...vq |
| NSG6                  | ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aa...3a |
+-----------------------+-------------------------------------------------...---+
(oracle-cli) [martin@ubuntu: ~]$ 

The table provides me with a list of NSGs and their OCIDs.

Getting a NSG’s Security Rules

Now that I have the NSG’s OCID, I can list its security rules:

(oracle-cli) [martin@ubuntu: ~]$ oci network nsg rules list \
> --nsg-id ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aa...

The result is a potentially looong JSON document, containing a data[] array with the rules and their metadata:

(oracle-cli) [martin@ubuntu: ~]oci network nsg rules list --nsg-id ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aa...
{
  "data": [
    {
      "description": "my first rule",
...

Updating a Security Rule

As per the documentation, I need to pass the NSG OCID as well as security rules to oci network nsg rules update. Which makes sense when you think about it … There is only one small caveat: the security rules are considered a complex type (= JSON document). Rather than passing a string on the command line, the suggestion is to create a JSON document with the appropriate parameters, store it on the file system and pass it via the file://payload.json directive.

But what exactly do I have to provide as part of the update request? The first thing I did was to look at the JSON document produced by oci network nsg rules list to identify the rule and payload I need to update. The documentation wasn’t 100% clear whether I can update just a single security rule so I thought I’d just try it. The API documentation has details about the various properties as well as links to the TcpOptions and UdpOptions. Not all of these are always required, have a look at the documentation for details. Using all the available sources I ended up with the following in /tmp/payload.json:

[
    {
        "description": "my first SSH rule",
        "direction": "INGRESS",
        "id": "04ABEC",
        "protocol": "6",
        "source": "192.168.10.0/24",
        "source-type": "CIDR_BLOCK",
        "tcp-options": {
            "destination-port-range": {
                "max": 22,
                "min": 22
            }
        }
    }
]

The actual contents of the file varies from use case to use case, however there are a couple of things worth pointing out:

  • Even though I intend to update a single rule, I need to provide a JSON array (containing a single object, the rule)
  • The security rule must be valid JSON
  • You absolutely NEED an id, otherwise OCI can’t update the existing rule

With these things in mind you can update the rule:

(oracle-cli) [martin@ubuntu: ~]$ oci network nsg rules update \
> --nsg-id ocid1.networksecuritygroup.oc1.eu-frankfurt-1.aaa... \
> --security-rules file:///tmp/payload.json 
{
  "data": {
    "security-rules": [
      {
        "description": "my first rule",
        "destination": null,
        "destination-type": null,
        "direction": "INGRESS",
        "icmp-options": null,
        "id": "04ABEC",
        "is-stateless": false,
        "is-valid": true,
        "protocol": "6",
        "source": "192.168.10.0/24",
        "source-type": "CIDR_BLOCK",
        "tcp-options": {
          "destination-port-range": {
            "max": 22,
            "min": 22
          },
          "source-port-range": null
        },
        "time-created": "2020-11-23T14:24:55.363000+00:00",
        "udp-options": null
      }
    ]
  }
}

In case of success you are presented with a JSON document listing the updated rule(s).

Automating Vagrant Box versioning

The longer I work in IT the more I dislike repetitive processes. For example, when updating my Oracle Linux 8 Vagrant Base Box I repeat the same process over and over:

  • Boot the VirtualBox (source) VM
  • Enable port forwarding for SSH
  • SSH to the VM to initiate the update via dnf update -y && reboot
  • Run vagrant package, calculate the SHA256 sum, modify the metadata file
  • Use vagrant box update to make it known to vagrant

There has to be a better way to do that, and in fact there is. A little bit of shell scripting later all I need to do is run my “update base box” script, and grab a coffee while it’s all done behind the scenes. The most part of the exercise laid out above is quite boring, but I thought I’d share how I’m modifying the metadata file in the hope to save you a little bit of time and effort. If you would like a more thorough explanation of the process please head over to my previous post.

Updating the Metadata File

If you would like to version-control your vagrant boxes locally, you need a metadata file, maybe something similar to ol8.json shown below. It defines my Oracle Linux 8 boxes (at the moment there is only one):

$ cat ol8.json 
{
  "name": "ol8",
  "description": "Martins Oracle Linux 8",
  "versions": [
    {
      "version": "8.4.0",
      "providers": [
        {
          "name": "virtualbox",
          "url": "file:///vagrant/boxes/ol8_8.4.0.box",
          "checksum": "b28a3413d33d4917bc3b8321464c54f22a12dadd612161b36ab20754488f4867",
          "checksum_type": "sha256"
        }
      ]
    }
  ]
}

For the sake of argument, let’s assume I want to upgrade my Oracle Linux 8.4.0 box to the latest and greatest packages that were available at the time of writing. As it’s a minor update I’ll call the new version 8.4.1. To keep the post short and (hopefully) entertaining I’m skipping the upgrade of the VM.

Option (1): jq

Fast forward to the metadata update: I need to add a new element to the versions array. I could have used jq for that purpose and it would have been quite easy:

$ jq '.versions += [{
>       "version": "8.4.1",
>       "providers": [
>         {
>           "name": "virtualbox",
>           "url": "file:///vagrant/boxes/ol8_8.4.1.box",
>           "checksum": "ecb3134d7337a9ae32c303e2dee4fa6e5b9fbbea5a38084097a6b5bde2a56671",
>           "checksum_type": "sha256"
>         }
>       ]
>     }]' ol8.json
{
  "name": "ol8",
  "description": "Martins Oracle Linux 8",
  "versions": [
    {
      "version": "8.4.0",
      "providers": [
        {
          "name": "virtualbox",
          "url": "file:///vagrant/boxes/ol8_8.4.0.box",
          "checksum": "b28a3413d33d4917bc3b8321464c54f22a12dadd612161b36ab20754488f4867",
          "checksum_type": "sha256"
        }
      ]
    },
    {
      "version": "8.4.1",
      "providers": [
        {
          "name": "virtualbox",
          "url": "file:///vagrant/boxes/ol8_8.4.1.box",
          "checksum": "ecb3134d7337a9ae32c303e2dee4fa6e5b9fbbea5a38084097a6b5bde2a56671",
          "checksum_type": "sha256"
        }
      ]
    }
  ]
}

That would be too easy ;) Sadly I don’t have jq available on all the systems I’d like to run this script on. But wait, I have Python available.

Option (2): Python

Although I’m certainly late to to the party I truly enjoy working with Python. Below you’ll find a (shortened) version of a Python script to take care of the metadata addition.

Admittedly it does a few additional things compared to the very basic jq example. For instance, it takes a backup of the metadata file, takes and parses command line arguments etc. It’s a bit longer than a one-liner though ;)

#!/usr/bin/env python3

# PURPOSE
# add metadata about a new box version to the metadata file
# should also work with python2

import json
import argparse
import os
import sys
from time import strftime
import shutil

# Parsing the command line. Use -h to print help
parser = argparse.ArgumentParser()
parser.add_argument("version",       help="the new version of the vagrant box to be added. Must be unique")
parser.add_argument("sha256sum",     help="the sha256 sum of the newly created package.box")
parser.add_argument("box_file",      help="full path to the package.box, eg /vagrant/boxes/ol8_8.4.1.box")
parser.add_argument("metadata_file", help="full path to the metadata file, eg /vagrant/boxes/ol8.json")
args = parser.parse_args()

# this is the JSON element to add
new_box_version = {
    "version": args.version,
    "providers": [
        {
            "name": "virtualbox",
            "url": "file://" + args.box_file,
            "checksum": args.sha256sum,
            "checksum_type": "sha256"
        }
    ]
}

...

# check if the box_file exists
if (not os.path.isfile(args.box_file)):
    sys.exit("FATAL: Vagrant box file {} does not exist".format(args.box_file))

# read the existing metadata file
try:
    with open(args.metadata_file, 'r+') as f:
        metadata = json.load(f)
except OSError as err:
    sys.exit ("FATAL: Cannot open the metadata file {} for reading: {}".format(args.metadata_file, err))

# check if the version to be added exists already. 
all_versions =  metadata["versions"]
if args.version in all_versions.__str__():
    sys.exit ("FATAL: new version {} to be added is a duplicate".format(args.version))

# if the new box doesn't exist already, it's ok to add it
metadata['versions'].append(new_box_version)

# create a backup of the existing file before writing
try:
    bkpfile = args.metadata_file + "_" + strftime("%y%m%d_%H%M%S")
    shutil.copy(args.metadata_file, bkpfile)
except OSError as err:
    sys.exit ("FATAL: cannot create a backup of the metadata file {}".format(err))

# ... and write changes to disk
try:
    with open(args.metadata_file, 'w') as f:
        json.dump(metadata, f, indent=2)
except OSError as err:
    sys.exit ("FATAL: cannot save metadata to {}: {}".format(args.metadata_file, err))

print("INFO: process completed successfully")

That’s it! Next time I need to upgrade my Vagrant boxes I can rely on a fully automated process, saving me quite a bit of time when I’m instantiating a new Vagrant-based environment.

Ansible tips’n’tricks: configuring the Ansible Dynamic Inventory for OCI – Oracle Linux 7

I have previously written about the configuration of the Ansible Dynamic Inventory for OCI. The aforementioned article focused on Debian, and I promised an update for Oracle Linux 7. You are reading it now.

The biggest difference between the older post and this one is the ability to use YUM in Oracle Linux 7. Rather than manually installing Ansible, the Python SDK and the OCI collection from Ansible Galaxy you can make use of the package management built into Oracle Linux 7 and Oracle-provided packages.

Warning about the software repositories

All the packages referred to later in the article are either provided by Oracle’s Extra Packages for Enterprise Linux (EPEL) repository or the development repo. Both repositories are listed in a section labelled “Packages for Test and Development“ in Oracle’s yum server. As per https://yum.oracle.com/oracle-linux-7.html, these packages come with the following warning:

Note: The contents in the following repositories are for development purposes only. Oracle suggests these not be used in production.

This is really important! Please make sure you understand the implications for your organisation. If this caveat is a show-stopper for you, please refer to the manual installation of the tools in my earlier article for an alternative approach.

I’m ok with the restriction as it’s my lab anyway, with myself as the only user. No one else to blame if things go wrong :)

Installing the software

You need to install a few packages from Oracle’s development repositories if you accept the warning quoted above. One of the components you will need – oci-ansible-collection – requires Python 3, so there is no need to install Ansible with support for Python 2.

The first step is to enable the necessary repositories:

sudo yum-config-manager --enable ol7_developer_EPEL
sudo yum-config-manager --enable ol7_developer

Once that’s done I can install the OCI collection. This package pulls all the other RPMs I need as dependencies.

[opc@dynInv ~]$ sudo yum install oci-ansible-collection

...

--> Finished Dependency Resolution

Dependencies Resolved

==================================================================================
 Package                  Arch     Version             Repository            Size
==================================================================================
Installing:
 oci-ansible-collection   x86_64   2.19.0-1.el7        ol7_developer        6.6 M
Installing for dependencies:
 ansible-python3          noarch   2.9.18-1.el7        ol7_developer_EPEL    16 M
 python3-jmespath         noarch   0.10.0-1.el7        ol7_developer         42 k
 python36-asn1crypto      noarch   0.24.0-7.el7        ol7_developer        179 k
 python36-cryptography    x86_64   2.3-2.el7           ol7_developer        501 k
 python36-idna            noarch   2.10-1.el7          ol7_developer_EPEL    98 k
 python36-jinja2          noarch   2.11.1-1.el7        ol7_developer_EPEL   237 k
 python36-markupsafe      x86_64   0.23-3.0.1.el7      ol7_developer_EPEL    32 k
 python36-paramiko        noarch   2.1.1-0.10.el7      ol7_developer_EPEL   272 k
 python36-pyasn1          noarch   0.4.7-1.el7         ol7_developer        173 k
 python36-pyyaml          x86_64   5.1.2-1.0.2.el7     ol7_developer        198 k
 python36-six             noarch   1.14.0-2.el7        ol7_developer_EPEL    33 k
 sshpass                  x86_64   1.06-1.el7          ol7_developer_EPEL    21 k

Transaction Summary
==================================================================================
Install  1 Package (+12 Dependent packages)

Total download size: 25 M
Installed size: 233 M
Is this ok [y/d/N]: 

Once all packages are installed you should be in the position to test the configuration. The article assumes the OCI Python SDK is already configured. If not, head over to the documentation for instructions on how to do so.

Verifying the installation

Out of habit I run ansible --version once the software is installed to make sure everything works as expected. Right after the installation I tried, but I noticed that Ansible seemingly wasn’t present:

[opc@dyninv ~]$ which ansible
/usr/bin/which: no ansible in (/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/opc/.local/bin:/home/opc/bin)

It is present though, and it took me a minute to understand the way Oracle packaged Ansible: Ansible/Python3 is found in ansible-python3 instead of ansible. A quick check of the package’s contents revealed that a suffix was added, for example:

[opc@dyninv ~]$ ansible-3 --version
ansible-3 2.9.18
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/opc/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible-3
  python version = 3.6.8 (default, Mar  9 2021, 15:08:44) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44.0.3)]
[opc@dyninv ~]$ 

An important detail can be found in the last line: the python version is reported to be 3.6.8, at least at it was at the time of writing.

Testing the Dynamic Inventory

Before going into details about the dynamic inventory, first I’d like to repeat a warning I had in my older post as well:

Remember that the use of the Dynamic Inventory plugin is a great time saver, but comes with a risk. If you aren’t careful, you can end up running playbooks against far too many hosts. Clever Identity and Access Management (IAM) and the use of filters in the inventory are a must to prevent accidents. And don’t ever use hosts: all in your playbooks! Principle of least privilege is key.

Ansible configuration

With the hard work completed and out of the way it’s time to test the dynamic inventory. First of all I need to tell Ansible to enable the Oracle collection. I’m doing this in ~/.ansible.cfg:

[opc@dyninv ansible]$ cat ~/.ansible.cfg 
[defaults]
stdout_callback = debug

[inventory]
enable_plugins = oracle.oci.oci

The next file to be created is the dynamic inventory file. It needs to be named following the Ansible convention:

filename.oci.yml.

You are only allowed to change the first part (“filename”) or else you get an error. The example file contains the following lines, limiting the output to a particular compartment and set of tags, following my own advice from above.

plugin: oracle.oci.oci

hostname_format: "fqdn"

filters:
 defined_tags: { "project": { "name": "simple-app" } } 

regions:
- eu-frankfurt-1

compartments:
- compartment_ocid: "ocid1.compartment.oc1..aaa...a"
  fetch_hosts_from_subcompartments: true

With the setup complete I can graph the inventory:

[opc@dyninv ansible]$ ansible-inventory-3 --inventory dynInv.oci.yml --graph
...
@all:
  |--@IHsr_EU-FRANKFURT-1-AD-2:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@IHsr_EU-FRANKFURT-1-AD-3:
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |--@all_hosts:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@ougdemo-department:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@project#name=simple-app:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@region_eu-frankfurt-1:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@tag_role=appserver:
  |  |--appserver1.app.simpleapp.oraclevcn.com
  |  |--appserver2.app.simpleapp.oraclevcn.com
  |--@tag_role=bastionhost:
  |  |--bastion1.bastion.simpleapp.oraclevcn.com
  |--@ungrouped:

Happy Automating!

Summary

It’s quite a time saver not having to install all components of the toolchain yourself. By pulling packages from Oracle’s yum repositories I can also count on updates being made available, providing many benefits such as security and bug fixes.

Device name persistence in the cloud: OCI + Terraform

This is a really short post (by my standards at least) demonstrating how I ensure device name persistence in Oracle Cloud Infrastructure (OCI). Device name persistence matters for many reasons, not the least for my Ansible scripts expecting a given block device to be of a certain size and used for a specific purpose. And I’m too lazy to write discovery code in Ansible, I just want to be able to use /dev/oracleoci/oraclevdb for LVM so that I can install the database.

The goal is to provision a VM with a sufficient number of block devices for use with the Oracle database. I wrote about the basics of device name persistence in December last year. In my earlier post I used the OCI Command Line Interface (CLI). Today I rewrote my code, switching from shell to Terraform.

As always I shall warn you that creating cloud resources as shown in this post will incur cost so please make sure you are aware of the fact. You should also be authorised to spend money if you use the code for your purposes.

Terraform Compute and Block Storage

When creating a VM in OCI, you make use of the oci_core_instance Terraform resource. Amongst the arguments you pass to it is the (operating system) image as well as the boot volume size. The boot volume is attached to the VM instance without any further input on your behalf.

Let’s assume you have already defined a VM resource named sitea_instance in your Terraform code.

I generally attach 5 block volumes to my VMs unless performance requirements mandate a different approach.

  • Block device number 1 hosts the database binaries
  • Devices number 2 and 3 are used for +DATA
  • The remaining devices (4 and 5) will be used for +RECO

Creating block volumes

The first step is to create block volumes. I know I want five, and I know they need to end up as /dev/oracleoci/oraclevd[b-f]. Since I’m pretty lazy I thought I’d go with some kind of loop instead of hard-coding 5 block devices. It should also allow for more flexibility in the long run.

I tried to use the count meta-argument but failed to get it to work the way I wanted. Which might be a PBKAC issue. The other option in Terraform is to use the for each meta-argument instead. This sounded a lot better for my purpose. To keep my code flexible I decided to store the future block devices’ names in a variable:

variable "block_volumes" {
    type = list(string)
    default = [ 
        "oraclevdb",
        "oraclevdc",
        "oraclevdd",
        "oraclevde",
        "oraclevdf"
    ]
}

Remember that Oracle assigns /dev/oracleoci/oraclevda to the boot volume. You definitely want to leave that one alone.

Next I’ll use the for-each block to get the block device name. I’m not sure if this is considered good code, all I know is that it does the job. The Terraform entity to create block devices is name oci_core_volume:

resource "oci_core_volume" "sitea_block_volume" {
  for_each = toset(var.block_volumes)

  availability_domain  = data.oci_identity_availability_domains.local_ads.availability_domains.0.name
  compartment_id       = var.compartment_ocid
  display_name         = "sitea-${each.value}"
  size_in_gbs          = 50

}

This takes care of creating 5 block volumes. On their own they aren’t very useful yet, they need to be attached to a VM.

Attaching block devices to the VM

In the next step I have to create a block device attachment. This is where the count meta-argument failed me as I couldn’t find a way to generate the persistent device name. I got around that issue using for-each, as shown here:

resource "oci_core_volume_attachment" "sitea_block_volume_attachement" {
  for_each = toset(var.block_volumes)

  attachment_type = "iscsi"
  instance_id     = oci_core_instance.sitea_instance.id
  volume_id       = oci_core_volume.sitea_block_volume[each.value].id
  device          = "/dev/oracleoci/${each.value}"
}

Using the contents of each.value I can refer to the block volume and also assign a suitable device name. Note that I’m specifying “iscsi” as the attachement type. Instead of the remote-exec provisioner I rely on cloud-init to make my iSCSI devices available to the VM.

The result

Once the Terraform script completes, I have a VM with block storage ready for Ansible provisioning scripts.

[opc@sitea ~]$ ls -l /dev/oracleoci/
total 0
lrwxrwxrwx. 1 root root 6 Mar 25 14:47 oraclevda -> ../sda
lrwxrwxrwx. 1 root root 7 Mar 25 14:47 oraclevda1 -> ../sda1
lrwxrwxrwx. 1 root root 7 Mar 25 14:47 oraclevda2 -> ../sda2
lrwxrwxrwx. 1 root root 7 Mar 25 14:47 oraclevda3 -> ../sda3
lrwxrwxrwx. 1 root root 6 Mar 25 14:51 oraclevdb -> ../sdc
lrwxrwxrwx. 1 root root 6 Mar 25 14:51 oraclevdc -> ../sdd
lrwxrwxrwx. 1 root root 6 Mar 25 14:51 oraclevdd -> ../sde
lrwxrwxrwx. 1 root root 6 Mar 25 14:51 oraclevde -> ../sdb
lrwxrwxrwx. 1 root root 6 Mar 25 14:51 oraclevdf -> ../sdf
[opc@sitea ~]$ lsblk
NAME               MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda                  8:0    0   50G  0 disk 
├─sda1               8:1    0  100M  0 part /boot/efi
├─sda2               8:2    0    1G  0 part /boot
└─sda3               8:3    0 48,9G  0 part 
  ├─ocivolume-root 252:0    0 38,9G  0 lvm  /
  └─ocivolume-oled 252:1    0   10G  0 lvm  /var/oled
sdb                  8:16   0   50G  0 disk 
sdc                  8:32   0   50G  0 disk 
sdd                  8:48   0   50G  0 disk 
sde                  8:64   0   50G  0 disk 
sdf                  8:80   0   50G  0 disk 

Summary

There are many ways to complete tasks, and cloud providers usually offer plenty of them. I previously wrote about ensuring device name persistence using the OCI CLI whereas this post covers Terraform. Looking back and comparing both I have to say that I like the new approach better.

Oracle Database Cloud Service: Create a database from backup using Terraform

A common DBA task is to ensure that a development-type environment is refreshed. In a typical on-premises case a “dev refresh” involves quite a bit of scripting in various programming languages. Whilst that’s a perfectly fine approach, it can be done a lot simpler when you consider the use of the cloud. My example uses Oracle’s Database Cloud Service (DBCS).

I prefix all my cloud posts with a similar warning, and this is no exception. Using cloud services costs money, so please make sure you are authorised to make use of these services. You also need to ensure you are licensed appropriately

The Scenario

I am recreating a typical scenario: a database backup acts as the source for the “DEV” environment. To keep this post simple-ish, let’s assume I can use the backup as it is. The database backup is located in Oracle Cloud Infrastructure (OCI) Object Storage.

Implementation

Writing a piece of Terraform code with the intention of storing it in version control requires the use of variables, at least in my opinion. Otherwise, any change to the input parameters will result in git marking the file’s status as untracked. And you certrainly don’t want to store passwords in code, ever.

You’ll see variables used throughout in my example code.

As I tend to forget how I did things I now pushed my code to my Github repository.

Getting backup details

Backup details for my source database are provided by a database backups data source. My requirement is quite simple: just take the latest backup and use it for the restore operation.

#
# get the database backups for src_db_ocid
#
data "oci_database_backups" "src_bkp" {
  database_id    = var.src_db_ocid
} 

The database backup to grab is element 0 in the resulting list of backups provided by the data source.

Thinking about passwords

Passwords are a tricky affair in OCI. It would be great if we could lift them from (OCI) Vault, but this wasn’t possible at the time of writing. A Github issue has been raised but didn’t seem to gain much momentum. There are workarounds though, please refer to this excellent post by Yevgeniy Brikman on the topic. I’ll leave it as an exercise to the reader to work out the best strategy.

Since Terraform v0.14 it is possible to declare a variable to be “sensitive”. That sounds great:

variable "new_admin_pwd" {
  type      = string
  sensitive = true
}

variable "backup_tde_password" {
  type      = string
  sensitive = true
}

Except they aren’t quite there yet: all sensitive information still appears in the state file in plain text :(

Creating the DB System

The final step is to create the database system. In my case, I only need a single resource:

resource "oci_database_db_system" "dev_system" {

  # the AD of the new environment has to match the AD where
  # the backup is stored (a property exported by the data source)
  availability_domain = data.oci_database_backups.src_bkp.backups.0.availability_domain

  # instruction to create the database from a backup
  source = "DB_BACKUP"

  # Some of these properties are hard-coded to suit my use case. 
  # Your requirement is almost certainly different. Make sure you
  # change paramaters as required
  compartment_id          = var.compartment_ocid
  database_edition        = "ENTERPRISE_EDITION"
  data_storage_size_in_gb = 256
  hostname                = "dev"
  shape                   = "VM.Standard2.1"
  node_count              = 1
  ssh_public_keys         = [var.ssh_public_key]
  subnet_id               = var.subnet_id
  nsg_ids                 = [var.nsg_id]
  license_model           = "LICENSE_INCLUDED"

  display_name = "development DB system"

  db_home {

    database {
      # the admin password for the _new_ database
      admin_password = var.new_admin_pwd

      # this is from the source backup!
      backup_tde_password = var.backup_tde_password
      backup_id           = data.oci_database_backups.src_bkp.backups.0.id

      db_name = "DEV"
    }

  }

  db_system_options {
    storage_management = "ASM"
  }
}

This is all it takes. The majority of input parameters are provided as variables to make the script a little more portable between environments and easier to check in with version control.

A short terraform apply later a new database system is created. Happy Automating!