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**. - [![Deploy to Oracle Cloud](../images/deploy-to-oracle-cloud-centered.svg)](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) + [![Deploy to Oracle Cloud](../images/deploy-to-oracle-cloud-centered.svg)](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) ![Deploy to Oracle Cloud button](../images/01-create-stack-package.png) 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

- + Deploy to Oracle Cloud

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. ![Resource Manager job resources](../images/10-job-resources.png) 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", "")