Ansible

How to build OCI Infrastructure Environments with Ansible

The Oracle provided Ansible module gives us the opportunity to provision and configure Oracle Cloud Infrastructure resources on an automated base. The Ansible basic setup is very easy and the Oracle provided example playbooks in Git are a good base to start with your infrastructure automation project. Oracle provides Ansible example playbooks for

  • Block Volumes
  • Compute 
  • Database
  • File Storage
  • IAM
  • Load Balancer
  • Private Subnets with VPN
  • Delete Objects
  • etc.

In this blog post, I will show you how easy it is to bring Ansible and the Oracle Cloud Infrastructure together. 

Requirements

  • A local machine to install Ansible and the required software and modules, in my case it’s an Oracle Linux 7 virtual machine with Internet access.
  • An Oracle Cloud Infrastructure Account with permissons to create new resources.

Steps to configure Ansible and OCI

  1. Install and configure the Oracle Cloud Infrastructure Python SDK
  2. Install and configure Ansible
  3. Download and configure the OCI modules for Ansible
  4. OCI Test Run

Install and configure the Oracle Cloud Infrastructure Python SDK

In this step, the OCI Python SDK will be installed an configured. The new created SSH public key has to be added in the OCI console for further actions.  As OS user root we create a new operating system user called oci for Oracle Cllud Infrastrcuture actions and give him sudo privileges.

Create a User and SSH Keys

# groupadd oci
# useradd oci -g oci
# passwd oci

Add this line in /etc/sudoers.

oci ALL=(ALL) ALL

Login as user oci, create a new SSH key and download an configure the OCI SDK. Protect your keys.

$ mkdir ~/.oci
$ openssl genrsa -out ~/.oci/oci_api_key.pem 2048
$ chmod go-rwx ~/.oci/oci_api_key.pem
$ openssl rsa -pubout -in ~/.oci/oci_api_key.pem -out ~/.oci/oci_api_key_public.pem
$ chmod go-rwx ~/.oci/oci_api_key_public.pem
$ chmod go-rwx ~/.oci/oci_api_key.pem

Show the public key and add it in the OCI console to your cloud account user.

 

The OCI Configuration File

As user oci, create the Oracle Cloud Infrastructure configuration file

$ vi  ~/.oci/config

Content of the file – for example in region Frankfurt and with the created SSH key file from above.

[DEFAULT]
user=[OCID of your OCI User]
fingerprint=[API Key Fingerprint of added SSH public key]
tenancy=[Your tenancy id]
region=eu-frankfurt-1
key_file=~/.oci/oci_api_key.pem

Change the file permissions.

$ chmod 600 /home/oci/.oci/config

Install the Oracle Cloud Infrastructure Python SDK

$ sudo yum-config-manager --enable ol7_developer ol7_developer_epel
$ sudo yum -y install python-oci-sdk python-oci-cli

Test

Command to list all instances in the selected compartment.

$ oci compute instance list --compartment-id [OCID of your OCI Compartment]| grep display-name
     "display-name": "Instance-AS-Test-1"

Install and configure Ansible

As user oci, download and install Ansible and Git.

$ sudo yum -y install git ansible
$ sudo mkdir -p /usr/share/ansible/modules

Set up the module directory.

$ sudo vi /etc/ansible/ansible.cfg
library=/usr/share/ansible/modules

Install additional packages.

$ sudo yum install python-pip
$ sudo pip install --upgrade Jinja2

This upgrade step is required, otherwise the public key creation in the OCI Ansible module fails (for example when you want to launch a new Compute instance).

Download and configure the OCI modules for Ansible

As user oci, download the Ansible modules from Git.

$ cd /usr/share/ansible/modules
$ sudo git clone https://github.com/oracle/oci-ansible-modules.git

Show the content.

$ ls -la
total 4
drwxr-xr-x. 3 root root 33 Mar 11 13:58 .
drwxr-xr-x. 3 root root 21 Mar 11 13:56 ..
drwxr-xr-x. 12 root root 4096 Mar 11 13:58 oci-ansible-modules

Change into the new created directory and execute the configuration script install.py.

$ cd oci-ansible-modules
$ sudo ./install.py
Copying documentation fragments from /usr/share/ansible/modules/oci-ansible-modules/module_docs_fragments to /usr/lib/python2.7/site-packages/ansible/utils/module_docs_fragments
Copying oracle utility files from /usr/share/ansible/modules/oci-ansible-modules/module_utils/oracle to /usr/lib/python2.7/site-packages/ansible/module_utils/oracle
Creating directory /usr/lib/python2.7/site-packages/ansible/modules/cloud/oracle
Copying OCI Ansible modules from /usr/share/ansible/modules/oci-ansible-modules/library to /usr/lib/python2.7/site-packages/ansible/modules/cloud/oracle
OCI Ansible modules installed successfully.

OCI Test Run

We copy the example playbook to launch a Compute cloud instance into the local folder and run the playbook. The Oracle provided playbook needs three variables:

SAMPLE_AD_NAME Availability Domain, e.g. EUZg:EU-FRANKFURT-1-AD-1
SAMPLE_IMAGE_OCID OCID of the selected OS – see https://docs.cloud.oracle.com/iaas/images/ to list all available images
SAMPLE_COMPARTMENT_OCID OCID of your compartment – OCI > Identity > Compartments

 

Create a working directory and copy the example playbook.

$ sudo mkdir -p ~/ansible/playbooks cd ~/ansible/playbooks
$ cp -r /usr/share/ansible/modules/oci-ansible-modules/samples/compute/launch_compute_instance ~/ansible/playbooks

$ cd ~/ansible/playbooks/launch_compute_instance

Set variables.

$ export SAMPLE_AD_NAME=EUZg:EU-FRANKFURT-1-AD-1
$ export SAMPLE_IMAGE_OCID=ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq
$ export SAMPLE_COMPARTMENT_OCID=ocid1.compartment.oc1..aaaaaaaayc4703470347034703470347034703o3hx2exkz5pzi6kt4kunhiq

Run the playbook

Attention: All OCI resources are created and afterwards terminated immediately. If you don’t want to terminate them, comment out this line in file sample.yaml.

- import_tasks: teardown.yaml

Execute the Ansible playbook. The infrastructure will be created step by step. Key generation, network configuration, firewall rule setup, instance creation etc. is all automated.

$ ansible-playbook sample.yaml
 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAY [Launch a compute instance and connect to it using SSH] *******************************************************************************************************************

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

TASK [Check pre-requisites] ****************************************************************************************************************************************************
skipping: [localhost] => (item=SAMPLE_COMPARTMENT_OCID)
skipping: [localhost] => (item=SAMPLE_IMAGE_OCID)
skipping: [localhost] => (item=SAMPLE_AD_NAME)

TASK [Create a temporary directory to house a temporary SSH keypair we will later use to connect to instance] ******************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Generate a Private Key] **************************************************************************************************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Generate a Public Key] ***************************************************************************************************************************************************
changed: [localhost]

TASK [Create a VCN] ************************************************************************************************************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Create a new Internet Gateway] *******************************************************************************************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Create route table to connect internet gateway to the VCN] ***************************************************************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [create ingress rules yaml body] ******************************************************************************************************************************************
ok: [localhost]

TASK [create egress yaml body] *************************************************************************************************************************************************
ok: [localhost]

TASK [load the variables defined in the ingress rules yaml body] ***************************************************************************************************************
ok: [localhost]

TASK [print loaded_ingress] ****************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "loaded ingress is {u'instance_ingress_security_rules': [{u'source': u'0.0.0.0/0', u'protocol': u'6', u'tcp_options': {u'destination_port_range': {u'max': 22, u'min': 22}}}]}"
}

TASK [load the variables defined in the egress rules yaml body] ****************************************************************************************************************
ok: [localhost]

TASK [print loaded_egress] *****************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "loaded egress is {u'instance_egress_security_rules': [{u'tcp_options': {u'destination_port_range': {u'max': 22, u'min': 22}}, u'destination': u'0.0.0.0/0', u'protocol': u'6'}]}"
}

TASK [Create a security list for allowing access to public instance] ***********************************************************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Create a subnet to host the public instance. Link security_list and route_table.] ****************************************************************************************
changed: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Launch an instance] ******************************************************************************************************************************************************
changed: [localhost]

TASK [Print instance details] **************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "Launched a new instance {u'added_instances': [{u'lifecycle_state': u'RUNNING', u'availability_domain': u'EUZg:EU-FRANKFURT-1-AD-1', u'display_name': u'my_test_instance', u'time_maintenance_reboot_due': None, u'compartment_id': u'ocid1.compartment.oc1..aaaaaaaayc5kgqshdb5g2mjg4bnt34htnybbho3hx2exkz5pzi6kt4kunhiq', u'defined_tags': {}, u'region': u'eu-frankfurt-1', u'freeform_tags': {}, u'time_created': u'2019-03-11T13:52:34.238000+00:00', u'launch_options': {u'remote_data_volume_type': u'PARAVIRTUALIZED', u'firmware': u'UEFI_64', u'boot_volume_type': u'PARAVIRTUALIZED', u'is_consistent_volume_naming_enabled': True, u'network_type': u'VFIO', u'is_pv_encryption_in_transit_enabled': True}, u'image_id': u'ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq', u'source_details': {u'source_type': u'image', u'kms_key_id': None, u'boot_volume_size_in_gbs': None, u'image_id': u'ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq'}, u'fault_domain': u'FAULT-DOMAIN-3', u'shape': u'VM.Standard1.1', u'launch_mode': u'NATIVE', u'agent_config': {u'is_monitoring_disabled': False}, u'extended_metadata': {}, u'ipxe_script': None, u'id': u'ocid1.instance.oc1.eu-frankfurt-1.abtheljslxpqenvnafkstq2s7hxqizyam7shjd6kihyg33i7w2geeynedotq', u'metadata': {u'ssh_authorized_keys': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAA4703470347034703470347034703470347034703470347034703470347034703XR6Zcv4sHKdMH/aDfZKFaLpqHtJ2P+JEC0ok2lWrPQSG0zPoSYWTrldllpgSfzOv1kQ1R/Uor+eedRz9x2DXtKl0anSXY6KNW1GVfOvoFk/c8AuaQy6Y7AZnJAculyF1pRJiHQyZEPuBL9E9EsiRqkQR18G9yewBApRZf/QGXZbLydG8vAa9T1DCYrODb3N2u73IIqbMFwr7sgQ8XQb7h9wlYq2mfUKxy+8H4w7S67'}}], u'instances': [{u'lifecycle_state': u'RUNNING', u'availability_domain': u'EUZg:EU-FRANKFURT-1-AD-1', u'display_name': u'my_test_instance', u'time_maintenance_reboot_due': None, u'compartment_id': u'ocid1.compartment.oc1..aaaaaaaayc5kgqshdb5g2mjg4bnt34htnybbho3hx2exkz5pzi6kt4kunhiq', u'defined_tags': {}, u'region': u'eu-frankfurt-1', u'freeform_tags': {}, u'time_created': u'2019-03-11T13:52:34.238000+00:00', u'launch_options': {u'remote_data_volume_type': u'PARAVIRTUALIZED', u'firmware': u'UEFI_64', u'boot_volume_type': u'PARAVIRTUALIZED', u'is_consistent_volume_naming_enabled': True, u'network_type': u'VFIO', u'is_pv_encryption_in_transit_enabled': True}, u'image_id': u'ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq', u'source_details': {u'source_type': u'image', u'kms_key_id': None, u'boot_volume_size_in_gbs': None, u'image_id': u'ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq'}, u'fault_domain': u'FAULT-DOMAIN-3', u'shape': u'VM.Standard1.1', u'launch_mode': u'NATIVE', u'agent_config': {u'is_monitoring_disabled': False}, u'extended_metadata': {}, u'ipxe_script': None, u'id': u'ocid1.instance.oc1.eu-frankfurt-1.abtheljslxpqenvnafkstq2s7hxqizyam7shjd6kihyg33i7w2geeynedotq', u'metadata': {u'ssh_authorized_keys': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAA4703470347034703470347034703470347034703470347034703470347034703XR6Zcv4sHKdMH/aDfZKFaLpqHtJ2P+JEC0ok2lWrPQSG0zPoSYWTrldllpgSfzOv1kQ1R/Uor+eedRz9x2DXtKl0anSXY6KNW1GVfOvoFk/c8AuaQy6Y7AZnJAculyF1pRJiHQyZEPuBL9E9EsiRqkQR18G9yewBApRZf/QGXZbLydG8vAa9T1DCYrODb3N2u73IIqbMFwr7sgQ8XQb7h9wlYq2mfUKxy+8H4w7S67'}}], u'changed': True, 'failed': False, u'instance': {u'lifecycle_state': u'RUNNING', u'fault_domain': u'FAULT-DOMAIN-3', u'extended_metadata': {}, u'time_maintenance_reboot_due': None, u'compartment_id': u'ocid1.compartment.oc1..aaaaaaaayc5kgqshdb5g2mjg4bnt34htnybbho3hx2exkz5pzi6kt4kunhiq', u'defined_tags': {}, u'region': u'eu-frankfurt-1', u'freeform_tags': {}, u'time_created': u'2019-03-11T13:52:34.238000+00:00', u'source_details': {u'source_type': u'image', u'image_id': u'ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq', u'kms_key_id': None, u'boot_volume_size_in_gbs': None}, u'agent_config': {u'is_monitoring_disabled': False}, u'image_id': u'ocid1.image.oc1.eu-frankfurt-1.aaaaaaaan7ghs3nhu6bbujqnyukj755642xnmzshck5pm5svol6uigkxl2hq', u'shape': u'VM.Standard1.1', u'availability_domain': u'EUZg:EU-FRANKFURT-1-AD-1', u'launch_mode': u'NATIVE', u'ipxe_script': None, u'display_name': u'my_test_instance', u'launch_options': {u'remote_data_volume_type': u'PARAVIRTUALIZED', u'firmware': u'UEFI_64', u'boot_volume_type': u'PARAVIRTUALIZED', u'is_consistent_volume_naming_enabled': True, u'network_type': u'VFIO', u'is_pv_encryption_in_transit_enabled': True}, u'id': u'ocid1.instance.oc1.eu-frankfurt-1.abtheljslxpqenvnafkstq2s7hxqizyam7shjd6kihyg33i7w2geeynedotq', u'metadata': {u'ssh_authorized_keys': u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAA4703470347034703470347034703470347034703470347034703470347034703XR6Zcv4sHKdMH/aDfZKFaLpqHtJ2P+JEC0ok2lWrPQSG0zPoSYWTrldllpgSfzOv1kQ1R/Uor+eedRz9x2DXtKl0anSXY6KNW1GVfOvoFk/c8AuaQy6Y7AZnJAculyF1pRJiHQyZEPuBL9E9EsiRqkQR18G9yewBApRZf/QGXZbLydG8vAa9T1DCYrODb3N2u73IIqbMFwr7sgQ8XQb7h9wlYq2mfUKxy+8H4w7S67'}}}"
}

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Get the VNIC attachment details of instance] *****************************************************************************************************************************
ok: [localhost]

TASK [Get details of the VNIC] *************************************************************************************************************************************************
ok: [localhost]

TASK [set_fact] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [Print the public ip of the newly launched instance] **********************************************************************************************************************
ok: [localhost] => {
    "msg": "Public IP of launched instance 130.61.40.231"
}

TASK [Wait (upto 5 minutes) for port 22 to become open] ************************************************************************************************************************
ok: [localhost]

TASK [Attempt a ssh connection to the newly launced instance] ******************************************************************************************************************
changed: [localhost]

TASK [Print SSH response from launched instance] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "SSH response from instance -> [u'Linux mytestinstance 3.10.0-957.5.1.el7.x86_64 #1 SMP Fri Feb 1 14:54:57 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux']"
}

TASK [Terminate the instance] **************************************************************************************************************************************************
changed: [localhost]

TASK [Delete the subnet] *******************************************************************************************************************************************************
changed: [localhost]

TASK [Delete the security list] ************************************************************************************************************************************************
changed: [localhost]

TASK [Delete the route table] **************************************************************************************************************************************************
changed: [localhost]

TASK [Delete the Internet Gateway] *********************************************************************************************************************************************
changed: [localhost]

TASK [Delete the VCN] **********************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP *********************************************************************************************************************************************************************
localhost                  : ok=38   changed=16   unreachable=0    failed=0


Ready to Use

After a few minutes, a complete infrastructure for an OCI compute instance is created and the instance is ready to connect. 

The required SSH keys for the terminal connection were generated in a subdirectory of /tmp with the prefix ansible. In my example, the private and the public SSH key are located in /tmp/ansible.v6ckX0cert.

$ pwd
/tmp/ansible.v6ckX0cert

$ ll
total 8
-rw-------. 1 oci oci 1708 Mar 11 14:03 private_key.pem
-rw-rw-r--. 1 oci oci  380 Mar 11 14:03 public_key.pem

Links

Summary

The Oracle provided Ansible playbooks are a good entry point to start with OCI automation. I am already working at the next tasks to make my work easier with more variables and simplified playbooks. And finally I want to integrate it in Ansible AWX. Well done Oracle!