diff --git a/mcp-servers-on-oci-container-instances/deploy/deploy.md b/mcp-servers-on-oci-container-instances/deploy/deploy.md
index 68b289cdd..c0d1b708e 100644
--- a/mcp-servers-on-oci-container-instances/deploy/deploy.md
+++ b/mcp-servers-on-oci-container-instances/deploy/deploy.md
@@ -4,7 +4,8 @@
In this lab, you create an OCI Resource Manager stack from the workshop package
and run the apply job. Resource Manager creates the OCI networking, API Gateway,
-Container Instance, and three MCP server containers.
+Container Instance, and three MCP server containers. The Container Instance runs
+in a private subnet and uses a NAT Gateway for outbound public image pulls.
Estimated Time: 15 minutes
@@ -22,13 +23,13 @@ In this lab, you will:
Complete the workshop introduction and the Get Started lab. Make sure you can
access an OCI tenancy and a compartment where you can create Resource Manager,
-networking, API Gateway, and Container Instance resources.
+networking, NAT Gateway, API Gateway, and Container Instance resources.
## Task 1: Launch the Resource Manager stack
1. Select **Deploy to Oracle Cloud**.
- [](https://cloud.oracle.com/resourcemanager/stacks/create?zipUrl=https://github.com/Phirlly/developer/raw/main/mcp-servers-on-oci-container-instances/files/resource-manager/mcp-servers-on-oci-container-instances-rm.zip)
+ [](https://cloud.oracle.com/resourcemanager/stacks/create?zipUrl=https://github.com/oracle-livelabs/developer/raw/main/mcp-servers-on-oci-container-instances/files/resource-manager/mcp-servers-on-oci-container-instances-rm.zip)

diff --git a/mcp-servers-on-oci-container-instances/files/resource-manager/mcp-servers-on-oci-container-instances-rm.zip b/mcp-servers-on-oci-container-instances/files/resource-manager/mcp-servers-on-oci-container-instances-rm.zip
index 757b07468..91b62da78 100644
Binary files a/mcp-servers-on-oci-container-instances/files/resource-manager/mcp-servers-on-oci-container-instances-rm.zip and b/mcp-servers-on-oci-container-instances/files/resource-manager/mcp-servers-on-oci-container-instances-rm.zip differ
diff --git a/mcp-servers-on-oci-container-instances/files/terraform/container-instance.tf b/mcp-servers-on-oci-container-instances/files/terraform/container-instance.tf
index e0f1cc993..e728c4b9b 100644
--- a/mcp-servers-on-oci-container-instances/files/terraform/container-instance.tf
+++ b/mcp-servers-on-oci-container-instances/files/terraform/container-instance.tf
@@ -17,7 +17,7 @@ resource "oci_container_instances_container_instance" "mcp_servers" {
vnics {
display_name = "${local.name_prefix}-vnic"
- is_public_ip_assigned = true
+ is_public_ip_assigned = false
subnet_id = oci_core_subnet.container_instance.id
}
diff --git a/mcp-servers-on-oci-container-instances/files/terraform/network.tf b/mcp-servers-on-oci-container-instances/files/terraform/network.tf
index ab1d666b1..38f4cc0ba 100644
--- a/mcp-servers-on-oci-container-instances/files/terraform/network.tf
+++ b/mcp-servers-on-oci-container-instances/files/terraform/network.tf
@@ -14,6 +14,13 @@ resource "oci_core_internet_gateway" "mcp_lab" {
vcn_id = oci_core_vcn.mcp_lab.id
}
+resource "oci_core_nat_gateway" "mcp_lab" {
+ compartment_id = var.compartment_ocid
+ display_name = "${local.name_prefix}-nat"
+ freeform_tags = local.common_freeform_tags
+ vcn_id = oci_core_vcn.mcp_lab.id
+}
+
resource "oci_core_route_table" "mcp_lab" {
compartment_id = var.compartment_ocid
display_name = "${local.name_prefix}-rt"
@@ -27,6 +34,19 @@ resource "oci_core_route_table" "mcp_lab" {
}
}
+resource "oci_core_route_table" "container_instance" {
+ compartment_id = var.compartment_ocid
+ display_name = "${local.name_prefix}-container-instance-rt"
+ freeform_tags = local.common_freeform_tags
+ vcn_id = oci_core_vcn.mcp_lab.id
+
+ route_rules {
+ destination = "0.0.0.0/0"
+ destination_type = "CIDR_BLOCK"
+ network_entity_id = oci_core_nat_gateway.mcp_lab.id
+ }
+}
+
resource "oci_core_security_list" "api_gateway" {
compartment_id = var.compartment_ocid
display_name = "${local.name_prefix}-api-gateway-sl"
@@ -113,8 +133,8 @@ resource "oci_core_subnet" "container_instance" {
display_name = "${local.name_prefix}-container-instance-subnet"
dns_label = "mcpservers"
freeform_tags = local.common_freeform_tags
- prohibit_public_ip_on_vnic = false
- route_table_id = oci_core_route_table.mcp_lab.id
+ prohibit_public_ip_on_vnic = true
+ route_table_id = oci_core_route_table.container_instance.id
security_list_ids = [oci_core_security_list.container_instance.id]
vcn_id = oci_core_vcn.mcp_lab.id
}
diff --git a/mcp-servers-on-oci-container-instances/introduction/introduction.md b/mcp-servers-on-oci-container-instances/introduction/introduction.md
index baf49309e..23172aef9 100644
--- a/mcp-servers-on-oci-container-instances/introduction/introduction.md
+++ b/mcp-servers-on-oci-container-instances/introduction/introduction.md
@@ -31,7 +31,10 @@ In this workshop, you will:
The Resource Manager stack creates:
-* one VCN for the lab;
+* one VCN for the lab, with a public API Gateway subnet and a private Container
+ Instance subnet;
+* one NAT Gateway for outbound access from the private Container Instance
+ subnet;
* one OCI API Gateway HTTPS endpoint;
* one OCI Container Instance;
* three containers in that Container Instance:
@@ -50,7 +53,8 @@ After deployment, Resource Manager returns three MCP URLs:
You need:
* access to an OCI tenancy and a compartment where you can create Resource
- Manager stacks, networking, API Gateway, and Container Instance resources;
+ Manager stacks, networking, NAT Gateway, API Gateway, and Container Instance
+ resources;
* an MCP-capable AI client for the client-connection labs;
* a least-privilege GitHub token available for the GitHub MCP lab.
diff --git a/mcp-servers-on-oci-container-instances/readme.md b/mcp-servers-on-oci-container-instances/readme.md
index 0c7346853..d16e173b3 100644
--- a/mcp-servers-on-oci-container-instances/readme.md
+++ b/mcp-servers-on-oci-container-instances/readme.md
@@ -19,7 +19,7 @@ Use the step-by-step guide:
## Deploy to OCI
-
+
diff --git a/mcp-servers-on-oci-container-instances/validate/validate-deployment.md b/mcp-servers-on-oci-container-instances/validate/validate-deployment.md
index f7935f003..b7acd0aed 100644
--- a/mcp-servers-on-oci-container-instances/validate/validate-deployment.md
+++ b/mcp-servers-on-oci-container-instances/validate/validate-deployment.md
@@ -46,8 +46,8 @@ Complete Lab 1 and start the Resource Manager apply job for this workshop.
## Task 3: Review the created resources
-1. Open the job resources and confirm Resource Manager created the expected API
- Gateway, networking, and Container Instance resources.
+1. Open the job resources and confirm Resource Manager created the expected
+ networking, NAT Gateway, API Gateway, and Container Instance resources.

diff --git a/mcp-servers-on-oci-container-instances/validation/terraform_contracts.py b/mcp-servers-on-oci-container-instances/validation/terraform_contracts.py
index 8958b11c7..44fc90207 100644
--- a/mcp-servers-on-oci-container-instances/validation/terraform_contracts.py
+++ b/mcp-servers-on-oci-container-instances/validation/terraform_contracts.py
@@ -138,6 +138,27 @@ def extract_named_blocks(text: str, block_name: str) -> list[str]:
return blocks
+def extract_labeled_block(text: str, header: str) -> str | None:
+ header_index = text.find(header)
+ if header_index == -1:
+ return None
+
+ open_index = text.find("{", header_index)
+ if open_index == -1:
+ return None
+
+ depth = 0
+ for index in range(open_index, len(text)):
+ if text[index] == "{":
+ depth += 1
+ elif text[index] == "}":
+ depth -= 1
+ if depth == 0:
+ return text[header_index : index + 1]
+
+ return None
+
+
def validate_versions(texts: dict[str, str], failures: list[str]) -> None:
text = texts.get("versions.tf", "")
require_contains(failures, "versions.tf", text, 'source = "oracle/oci"')
@@ -271,12 +292,16 @@ def validate_network(texts: dict[str, str], failures: list[str]) -> None:
"oci_core_vcn",
"oci_core_subnet",
"oci_core_internet_gateway",
+ "oci_core_nat_gateway",
"oci_core_route_table",
"oci_core_security_list",
]:
require_contains(failures, "network.tf", text, f'resource "{resource_type}"')
for resource in [
+ 'resource "oci_core_nat_gateway" "mcp_lab"',
+ 'resource "oci_core_route_table" "mcp_lab"',
+ 'resource "oci_core_route_table" "container_instance"',
'resource "oci_core_security_list" "api_gateway"',
'resource "oci_core_security_list" "container_instance"',
'resource "oci_core_subnet" "api_gateway"',
@@ -295,7 +320,56 @@ def validate_network(texts: dict[str, str], failures: list[str]) -> None:
require_contains(failures, "network.tf", text, "var.terraform_mcp_port")
require_contains(failures, "network.tf", text, "var.github_mcp_port")
require_contains(failures, "network.tf", text, "var.playwright_mcp_port")
- require_contains(failures, "network.tf", text, "prohibit_public_ip_on_vnic = false")
+ require_contains(failures, "network.tf", text, "network_entity_id = oci_core_internet_gateway.mcp_lab.id")
+ require_contains(failures, "network.tf", text, "network_entity_id = oci_core_nat_gateway.mcp_lab.id")
+ require_contains(failures, "network.tf", text, "route_table_id = oci_core_route_table.mcp_lab.id")
+ require_contains(
+ failures,
+ "network.tf",
+ text,
+ "route_table_id = oci_core_route_table.container_instance.id",
+ )
+ api_gateway_subnet = extract_labeled_block(
+ text, 'resource "oci_core_subnet" "api_gateway"'
+ )
+ if api_gateway_subnet is None:
+ failures.append("network.tf must define the API Gateway subnet resource")
+ else:
+ if "prohibit_public_ip_on_vnic = false" not in api_gateway_subnet:
+ failures.append("network.tf must keep the API Gateway subnet public")
+ if "route_table_id = oci_core_route_table.mcp_lab.id" not in api_gateway_subnet:
+ failures.append(
+ "network.tf must route the API Gateway subnet through the internet gateway route table"
+ )
+
+ container_instance_subnet = extract_labeled_block(
+ text, 'resource "oci_core_subnet" "container_instance"'
+ )
+ if container_instance_subnet is None:
+ failures.append("network.tf must define the Container Instance subnet resource")
+ else:
+ if "prohibit_public_ip_on_vnic = true" not in container_instance_subnet:
+ failures.append("network.tf must make the Container Instance subnet private")
+ if (
+ "route_table_id = oci_core_route_table.container_instance.id"
+ not in container_instance_subnet
+ ):
+ failures.append(
+ "network.tf must route the Container Instance subnet through the NAT route table"
+ )
+
+ container_instance_route_table = extract_labeled_block(
+ text, 'resource "oci_core_route_table" "container_instance"'
+ )
+ if container_instance_route_table is None:
+ failures.append("network.tf must define the Container Instance route table")
+ elif (
+ "network_entity_id = oci_core_nat_gateway.mcp_lab.id"
+ not in container_instance_route_table
+ ):
+ failures.append(
+ "network.tf must route private Container Instance subnet egress through NAT Gateway"
+ )
ingress_blocks = extract_named_blocks(text, "ingress_security_rules")
for port_variable in [
@@ -335,7 +409,7 @@ def validate_container_instance(texts: dict[str, str], failures: list[str]) -> N
require_contains(failures, "container-instance.tf", text, 'data "oci_core_vnic"')
require_contains(failures, "container-instance.tf", text, "available_container_shape_names")
require_contains(failures, "container-instance.tf", text, "precondition")
- require_contains(failures, "container-instance.tf", text, "is_public_ip_assigned = true")
+ require_contains(failures, "container-instance.tf", text, "is_public_ip_assigned = false")
require_contains(failures, "container-instance.tf", text, "subnet_id = oci_core_subnet.container_instance.id")
require_contains(failures, "container-instance.tf", text, "var.container_ocpus <= 64")
require_contains(failures, "container-instance.tf", text, "var.container_ocpus <= 94")
@@ -383,6 +457,9 @@ def validate_container_instance(texts: dict[str, str], failures: list[str]) -> N
f"container-instance.tf must not configure secret environment name {unsafe_name}"
)
+ if "is_public_ip_assigned = true" in text:
+ failures.append("container-instance.tf must not assign a public IP to the Container Instance")
+
def validate_api_gateway(texts: dict[str, str], failures: list[str]) -> None:
text = texts.get("api-gateway.tf", "")