Building resilient architecture is much easier in the cloud than in a private data center, and the Oracle Cloud Infrastructure team is constantly working to make it even easier.

In the last few weeks, our Python SDK added support for many features, including these key capabilities:

Instance Principals for IAM
Instance Principals provide automatic authentication capability for API calls made from an instance without having to place an API key on the machine. The authentication certificates are automatically generated, provided through metadata server and rotated on a regular basis.

Reserved Public IPs
Reserved Public IP addresses can be assigned to any compute instance private IP address, can float between instances, and are reserved for their tenancy until explicitly deleted.

We can use these features together to simplify failover between compute instances.

You can read more about the differences between ephemeral and reserved IP addresses in the Reserved Public IPs article, but the important factor to note is that although ephemeral addresses are allocated within an availability domain, reserved ones can be moved within a region. This allows for easy regional, multiple-availability-domain redundancy without the need to change route tables or manage DNS records – just reassign a reserved IP address from an instance in one availability domain to an instance in another.

Now I’ll show you how you can failover IP addresses and use Instance Principals in your Python code.

First create resources by using the CLI, if you’re unfamiliar with oci-cli utility, review the documentation: https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/cliconcepts.htm

oci network  public-ip create  --compartment-id ocid1.compartment.oc1..aaaaaaaa --lifetime RESERVED

Now create a dynamic group and policy for virtual machines:

oci iam dynamic-group create --compartment-id ocid1.tenancy.oc1..aaaaaaaa --description FailoverGroup --matching-rule "instance.compartment.id = ocid1.compartment.oc1..aaaaaaa" --name FailoverGroup
oci iam policy create --compartment-id ocid1.tenancy.oc1..aaaaaaaas --name FailoverPolicy --description "Failover management policy" --statements '["allow dynamic-group FailoverGroup to manage public-ips in compartment id ocid1.compartment.oc1..aaaaaaaae", "allow dynamic-group FailoverGroup to manage route-tables in compartment id ocid1.compartment.oc1..aaaaaaaa ", "allow dynamic-group FailoverGroup to manage private-ips in compartment id ocid1.compartment.oc1..aaaaaaaa"]'

Then install the Python SDK on the machine that will perform the failover. Some good examples are to use an external monitoring instance, running Monit or utilize corosync & pacemaker installed directly on

sudo yum install -y python2-pip
sudo pip install oci

Using Instance Principals in Python Code

To use the Instance Principals functionality, instead of providing configuration to Signer, you can leverage oci.auth.signers.InstancePrincipalsSecurityTokenSigner().

Defining signer and then passing it to the API client are the only required things in the program:

signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
network = oci.core.virtual_network_client.VirtualNetworkClient(config={}, signer=signer)

It’s that simple!

Failover

Examine the following example script (failover.py), and use it to fail over a public IP address between instances. Note the public IP OCID that you want to reassign and the private IP OCID that you want to associate with the public IP.

Run the following command:

python failover.py -u <public_ip_ocid> -p <private_ip_ocid>

Now you can leverage this script or a similar one with your Keepalived or Corosync/Pacemaker configuration. For a great example, see Automatic Virtual IP Failover on Oracle Cloud Infrastructure Looks Hard, But it isn’t by Gilson Melo. 

Failover.py

#!/usr/bin/python

'''
    Author: Marcin Zablocki 
  
    Description: Example script to move public IP to Private IP
             for failover purpose using Instance Principal.
'''
import argparse
import sys
import oci


# configuration parameters

SIGNER = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
NETWORK = oci.core.virtual_network_client.VirtualNetworkClient(config={}, signer=SIGNER)
COMPUTE = oci.core.compute_client.ComputeClient(config={}, signer=SIGNER)

PARSER = argparse.ArgumentParser()
PARSER.add_argument("-u", "--public", help="public IP OCID", required=True)
PARSER.add_argument("-p", "--private",
                    help="private IP OCID. If not specified public IP will be detached")
PARSER.add_argument("-r", "--rt_id", help="route table OCID")

ARGS = PARSER.parse_args()

PRIVATE = ARGS.private
PUBLIC = ARGS.public
ROUTE_TABLE_ID = ARGS.rt_id

if not PRIVATE:
    PRIVATE = str("")

def update_default_route(route_table_id, private):
    """
This routine will update specified route table and replace the rules with
0.0.0.0/0 via 
  
   

Route rules are list of RouteRule objects.
Updating Route Table uses RouteTableDetails object containing
RouteRules and Description.
"""

    route_table_details = oci.core.models.UpdateRouteTableDetails()
    route_rules = []
    route_rules.append(
	       oci.core.models.RouteRule(
                   cidr_block='0.0.0.0/0',
                   network_entity_id=private)
    )

    route_table_details.route_rules = route_rules

    ip_details = oci.core.models.UpdatePublicIpDetails()
    ip_details.private_ip_id = private

    try:
        request = NETWORK.update_route_table(route_table_id, route_table_details).data
        print "Route table "+str(request.display_name)+" updated"

    except Exception as exception_message:

        print("Failed to update the route rule. Exception: ")
        print(exception_message)
        sys.exit(1)

def activate(private, public):
    ''' This function maps public IP to private IP or detaches Public IP if
	private IP is not specified. '''
    if private:

        if not ROUTE_TABLE_ID:
            print "route table OCID not found. Updating only IP association"
        else:
            update_default_route(ROUTE_TABLE_ID, private)

        ip_details = oci.core.models.UpdatePublicIpDetails()
        ip_details.private_ip_id = private

    public_ip_details = NETWORK.get_public_ip(public).data

    if public_ip_details.private_ip_id == private:
        private_ip_details = NETWORK.get_private_ip(private).data
        print("IP "+str(public_ip_details.ip_address)+
		            " already assigned to private IP "+
		            str(private_ip_details.ip_address)
             )

        sys.exit(1)

    try:
        NETWORK.update_public_ip(public, ip_details)

        if not private:
            print("IP unassigned")

        else:
            print("IP assigned")

    except Exception as exception_message:
        print("Failed to updated Public IP. Exception:")
        print(exception_message)

if __name__ == "__main__":
    activate(PRIVATE, PUBLIC)