Scripting OCI Objects creation using CLI

Plamen MushkovPlamen Mushkov
10 min read

The problem

Already having some experience with AI assisted coding using Cursor, I tried to put it on another test today. In one of my personal projects, I needed secure access to Oracle Cloud's Object Storage buckets for uploading and downloading files. I've done it already in the past and I knew the steps. You need a new compartment, a new bucket, optionally a new user and security credentials. After that, you need to create a new credential in your APEX application and use the credential static ID in your REST API calls to the OCI object storage. Jon Dixon has all the steps very nicely explained in his blog post from 2022.The guidelines are very clear and easy to execute.

πŸ‘€
The only extra thing that I needed was a script that I could directly run in the OCI console and have everything automated in just one click.

The solution

βœ…
What I did is to use Jon’s blog post in the chat context and ask a large language model (LLM) to help me generate a shell script and guide me on how to execute it. What's more, it already had my project repository loaded into the context too. So the LLM response figured out all the details like naming, paths and so on. I literally just had to go and run in using the OCI CLI.

When I first tried running the shell script, I got the following error (expected, as I didn’t have the OCI CLI installed on my machine).

πŸ’‘
What’s cool about Cursor is that it detects the error in the built-in Terminal and suggests putting the error message in the chat, so it can be answered by the LLM. I already knew what I was missing, so did it by myself. The following steps are enough to fulfil the prerequisites.

Prerequisites

# 1.Install OCI CLI 
# Install using Homebrew (recommended for Mac)
brew update && brew install oci-cli

# Verify installation
oci --version
# 2.Install jq (JSON processor)
# Install using Homebrew
brew install jq

# Verify installation
jq --version
# 3. Configure OCI CLI with your credentials
# Run the interactive setup
oci setup config

oci setup config will configure the access to your OCI resources from the CLI.

When you run it, it will prompt you for:

User OCID - Your OCI user ID (find in OCI Console β†’ Profile β†’ User Settings)
Tenancy OCID - Your tenancy ID (find in OCI Console β†’ Profile β†’ Tenancy)
Region - Your home region (e.g., us-ashburn-1)
Key file location - It will generate API keys for you

The setup will:

Generate an API key pair
Create a config file at ~/.oci/config
Show you the public key to upload to OCI

Upload the public key to OCI Console

  1. Copy the public key shown by the setup command

  2. Go to OCI Console β†’ Profile β†’ User Settings β†’ Tokens and Keys β†’ API Keys

  3. Click "Add API Key" β†’ "Paste Public Key"

  4. Paste the key and click "Add"

Test your OCI CLI setup

# Test basic connectivity
oci iam region list

# Test authentication
oci iam user get --user-id <your-user-ocid>
⚠
If you have multiple records in your .oci/config file, make sure you are using the right profile. In my case I had two etries - [DEFAULT] and [APEX_APPS_OCI] that I just created. To use the newly created you need to add β€œ--profile APEX_APPS_OCIβ€œ at the end of the line, when using cli
# Test basic connectivity using a specific profile, located in .oci/profile
oci iam region list --profile APEX_APPS_OCI

Running the script

If both tests are successful, you are ready to run the setup_oci_object_storage.sh script.
Open your Terminal, navigate to the folder with the script and execute it using this command:

./setup_oci_object_storage.sh

If everything is fine, you will find all the object created in your OCI console.

The Code

Below is the code of the setup_oci_object_storage.sh script. All you need is replace the configuration values in the beginning as you like.

#!/bin/bash

# OCI Object Storage Setup Script for Oracle APEX and PL/SQL integration
# 
# Prerequisites:
# - OCI CLI installed and configured
# - User has administrative privileges in OCI tenancy
# - jq installed for JSON parsing

set -e  # Exit on any error

# Configuration Variables - CUSTOMIZE THESE VALUES
COMPARTMENT_NAME="APEX_APPS"
COMPARTMENT_DESCRIPTION="APEX applications development related compartment"
BUCKET_NAME="photo-bucket"
SERVICE_ACCOUNT_NAME="apex-apps-service"
SERVICE_ACCOUNT_DESCRIPTION="Service account for APEX applications"
SECURITY_GROUP_NAME="APEX_APPS_Security_Group"
SECURITY_GROUP_DESCRIPTION="Security group for APEX applications"
POLICY_NAME="APEX_APPS_Policy"
POLICY_DESCRIPTION="Policy for APEX apps Object Storage access"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Logging function
log() {
    echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}

error() {
    echo -e "${RED}[ERROR] $1${NC}" >&2
}

warning() {
    echo -e "${YELLOW}[WARNING] $1${NC}"
}

info() {
    echo -e "${BLUE}[INFO] $1${NC}"
}

# Check prerequisites
check_prerequisites() {
    log "Checking prerequisites..."

    if ! command -v oci &> /dev/null; then
        error "OCI CLI is not installed. Please install it first."
        exit 1
    fi

    if ! command -v jq &> /dev/null; then
        error "jq is not installed. Please install it first."
        exit 1
    fi

    # Test OCI CLI configuration
    if ! oci iam region list &> /dev/null; then
        error "OCI CLI is not properly configured. Please run 'oci setup config' first."
        exit 1
    fi

    log "Prerequisites check passed βœ“"
}

# Get tenancy OCID
get_tenancy_ocid() {
    log "Getting tenancy OCID..."
    TENANCY_OCID=$(oci iam tenancy get \
                    --profile DEFAULT \
                    --tenancy-id $(grep -A 5 "\[DEFAULT\]" ~/.oci/config | grep tenancy | awk -F "=" '{print $2}' | xargs) \
                    --query 'data.id' --raw-output)
    if [ "$TENANCY_OCID" == "null" ] || [ -z "$TENANCY_OCID" ]; then
        error "Failed to get tenancy OCID"
        exit 1
    fi
    info "Tenancy OCID: $TENANCY_OCID"
}

# Create compartment
create_compartment() {
    log "Creating compartment: $COMPARTMENT_NAME"

    # Check if compartment already exists
    EXISTING_COMPARTMENT=$(oci iam compartment list --compartment-id "$TENANCY_OCID" --query "data[?name=='$COMPARTMENT_NAME'].id | [0]" --raw-output)

    if [ "$EXISTING_COMPARTMENT" != "null" ] && [ -n "$EXISTING_COMPARTMENT" ]; then
        warning "Compartment '$COMPARTMENT_NAME' already exists"
        COMPARTMENT_OCID="$EXISTING_COMPARTMENT"
    else
        COMPARTMENT_RESPONSE=$(oci iam compartment create \
            --compartment-id "$TENANCY_OCID" \
            --name "$COMPARTMENT_NAME" \
            --description "$COMPARTMENT_DESCRIPTION")

        COMPARTMENT_OCID=$(echo "$COMPARTMENT_RESPONSE" | jq -r '.data.id')

        if [ "$COMPARTMENT_OCID" == "null" ] || [ -z "$COMPARTMENT_OCID" ]; then
            error "Failed to create compartment"
            exit 1
        fi

        log "Compartment created successfully βœ“"
    fi

    info "Compartment OCID: $COMPARTMENT_OCID"

    # Wait for compartment to be active
    log "Waiting for compartment to be active..."
    while true; do
        STATE=$(oci iam compartment get --compartment-id "$COMPARTMENT_OCID" | jq -r '.data."lifecycle-state"')
        if [ "$STATE" == "ACTIVE" ]; then
            break
        fi
        sleep 5
    done
    log "Compartment is active βœ“"
}

# Create Object Storage bucket
create_bucket() {
    log "Creating Object Storage bucket: $BUCKET_NAME"

    # Get namespace
    NAMESPACE=$(oci os ns get | jq -r '.data')
    info "Object Storage namespace: $NAMESPACE"

    # Check if bucket already exists
    if oci os bucket get --bucket-name "$BUCKET_NAME" --namespace "$NAMESPACE" &> /dev/null; then
        warning "Bucket '$BUCKET_NAME' already exists"
    else
        oci os bucket create \
            --compartment-id "$COMPARTMENT_OCID" \
            --name "$BUCKET_NAME" \
            --namespace "$NAMESPACE"

        log "Bucket created successfully βœ“"
    fi
}

# Create service account (IAM user)
create_service_account() {
    log "Creating service account: $SERVICE_ACCOUNT_NAME"

    # Check if user already exists
    EXISTING_USER=$(oci iam user list --compartment-id "$TENANCY_OCID" --query "data[?name=='$SERVICE_ACCOUNT_NAME'].id | [0]" --raw-output)

    if [ "$EXISTING_USER" != "null" ] && [ -n "$EXISTING_USER" ]; then
        warning "Service account '$SERVICE_ACCOUNT_NAME' already exists"
        SERVICE_USER_OCID="$EXISTING_USER"
    else
        USER_RESPONSE=$(oci iam user create \
            --compartment-id "$TENANCY_OCID" \
            --name "$SERVICE_ACCOUNT_NAME" \
            --description "$SERVICE_ACCOUNT_DESCRIPTION")

        SERVICE_USER_OCID=$(echo "$USER_RESPONSE" | jq -r '.data.id')

        if [ "$SERVICE_USER_OCID" == "null" ] || [ -z "$SERVICE_USER_OCID" ]; then
            error "Failed to create service account"
            exit 1
        fi

        log "Service account created successfully βœ“"
    fi

    info "Service account OCID: $SERVICE_USER_OCID"
}

# Generate API key pair
generate_api_keys() {
    log "Generating API key pair for service account..."

    # Create directory for keys if it doesn't exist
    mkdir -p ./oci-keys

    PRIVATE_KEY_FILE="./oci-keys/${SERVICE_ACCOUNT_NAME}_private_key.pem"
    PUBLIC_KEY_FILE="./oci-keys/${SERVICE_ACCOUNT_NAME}_public_key.pem"

    # Generate private key
    openssl genrsa -out "$PRIVATE_KEY_FILE" 2048

    # Generate public key
    openssl rsa -pubout -in "$PRIVATE_KEY_FILE" -out "$PUBLIC_KEY_FILE"

    # Set proper permissions
    chmod 600 "$PRIVATE_KEY_FILE"
    chmod 644 "$PUBLIC_KEY_FILE"

    log "API key pair generated βœ“"
    info "Private key: $PRIVATE_KEY_FILE"
    info "Public key: $PUBLIC_KEY_FILE"

    # Upload public key to OCI
    log "Uploading public key to OCI..."

    API_KEY_RESPONSE=$(oci iam user api-key upload \
        --user-id "$SERVICE_USER_OCID" \
        --key-file "$PUBLIC_KEY_FILE")

    API_KEY_FINGERPRINT=$(echo "$API_KEY_RESPONSE" | jq -r '.data.fingerprint')

    if [ "$API_KEY_FINGERPRINT" == "null" ] || [ -z "$API_KEY_FINGERPRINT" ]; then
        error "Failed to upload API key"
        exit 1
    fi

    log "API key uploaded successfully βœ“"
    info "API key fingerprint: $API_KEY_FINGERPRINT"
}

# Create security group
create_security_group() {
    log "Creating security group: $SECURITY_GROUP_NAME"

    # Check if group already exists
    EXISTING_GROUP=$(oci iam group list --compartment-id "$TENANCY_OCID" --query "data[?name=='$SECURITY_GROUP_NAME'].id | [0]" --raw-output)

    if [ "$EXISTING_GROUP" != "null" ] && [ -n "$EXISTING_GROUP" ]; then
        warning "Security group '$SECURITY_GROUP_NAME' already exists"
        GROUP_OCID="$EXISTING_GROUP"
    else
        GROUP_RESPONSE=$(oci iam group create \
            --compartment-id "$TENANCY_OCID" \
            --name "$SECURITY_GROUP_NAME" \
            --description "$SECURITY_GROUP_DESCRIPTION")

        GROUP_OCID=$(echo "$GROUP_RESPONSE" | jq -r '.data.id')

        if [ "$GROUP_OCID" == "null" ] || [ -z "$GROUP_OCID" ]; then
            error "Failed to create security group"
            exit 1
        fi

        log "Security group created successfully βœ“"
    fi

    info "Security group OCID: $GROUP_OCID"
}

# Add user to security group
add_user_to_group() {
    log "Adding service account to security group..."

    # Check if user is already in group
    MEMBERSHIP_CHECK=$(oci iam group list-users --group-id "$GROUP_OCID" --query "data[?id=='$SERVICE_USER_OCID'].id | [0]" --raw-output)

    if [ "$MEMBERSHIP_CHECK" != "null" ] && [ -n "$MEMBERSHIP_CHECK" ]; then
        warning "Service account is already a member of the security group"
    else
        oci iam group add-user \
            --group-id "$GROUP_OCID" \
            --user-id "$SERVICE_USER_OCID"

        log "Service account added to security group βœ“"
    fi
}

# Create security policy
create_security_policy() {
    log "Creating security policy: $POLICY_NAME"

    # Define policy statements
    POLICY_STATEMENTS=$(cat <<EOF
[
  "Allow group $SECURITY_GROUP_NAME to read buckets in compartment $COMPARTMENT_NAME",
  "Allow group $SECURITY_GROUP_NAME to manage objects in compartment $COMPARTMENT_NAME where all {target.bucket.name='$BUCKET_NAME', any {request.permission='OBJECT_CREATE', request.permission='OBJECT_INSPECT', request.permission='OBJECT_READ', request.permission='OBJECT_OVERWRITE', request.permission='OBJECT_DELETE'}}"
]
EOF
)

    # Check if policy already exists
    EXISTING_POLICY=$(oci iam policy list --compartment-id "$COMPARTMENT_OCID" --query "data[?name=='$POLICY_NAME'].id | [0]" --raw-output)

    if [ "$EXISTING_POLICY" != "null" ] && [ -n "$EXISTING_POLICY" ]; then
        warning "Policy '$POLICY_NAME' already exists"
        POLICY_OCID="$EXISTING_POLICY"
    else
        POLICY_RESPONSE=$(oci iam policy create \
            --compartment-id "$COMPARTMENT_OCID" \
            --name "$POLICY_NAME" \
            --description "$POLICY_DESCRIPTION" \
            --statements "$POLICY_STATEMENTS")

        POLICY_OCID=$(echo "$POLICY_RESPONSE" | jq -r '.data.id')

        if [ "$POLICY_OCID" == "null" ] || [ -z "$POLICY_OCID" ]; then
            error "Failed to create security policy"
            exit 1
        fi

        log "Security policy created successfully βœ“"
    fi

    info "Policy OCID: $POLICY_OCID"
}

# Generate configuration summary
generate_summary() {
    log "Generating configuration summary..."

    # Get region and namespace
    REGION=$(oci iam region-subscription list | jq -r '.data[] | select(.["is-home-region"] == true) | .["region-name"]')
    NAMESPACE=$(oci os ns get | jq -r '.data')

    # Read private key content (remove header/footer and newlines)
    PRIVATE_KEY_CONTENT=$(grep -v "BEGIN\|END" "$PRIVATE_KEY_FILE" | tr -d '\n')

    SUMMARY_FILE="./oci-setup-summary.txt"

    cat > "$SUMMARY_FILE" << EOF
=============================================================================
OCI Object Storage Setup Summary
=============================================================================
Generated on: $(date)

CONFIGURATION VALUES:
=============================================================================
Tenancy OCID:           $TENANCY_OCID
Compartment Name:       $COMPARTMENT_NAME
Compartment OCID:       $COMPARTMENT_OCID
Bucket Name:            $BUCKET_NAME
Service Account Name:   $SERVICE_ACCOUNT_NAME
Service Account OCID:   $SERVICE_USER_OCID
Security Group Name:    $SECURITY_GROUP_NAME
Security Group OCID:    $GROUP_OCID
Policy Name:            $POLICY_NAME
Policy OCID:            $POLICY_OCID
Region:                 $REGION
Namespace:              $NAMESPACE
API Key Fingerprint:    $API_KEY_FINGERPRINT

APEX WEB CREDENTIAL CONFIGURATION:
=============================================================================
Name:                   APEX_OCI_PHOTO_BUCKET_CREDENTIAL
Static Identifier:      APEX_OCI_PHOTO_BUCKET_CREDENTIAL
Authentication Type:    Oracle Cloud Infrastructure (OCI)
OCI User ID:            $SERVICE_USER_OCID
OCI Private Key:        $PRIVATE_KEY_CONTENT
OCI Tenancy ID:         $TENANCY_OCID
OCI Public Key Fingerprint: $API_KEY_FINGERPRINT

OBJECT STORAGE URL PATTERN:
=============================================================================
Base URL: https://objectstorage.$REGION.oraclecloud.com/n/$NAMESPACE/b/$BUCKET_NAME/o/

Example URLs:
- Input image:  https://objectstorage.$REGION.oraclecloud.com/n/$NAMESPACE/b/$BUCKET_NAME/o/photo-bucket/username/input/filename.png
- Output image: https://objectstorage.$REGION.oraclecloud.com/n/$NAMESPACE/b/$BUCKET_NAME/o/photo-bucket/username/output/filename.png

DBMS_CLOUD CREDENTIAL SQL:
=============================================================================
BEGIN
    DBMS_CLOUD.create_credential(
        credential_name => 'APEX_APPS_OCI_CREDENTIAL',
        user_ocid       => '$SERVICE_USER_OCID',
        tenancy_ocid    => '$TENANCY_OCID',
        private_key     => '$PRIVATE_KEY_CONTENT',
        fingerprint     => '$API_KEY_FINGERPRINT'
    );
END;
/

UPDATE PACKAGE CONFIGURATION:
=============================================================================
Update the following in your PL/SQL package:
1. Replace 'YOUR_NAMESPACE' with: $NAMESPACE
2. Replace 'us-ashburn-1' with: $REGION (if different)
3. Use credential name: APEX_APPS_OCI_CREDENTIAL

FILES GENERATED:
=============================================================================
Private Key: $PRIVATE_KEY_FILE
Public Key:  $PUBLIC_KEY_FILE
Summary:     $SUMMARY_FILE

NEXT STEPS:
=============================================================================
1. Create the APEX Web Credential using the values above
2. Create the DBMS_CLOUD credential using the SQL above
3. Update your PL/SQL package with the correct namespace and region
4. Test the connection by uploading a sample file
5. Secure the private key file and remove it from this location

=============================================================================
EOF

    log "Configuration summary saved to: $SUMMARY_FILE"
}

# Main execution
main() {
    log "Starting OCI Object Storage setup for your APEX App..."
    log "=========================================================="

    check_prerequisites
    get_tenancy_ocid
    create_compartment
    create_bucket
    create_service_account
    generate_api_keys
    create_security_group
    add_user_to_group
    create_security_policy
    generate_summary

    log "=========================================================="
    log "OCI Object Storage setup completed successfully! βœ“"
    log "Please review the configuration summary: oci-setup-summary.txt"
    warning "Remember to secure your private key file!"
}

# Run main function
main "$@"

Takeaways

πŸ”Έ OCI allows you to automate and script all console operations, using the CLI

πŸ”Έ The script I have used allows you to create a Bucket, Service Account (User), Security Group, Policy and API Key Pair

πŸ”Έ The script is made in a way that allows rerunning it multiple times, as it checks if needed objects are already created

πŸ”Έ It outputs a summary file, called oci-setup-summary.txt with everything that has been created, together with URL to be used when working with the Object Storage, examples and instructions on accessing the buckets using PL/SQL

0
Subscribe to my newsletter

Read articles from Plamen Mushkov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Plamen Mushkov
Plamen Mushkov

Creating apps built with ❀️ using Oracle APEX. PL/SQL dev. Exploring the AI and ML world. Keen on front-end, visual arts and new technologies. Love travel, sports, photography.