Terraform manages the lifecycle of cloud resources. Ansible manages what happens inside those resources after they exist. The Oracle Cloud Ansible Collections provide modules for every OCI service, a dynamic inventory plugin that builds your host list directly from OCI, and connection support through OCI Bastion for private instances.
Step 1: Install and Configure
pip install oci --break-system-packages
ansible-galaxy collection install oracle.oci
ansible-galaxy collection list | grep oracle.oci
Step 2: Dynamic Inventory from OCI Tags
# inventory/oci_inventory.yml
plugin: oracle.oci.oci
group_by_tag_namespace: "Operations"
compartments:
- ocid: ocid1.compartment.oc1..yourcompartmentocid
fetch_hosts_from_subcompartments: true
groups:
web_servers: "'Operations.Team' in host_vars and host_vars['Operations.Team'] == 'orders'"
db_servers: "'Operations.Application' in host_vars and host_vars['Operations.Application'] == 'database'"
production_hosts: "'Operations.Environment' in host_vars and host_vars['Operations.Environment'] == 'production'"
hostname_format: "private_ip"
# Test inventory
ansible-inventory -i inventory/oci_inventory.yml --list | jq 'keys'
ansible-inventory -i inventory/oci_inventory.yml --graph production_hosts
Step 3: Provision Compute Instances
---
- name: Provision OCI compute instance
hosts: localhost
collections:
- oracle.oci
tasks:
- name: Create compute instance
oci_compute_instance:
compartment_id: "{{ compartment_id }}"
display_name: "app-server-01"
availability_domain: "AbCd:ME-JEDDAH-1-AD-1"
shape: "VM.Standard.E4.Flex"
shape_config:
ocpus: 4
memory_in_gbs: 32
create_vnic_details:
subnet_id: "{{ subnet_id }}"
assign_public_ip: false
source_details:
source_type: image
image_id: "{{ oracle_linux_image_id }}"
defined_tags:
Operations:
Environment: production
Team: orders
register: instance_result
- name: Wait for RUNNING state
oci_compute_instance_facts:
id: "{{ instance_result.instance.id }}"
register: instance_facts
until: instance_facts.instances[0].lifecycle_state == 'RUNNING'
retries: 30
delay: 15
- name: Show instance details
debug:
msg: "Instance ready at {{ instance_facts.instances[0].private_ip }}"
Step 4: Retrieve OCI Vault Secrets in Playbooks
---
- name: Configure application with OCI Vault secrets
hosts: web_servers
collections:
- oracle.oci
tasks:
- name: Get DB password from OCI Vault
oci_vault_secret_bundle_facts:
secret_id: "ocid1.vaultsecret.oc1..yoursecretocid"
register: db_secret
delegate_to: localhost
- name: Decode secret value
set_fact:
db_password: "{{ db_secret.secret_bundle.secret_bundle_content.content | b64decode }}"
no_log: true # Suppresses output for this task - prevents secret appearing in logs
- name: Deploy application config with credentials
template:
src: application.properties.j2
dest: /opt/app/config/application.properties
owner: appuser
group: appuser
mode: '0640'
notify: restart_application
Step 5: Rolling Update with Load Balancer Integration
---
- name: Rolling application update
hosts: web_servers
serial: 1 # One host at a time - zero-downtime rolling update
pre_tasks:
- name: Remove instance from OCI load balancer backend
oci_loadbalancer_backend:
load_balancer_id: "{{ lb_ocid }}"
backend_set_name: "orders-api-backend"
ip_address: "{{ ansible_host }}"
port: 8080
state: absent
delegate_to: localhost
- name: Wait for connections to drain
pause:
seconds: 30
tasks:
- name: Stop application service
ansible.builtin.systemd:
name: orders-api
state: stopped
- name: Deploy new application artifact
ansible.builtin.copy:
src: "{{ playbook_dir }}/dist/orders-api-{{ app_version }}.jar"
dest: /opt/orders-api/orders-api.jar
mode: '0644'
- name: Start updated application
ansible.builtin.systemd:
name: orders-api
state: started
- name: Wait for application health check
ansible.builtin.uri:
url: "http://{{ ansible_host }}:8080/health"
status_code: 200
retries: 12
delay: 5
post_tasks:
- name: Add instance back to load balancer
oci_loadbalancer_backend:
load_balancer_id: "{{ lb_ocid }}"
backend_set_name: "orders-api-backend"
ip_address: "{{ ansible_host }}"
port: 8080
state: present
delegate_to: localhost
Step 6: Object Storage Operations
---
- name: Upload build artifacts to OCI Object Storage
hosts: localhost
collections:
- oracle.oci
tasks:
- name: Upload artifact to Object Storage
oci_object_storage_object:
namespace_name: "{{ tenancy_namespace }}"
bucket_name: "build-artifacts"
object_name: "releases/{{ app_version }}/orders-api-{{ app_version }}.jar"
src: "{{ playbook_dir }}/dist/orders-api-{{ app_version }}.jar"
state: present
- name: Create pre-authenticated request for deployment
oci_object_storage_preauthenticated_request:
namespace_name: "{{ tenancy_namespace }}"
bucket_name: "build-artifacts"
name: "deploy-{{ app_version }}-{{ ansible_date_time.epoch }}"
object_name: "releases/{{ app_version }}/orders-api-{{ app_version }}.jar"
access_type: "ObjectRead"
time_expires: "{{ (ansible_date_time.epoch | int + 3600) | strftime('%Y-%m-%dT%H:%M:%SZ') }}"
state: present
register: par_result
- name: Show download URL
debug:
msg: "Artifact available at {{ par_result.preauthenticated_request.full_path }}"
Operational Notes
The OCI dynamic inventory plugin groups instances by tag values automatically when you set group_by_tag_namespace. Tag an instance with Operations.Team=orders and it appears in the orders Ansible group without any inventory file changes. The inventory stays self-maintaining as your infrastructure scales without modifying any inventory files.
The serial: 1 directive on a play processes one host at a time. Combined with pre-tasks that remove the instance from the load balancer and post-tasks that add it back, this gives you zero-downtime rolling updates across all instances in a host group without any external orchestration tool.
Regards,
Osama
#OCI #OracleCloud #Ansible #Automation #DevOps #TechBlog #Oracle #IaC #CloudAutomation #PlatformEngineering #OracleCloudInfrastructure #ConfigurationManagement #AnsibleCollections #DynamicInventory #OracleDatabase #CloudNative #RollingUpdate #CICD #VaultIntegration #ObjectStorage
Leave a comment