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)
