OCI Ansible Automation: Dynamic Inventory, OCI Modules, and Vault Secret Integration

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.