diff --git a/.gitignore b/.gitignore index 4e16e08..0c13641 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,27 @@ Temporary Items .key .crt .csr -.pem \ No newline at end of file +.pem + +# Go build +**/bin/ +**/vendor/ + +# Editor +*.swp +*.swo +.idea/ +.vscode/ +.DS_Store + +# Debug +*.log + +# Local Terraform variable overrides +**/.terraform.lock.hcl +**/terraform_local.tfvars +**/.terraform* + +*.tfstate +*.tfstate.backup +**/__pycache__/ \ No newline at end of file diff --git a/api-gateway-function-queue-async/README.md b/api-gateway-function-queue-async/README.md new file mode 100644 index 0000000..b285619 --- /dev/null +++ b/api-gateway-function-queue-async/README.md @@ -0,0 +1,184 @@ +# API Gateway Function Queue Async + +This project demonstrates an asynchronous messaging pattern using Oracle Cloud Infrastructure (OCI) services: + +- **API Gateway**: Receives HTTP requests and routes them to functions +- **OCI Functions**: Serverless functions for processing requests +- **OCI Queue**: Asynchronous message queue for decoupling services + +## Architecture + +The application follows an asynchronous messaging pattern: + +1. HTTP request arrives at API Gateway → `/order` endpoint +2. API Gateway routes to **place-order OCI Function** +3. Function validates and enqueues order to **OCI Queue** +4. Function returns immediately (async response) +5. **process-order Container Instance** continuously polls the queue +6. Container Instance processes messages and inserts into **NoSQL Database** + +``` +HTTP Request → API Gateway → place-order Function → Queue → process-order Container Instance → NoSQL Table + ↓ + (Async Processing) +``` + +## Components + +### API Gateway +- Receives HTTP requests at `/order` endpoint +- Routes requests to the `place-order` function + +### Functions +- **place-order**: OCI Function that receives order requests via API Gateway and enqueues them to the queue +- **process-order**: Container Instance (not an OCI Function) that continuously polls the OCI Queue and processes orders by inserting them into the NoSQL table + +### Queue +- **OrderQueue**: Asynchronous message queue for order processing +- Provides reliable message delivery and processing + +## Directory Structure + +``` +api-gateway-function-queue-async/ +├── README.md # Project documentation +├── functions/ # Application components +│ ├── place-order/ # OCI Function for receiving orders +│ └── process-order/ # Container Instance for queue polling +└── terraform/ # Infrastructure as Code + └── modules/ # Reusable Terraform modules + ├── apigateway/ # API Gateway module + ├── container_repository/ # OCI Container Registry module + ├── functions/ # OCI Functions module + ├── queue/ # OCI Queue module + └── vcn/ # Virtual Cloud Network module +``` + +## Prerequisites + +Before you begin, ensure you have the following: + +- **OCI Account**: Active Oracle Cloud Infrastructure account with appropriate compartment access +- **Terraform**: Version 1.10.0 or higher installed on your local machine +- **Docker**: Docker Desktop or equivalent for building and pushing container images +- **OCI CLI**: Configured with your OCI credentials for authentication +- **Network**: A public subnet in your VPC for the API Gateway and Container Instance +- **IAM Permissions**: Your OCI user must have permissions to: + - Create API Gateways + - Create and manage OCI Functions + - Create and manage OCI Queues + - Create NoSQL tables + - Create Container Instances + - Create Container Registries + - Manage IAM policies and dynamic groups +- **Authentication**: OCI auth token for Docker Container Registry access + +## Getting Started + +1. **Configure OCI Variables**: + + Update `terraform/terraform.tfvars` with your OCI configuration: + ```hcl + region = "us-ashburn-1" # Your OCI region + compartment_ocid = "ocid1.compartment.oc1..." # Your compartment OCID + subnet_ocid = "ocid1.subnet.oc1..." # Your public subnet OCID + tenancy_ocid = "ocid1.tenancy.oc1..." # Your tenancy OCID + queue_name = "OrderQueue" + post_order_container_repository_name = "queue_async_repo" + process_order_container_repository_name = "queue_async_process_repo" + application_display_name = "queue_async_app" + nosql_table_name = "orders" + ``` + +2. **Deploy Infrastructure**: + ```bash + cd terraform + terraform init + terraform apply -var-file=terraform.tfvars + ``` + + **Note**: After successful deployment, note the OCIR repository addresses from the Terraform outputs: + - `post_order_repository_path`: Container registry path for place-order function + - `process_order_repository_path`: Container registry path for process-order function + + These will be used in the next step for building and pushing Docker images. + +3. **Build and Deploy Place-Order Image**: + ```bash + cd functions/place-order + docker build -t place-order:1 . + docker tag place-order:1 :1 + docker push :1 + ``` + +4. **Build and Deploy Process-Order Image**: + ```bash + cd ../process-order + docker build -t process-order:1 . + docker tag process-order:1 :1 + docker push :1 + ``` + +5. **Update Terraform Variables with Image Tags**: + + Update `terraform/terraform.tfvars` to add the function image URIs you just pushed: + ```hcl + # Add these to your existing terraform.tfvars + functions = { + "place-order" = { + source_image = ":1" + path = "place-order" + config = {} + } + } + queue_poller_image = ":1" + ``` + +6. **Re-apply Terraform Configuration**: + + Apply the updated configuration with the new image tags: + ```bash + cd terraform + terraform apply -var-file=terraform.tfvars + ``` + + This will deploy the OCI Function and Container Instance with the container images you just built and pushed. + +7. **Get API Gateway Endpoint**: + + After the Terraform apply completes, retrieve the API Gateway endpoint URL: + ```bash + terraform output api_gateway_endpoint + ``` + + Save this URL for testing the API in the next step. + +8. **Test the API**: + ```bash + curl -X POST https:///order \ + -H "Content-Type: application/json" \ + -d '{ + "data": { + "order_id": "ORD-001", + "customer_id": "CUST123", + "amount": 99.99 + } + }' + ``` + +9. **Verify Records in NoSQL Table**: + + After submitting a few orders, verify that the records have been processed and inserted into the NoSQL table: + ```bash + # Query the NoSQL table for all orders + oci nosql query execute --statement "SELECT * FROM order_info" --compartment-id --region + ``` + + You should see the orders you submitted through the API in the results. + +## Benefits + +- **Scalability**: Queue decouples request handling from processing +- **Reliability**: Messages are persisted and can be retried +- **Performance**: API responds immediately while processing happens asynchronously +- **Monitoring**: Queue provides visibility into message processing \ No newline at end of file diff --git a/api-gateway-function-queue-async/functions/place-order/Dockerfile b/api-gateway-function-queue-async/functions/place-order/Dockerfile new file mode 100644 index 0000000..0c776c0 --- /dev/null +++ b/api-gateway-function-queue-async/functions/place-order/Dockerfile @@ -0,0 +1,16 @@ +FROM fnproject/python:3.11-dev as build-stage +WORKDIR /function +ADD requirements.txt /function/ + +RUN pip3 install --target /python/ --no-cache --no-cache-dir -r requirements.txt &&\ + rm -fr ~/.cache/pip /tmp* requirements.txt func.yaml Dockerfile .venv &&\ + chmod -R o+r /python +ADD . /function/ +RUN rm -fr /function/.pip_cache +FROM fnproject/python:3.11 +WORKDIR /function +COPY --from=build-stage /python /python +COPY --from=build-stage /function /function +RUN chmod -R o+r /function +ENV PYTHONPATH=/function:/python +ENTRYPOINT ["/python/bin/fdk", "/function/func.py", "handler"] \ No newline at end of file diff --git a/api-gateway-function-queue-async/functions/place-order/func.py b/api-gateway-function-queue-async/functions/place-order/func.py new file mode 100644 index 0000000..05d7e2d --- /dev/null +++ b/api-gateway-function-queue-async/functions/place-order/func.py @@ -0,0 +1,103 @@ +import io +import json +import oci +import logging +import base64 +from io import BytesIO + +from fdk import response + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def handler(ctx, data: str = None): + """ + OCI Function handler to post a message to an OCI Queue using Resource Principal. + + Args: + ctx: Function context + data: Input data (JSON string) + + Returns: + JSON response indicating success or failure + """ + try: + # Initialize Resource Principal signer + signer = oci.auth.signers.get_resource_principals_signer() + + # Get region from function configuration or default to us-ashburn-1 + region = ctx.Config().get("OCI_REGION", "us-ashburn-1") + + # Set endpoint explicitly + endpoint = f"https://cell-1.queue.messaging.{region}.oci.oraclecloud.com" + + # Initialize Queue client with the correct endpoint + config = { + "region": ctx.Config().get("QUEUE_OCID") + } + + # Initialize Queue client + queue_client = oci.queue.QueueClient(config=config, signer=signer, service_endpoint= endpoint) + + # Get queue OCID from function configuration + queue_ocid = ctx.Config().get("QUEUE_OCID") + if not queue_ocid: + raise ValueError("QUEUE_OCID not found in function configuration") + + # Handle input data (string or BytesIO) + if data is None: + raise ValueError("No payload provided in POST body") + + # If data is BytesIO (e.g., from fn invoke), read and decode it + if isinstance(data, BytesIO): + data = data.read().decode('utf-8') + + # Parse the input as JSON + payload = json.loads(data) + + # Validate payload structure + if not isinstance(payload, dict) or "data" not in payload: + raise ValueError("Invalid payload format: 'data' key is missing") + if not all(key in payload["data"] for key in ["order_id", "customer_id", "amount"]): + raise ValueError("Invalid payload: 'order_id', 'customer_id', and 'amount' are required in 'data'") + + # Convert payload to JSON string and encode as base64 + message_content = json.dumps(payload) + + # Prepare message + message = { + "content": message_content, + "contentType": "application/json" + } + + put_messages_details = oci.queue.models.PutMessagesDetails( + messages=[oci.queue.models.PutMessagesDetailsEntry(content=message["content"])] + ) + + response = queue_client.put_messages( + queue_id=queue_ocid, + put_messages_details=put_messages_details + ) + + # Check response + if response.status == 200: + logger.info(f"Successfully posted message to queue {queue_ocid}") + return { + "status": "success", + "messageId": response.data.messages[0].id + } + else: + logger.error(f"Failed to post message: {response.status}") + return { + "status": "error", + "message": f"Failed to post message: {response.status}" + } + + except Exception as e: + logger.error(f"Error in handler: {str(e)}") + return { + "status": "error", + "message": str(e) + } diff --git a/api-gateway-function-queue-async/functions/place-order/func.yaml b/api-gateway-function-queue-async/functions/place-order/func.yaml new file mode 100644 index 0000000..2024d5d --- /dev/null +++ b/api-gateway-function-queue-async/functions/place-order/func.yaml @@ -0,0 +1,8 @@ +schema_version: 20180708 +name: my-python-function +version: 0.0.18 +runtime: python +build_image: fnproject/python:3.11-dev +run_image: fnproject/python:3.11 +entrypoint: /python/bin/fdk /function/func.py handler +memory: 256 diff --git a/api-gateway-function-queue-async/functions/place-order/requirements.txt b/api-gateway-function-queue-async/functions/place-order/requirements.txt new file mode 100644 index 0000000..165eac3 --- /dev/null +++ b/api-gateway-function-queue-async/functions/place-order/requirements.txt @@ -0,0 +1,2 @@ +fdk>=0.1.96 +oci \ No newline at end of file diff --git a/api-gateway-function-queue-async/functions/process-order/Dockerfile b/api-gateway-function-queue-async/functions/process-order/Dockerfile new file mode 100644 index 0000000..75ac4f5 --- /dev/null +++ b/api-gateway-function-queue-async/functions/process-order/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.9-slim +WORKDIR /app +COPY queue_poller.py . +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +CMD ["python", "queue_poller.py"] \ No newline at end of file diff --git a/api-gateway-function-queue-async/functions/process-order/queue_poller.py b/api-gateway-function-queue-async/functions/process-order/queue_poller.py new file mode 100644 index 0000000..926adc4 --- /dev/null +++ b/api-gateway-function-queue-async/functions/process-order/queue_poller.py @@ -0,0 +1,153 @@ +import oci +import json +import logging +import os +import time +from datetime import datetime + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +def poll_queue_and_insert_to_nosql(): + """ + Polls an OCI Queue and inserts messages into a NoSQL table using Resource Principal authentication. + """ + try: + # Initialize Resource Principal signer + signer = oci.auth.signers.get_resource_principals_signer() + + # Get configuration from environment variables + region = os.environ.get("OCI_REGION", "us-ashburn-1") + queue_ocid = os.environ.get("QUEUE_OCID") + table_ocid = os.environ.get("TABLE_OCID") + + if not queue_ocid: + raise ValueError("QUEUE_OCID not found in environment variables") + if not table_ocid: + raise ValueError("TABLE_OCID not found in environment variables") + + # Initialize Queue client with explicit endpoint + queue_endpoint = f"https://cell-1.queue.messaging.{region}.oci.oraclecloud.com" + logger.info(f"Configuring QueueClient with endpoint: {queue_endpoint}") + queue_client = oci.queue.QueueClient( + config={"region": region}, + service_endpoint=queue_endpoint, + signer=signer + ) + + # Initialize QueueAdminClient for administrative operations + queue_admin_client = oci.queue.QueueAdminClient( + config={"region": region}, + signer=signer + ) + + # Initialize NoSQL client + nosql_client = oci.nosql.NosqlClient( + config={"region": region}, + signer=signer + ) + + # Verify queue existence + try: + # pylint: disable=no-member + queue_response = queue_admin_client.get_queue(queue_id=queue_ocid) + logger.info(f"Queue {queue_ocid} is accessible: Status {queue_response.status}") + except oci.exceptions.ServiceError as e: + logger.error(f"Failed to access queue {queue_ocid}: {e.message}, Status: {e.status}, Code: {e.code}, Request ID: {e.request_id}") + raise + + # Polling loop + while True: + try: + # Read messages from the queue + messages_response = queue_client.get_messages( + queue_id=queue_ocid, + visibility_in_seconds=60, # Time message is invisible after reading + timeout_in_seconds=30 # Wait time for messages + ) + + if messages_response.status != 200 or not messages_response.data.messages: + logger.info(f"No messages found in queue {queue_ocid}") + time.sleep(10) # Wait before polling again + continue + + # Process each message + for message in messages_response.data.messages: + logger.info(f"Processing message ID: {message.id}") + + # Log raw message content for debugging + logger.debug(f"Raw message content: {message.content}") + + # Parse message content as JSON (no base64 decoding) + try: + payload = json.loads(message.content) + except json.JSONDecodeError as e: + logger.error(f"JSON parsing error for message {message.id}: {str(e)}") + continue + + # Validate payload structure + if not isinstance(payload, dict) or "data" not in payload: + logger.error(f"Invalid payload format for message {message.id}: 'data' key is missing") + continue + if not all(key in payload["data"] for key in ["order_id", "customer_id", "amount"]): + logger.error(f"Invalid payload for message {message.id}: 'order_id', 'customer_id', and 'amount' are required in 'data'") + continue + + # Prepare NoSQL record + record = { + "order_id": payload["data"]["order_id"], + "customer_id": payload["data"]["customer_id"], + "amount": payload["data"]["amount"], + "created_at": datetime.utcnow().isoformat() + } + + # Insert record into NoSQL table + try: + update_row_details = oci.nosql.models.UpdateRowDetails( + value=record, + option="IF_ABSENT" # Only insert if the row doesn't exist + ) + nosql_response = nosql_client.update_row( + table_name_or_id=table_ocid, + update_row_details=update_row_details + ) + + if nosql_response.status == 200: + logger.info(f"Successfully inserted record into table {table_ocid} for message {message.id}") + + # Delete the processed message from the queue + queue_client.delete_message( + queue_id=queue_ocid, + message_receipt=message.receipt + ) + logger.info(f"Deleted message {message.id} from queue {queue_ocid}") + else: + logger.error(f"Failed to insert record into table {table_ocid} for message {message.id}: {nosql_response.status}") + except oci.exceptions.ServiceError as e: + logger.error(f"NoSQL error for message {message.id}: {e.message}, Status: {e.status}, Code: {e.code}, Request ID: {e.request_id}") + continue + + # Sleep briefly to avoid overwhelming the queue + time.sleep(1) + + except oci.exceptions.ServiceError as e: + logger.error(f"Queue service error: {e.message}, Status: {e.status}, Code: {e.code}, Request ID: {e.request_id}") + time.sleep(10) # Wait before retrying + continue + + except Exception as e: + logger.error(f"Unexpected error while polling queue: {str(e)}") + time.sleep(10) # Wait before retrying + continue + + except Exception as e: + logger.error(f"Initialization error: {str(e)}") + raise + +if __name__ == "__main__": + try: + poll_queue_and_insert_to_nosql() + except Exception as e: + logger.error(f"Fatal error: {str(e)}") + exit(1) \ No newline at end of file diff --git a/api-gateway-function-queue-async/functions/process-order/requirements.txt b/api-gateway-function-queue-async/functions/process-order/requirements.txt new file mode 100644 index 0000000..d74f848 --- /dev/null +++ b/api-gateway-function-queue-async/functions/process-order/requirements.txt @@ -0,0 +1 @@ +oci>=2.157.0 \ No newline at end of file diff --git a/api-gateway-function-queue-async/terraform/data.tf b/api-gateway-function-queue-async/terraform/data.tf new file mode 100644 index 0000000..47bfb69 --- /dev/null +++ b/api-gateway-function-queue-async/terraform/data.tf @@ -0,0 +1,17 @@ +data "oci_identity_tenancy" "home" { + tenancy_id = var.tenancy_ocid +} + +data "oci_identity_regions" "current" { + filter { + name = "name" + values = [var.region] + } +} + +# Data source for availability domains +data "oci_identity_availability_domains" "ads" { + compartment_id = var.compartment_ocid +} + +data "oci_objectstorage_namespace" "this" {} diff --git a/api-gateway-function-queue-async/terraform/main.tf b/api-gateway-function-queue-async/terraform/main.tf new file mode 100644 index 0000000..4653469 --- /dev/null +++ b/api-gateway-function-queue-async/terraform/main.tf @@ -0,0 +1,150 @@ +locals { + function_configs = { + for key, fn in var.functions : key => { + source_image = fn.source_image + path = fn.path + config = merge(fn.config, { + COMPARTMENT_ID = var.compartment_ocid + OCI_REGION = var.region + QUEUE_OCID = module.queue.queue_id + }) + } + } +} + + +module "apigateway" { + source = "./modules/apigateway" + compartment_id = var.compartment_ocid + subnet_id = var.subnet_ocid +} + +module "queue" { + source = "./modules/queue" + compartment_id = var.compartment_ocid + queue_name = var.queue_name +} + +resource "oci_nosql_table" "order_info" { + compartment_id = var.compartment_ocid + name = var.nosql_table_name + table_limits { + max_read_units = 50 + max_write_units = 50 + max_storage_in_gbs = 1 + } + ddl_statement = < v } +} \ No newline at end of file diff --git a/api-gateway-function-queue-async/terraform/modules/vcn/variables.tf b/api-gateway-function-queue-async/terraform/modules/vcn/variables.tf new file mode 100644 index 0000000..e1f3e73 --- /dev/null +++ b/api-gateway-function-queue-async/terraform/modules/vcn/variables.tf @@ -0,0 +1,80 @@ +variable "region" { + description = "The OCI region identifier, e.g., 'us-ashburn-1'." + type = string +} + +variable "compartment_id" { + description = "The OCID of the compartment where the VCN will be created." + type = string +} + +variable "vcn_name" { + description = "The name for the VCN." + type = string + default = "sample_vcn" +} + +variable "vcn_cidr" { + description = "The CIDR block for the VCN." + type = string + default = "10.0.0.0/16" +} + +variable "subnets" { + description = "A map of subnets to create. Key is the subnet name." + type = map(object({ + cidr_block = string + is_public = bool + dns_label = string + use_service_gateway = optional(bool, false) + })) + default = {} +} + +variable "create_internet_gateway" { + description = "Set to true to create an internet Gateway for public subnets." + type = bool + default = true +} + +variable "create_nat_gateway" { + description = "Set to true to create a NAT Gateway for private subnets." + type = bool + default = false +} + +variable "create_service_gateway" { + description = "Set to true to create a service Gateway." + type = bool + default = false +} + +variable "vcn_dns_label" { + description = "The DNS label for the VCN." + type = string + default = "modvcn" +} + +variable "security_lists" { + description = "Map of security lists keyed by subnet name." + type = map(object({ + ingress_rules = list(object({ + protocol = string + source = string + tcp_min = optional(number) + tcp_max = optional(number) + icmp_type = optional(number) + icmp_code = optional(number) + })) + egress_rules = list(object({ + protocol = string + destination = string + dest_type = optional(string, "CIDR_BLOCK") + tcp_min = optional(number) + tcp_max = optional(number) + icmp_type = optional(number) + icmp_code = optional(number) + })) + })) + default = {} +} diff --git a/api-gateway-function-queue-async/terraform/outputs.tf b/api-gateway-function-queue-async/terraform/outputs.tf new file mode 100644 index 0000000..e69a01a --- /dev/null +++ b/api-gateway-function-queue-async/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "post_order_repository_path" { + description = "The full URL for pushing images to the OCIR repository." + value = "${lower(data.oci_identity_regions.current.regions[0].key)}.ocir.io/${data.oci_objectstorage_namespace.this.namespace}/${var.post_order_container_repository_name}" +} + +output "process_order_repository_path" { + description = "The full URL for pushing images to the OCIR repository." + value = "${lower(data.oci_identity_regions.current.regions[0].key)}.ocir.io/${data.oci_objectstorage_namespace.this.namespace}/${var.process_order_container_repository_name}" +} + +output "api_gateway_endpoint" { + description = "The endpoint URL of the API Gateway" + value = module.apigateway.gateway_endpoint +} diff --git a/api-gateway-function-queue-async/terraform/provider.tf b/api-gateway-function-queue-async/terraform/provider.tf new file mode 100644 index 0000000..06ebaa2 --- /dev/null +++ b/api-gateway-function-queue-async/terraform/provider.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.10.0" + required_providers { + oci = { + source = "oracle/oci" + version = ">= 7.14.0" + } + } +} + +provider "oci" {} diff --git a/api-gateway-function-queue-async/terraform/terraform.tfvars b/api-gateway-function-queue-async/terraform/terraform.tfvars new file mode 100644 index 0000000..667997c --- /dev/null +++ b/api-gateway-function-queue-async/terraform/terraform.tfvars @@ -0,0 +1,17 @@ +# Update these values with your actual OCI configuration +region = "us-ashburn-1" +compartment_ocid = "ocid1.compartment.oc1..exampleuniqueID" +subnet_ocid = "ocid1.subnet.oc1..exampleuniqueID" +tenancy_ocid = "ocid1.tenancy.oc1..exampleuniqueID" +queue_name = "OrderQueue" +post_order_container_repository_name = "queue_async_repo" +application_display_name = "queue_async_app" +nosql_table_name = "order_info" +functions = { + place-order = { + source_image = null + path = "/order" + config = {} + } +} +queue_poller_image = null diff --git a/api-gateway-function-queue-async/terraform/variables.tf b/api-gateway-function-queue-async/terraform/variables.tf new file mode 100644 index 0000000..89b7ceb --- /dev/null +++ b/api-gateway-function-queue-async/terraform/variables.tf @@ -0,0 +1,61 @@ +variable "tenancy_ocid" { + default = "ocid1.tenancy.oc1..exampleuniqueID" +} + +variable "region" { + default = "us-ashburn-1" +} + +variable "compartment_ocid" { + default = "ocid1.compartment.oc1..exampleuniqueID" +} + +variable "subnet_ocid" { + default = "ocid1.subnet.oc1..exampleuniqueID" +} + +variable "queue_name" { + description = "Name of the OCI Queue" + default = "OrderQueue" +} + +variable "post_order_container_repository_name" { + description = "Name of the OCI Container Repository" + default = "queue_async_repo" +} + +variable "process_order_container_repository_name" { + description = "Name of the OCI Container Repository" + default = "process_order" +} + +variable "application_display_name" { + description = "Name of function application" + default = "queue_async_app" +} + +variable "nosql_table_name" { + default = "order_info" +} + +variable "functions" { + description = "A map of function configurations. The key is the function name." + type = map(object({ + source_image = optional(string, null) + path = optional(string, null) + config = map(string) + })) + default = { + "place-order" = { + # This function will be SKIPPED because its source_image is null. + source_image = null #Change this to image name after uploading image to OCIR + path = "/order" + config = {} + } + } +} + +variable "queue_poller_image" { + type = string + default = null +}