From 8bb8079d686d26c4ca6b41329fc7b037b3677091 Mon Sep 17 00:00:00 2001 From: "Adekola O. Okunola" Date: Tue, 15 Sep 2026 15:33:59 -0500 Subject: [PATCH] Improve MCP LiveLab launch networking --- .../deploy/deploy.md | 7 +- ...-servers-on-oci-container-instances-rm.zip | Bin 5839 -> 5873 bytes .../files/terraform/container-instance.tf | 2 +- .../files/terraform/network.tf | 24 +++++- .../introduction/introduction.md | 8 +- .../readme.md | 2 +- .../validate/validate-deployment.md | 4 +- .../validation/terraform_contracts.py | 81 +++++++++++++++++- 8 files changed, 115 insertions(+), 13 deletions(-) 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 757b07468c89c8beb324fcfba2d1aeba1fff91f3..91b62da78a0d35c0a8fc4e92a88d9bef070520b3 100644 GIT binary patch delta 1987 zcmV;!2R!)CE%7a|`2~MvdoNuhIYw5=0ssKp5C8xQ0001PWpsCMa%(PhX5Cj&Z`v>r ze&<(M(OzMdR_%?4t(Dqpn})W&Rh8wKgIaa$=Qo1p|2S*f_R2z((;#G*u|T#Gnjj|0Y}O;UkA0pq!vvN3;K2pm;k(&TA~Q&Z%z zw({OF`i$8P{T6yeko;-vmLjLhrCO}G5eejx(Wl2EHoV~}!c0A^har~`ZCpqOG-CyB zNKdLTG(&)y#e{n?cVVbNq2v;4cx`mB9t^O_R~Zjz!1KBAuoSD;lJ7}L&6go>Fe274 zWo%2yV+MQjVJv?XR+S(WT~%?kp)sQL+Nm4yzgW{s`>J&?JG7Yx!lF7KnH?UJ9K{w) zFw@}j^eHiLUQtn%H6=iFGRnW7Pomuz?pG<~F=tojfB5&Gzt4UYL1g@;#d}Vqm0uOb z=xmK@T8yoG)m>WSg))GI=DaWxjv_(1%y7HRhgGH0wf%ntaUe4cI9fSRl8972wfzya zApGG5f9UjaMSwXGc25+$uO(%w;QQitdb9VqH6P%wu>{CsIctm>6h)Zc&I$D_prg+b>@dR z#Wf&`kUf96zCHJDJNEK$wx{JX3}TBg@si>{bwx154b+mTb| z$96ycST**kIz+!ct3Id4X;#X!^Os}?hzdm0ikocy&NkP^v}4O71Kg=td7kEH3|Azf z;R{2@&6v%Ec2M3z^4nIk3T=0?`kkS6C=h_FPQPhCJF3bM%58IZ?#QCowY~IA=lS%q zb(&M((m`z}V1FYyzy+W?qLns(J@UIHE+eo~pq9ACAwmDFw zMpC&T&41q^b+P0)jqMbe69*B`{rk=##e`Z4fr>eam9&&grU{qEQW-O|VU3t2&6o)w zpx2ZOIuk5_`wl>?(u``GGHD47`yT zWvSA42rgNF%*-RbmK60LT(oP+V^fCNgw50FILpA>aYM3vCU{JEMyN4-Az6ZUb1Dq; z8YZ7fwqmFDTr`Ttj`a%B;|7U5Yd+Ol_~U$=iCd;MbxefVWns}}WzH#GFp_Jr!LY`- zHYcu0y-8JY)$9ldmod+%AefFDHCMUSx#tpq$!o5EmGmu$F1gci)D#r}3`eJDe+|j> z6B$p2m#5Q<@#xBB?y(C>r7!0bG8}!Jj4wvhMv4U~E;S~TOmPg6QM)XSrkAIqtI7Cs zO8y*QO>3jzF!DdaBegH%kHeLkv6|7;r5i4-%^s_}Kef75sL$o1`xv%NY1{MG4=-SH z!R<1CpY2CuVY@$vR4;NzpyTapi9_H)IjEk(P;g_J43`MRYPrI?q37VJpIfjs()5@$ zjAqoZ&~aN^KEZv%2I)&eCB2LIQRM zEyYW%Ql6(xC8JJHP&X{`ij8dFGQT*-PXS*;Ynmpq$p(JS@409iY9Jn!aU(|Ob@UE@G2cYqj!#JPsJVuHn( zD6APEp&*roIvC(S0Cvg1tSst784&;0aSz3z9>MuiDZ@ZX{LX)aSNH#5i+6(qz~8NZ z%IQ5sLAmK{gV+glJ5|eeHvj=ASoHZtbcKgfLUj@i2hk_NZUcDzded-+K>;GU=>;H)HCoS7AV1JxIN^iUU_0jU^ z^|tqe?4;QJ{li}e8(qO0*K4)2c6;nv<Oa-YwBam#>3o3m>x zd`5w#;zMH!`=MhBMIYSoy&wXjR0pWX)qQbjlg8Z^?*aZ&vEfeW*4_1?x-x8Yw~QKI zB7e{MdiX6WJz?zs0JAC&a|RA&doNuhIYw5=0ssKp5RP003cdX)R}AbY*v8 zc`kHjP)h*<6ay3h000O8WqU7OQkqkDJp=#%R}TOH6#xJL0000000000q=DR%Fcv}q VOp|jKHv#dJs}>#x&lCUv006Po#x4K= delta 1991 zcmV;&2RQigEzd2m`2~N%Ob%Tv;6sI~0ssJF4gdfO0001PWpsCMa%(PhX5Ci7Zrd;n zz2_?kW7lK_R_xZp)&Xs?4GXs3ib9YTCKYN+1|=sAiv0U1J8|vUZk-OniaM9rqMqI( zpF|Nlo0CF_tkObRLa|gPf+$s)NMszr;|M_6G?V00G+yW#oWXxm(xm>8C(lBho{@+X ztL`1}a|Sp3UnUcV6i;Kf968f2OXOM@nPL$ceRwQm!xxbe&h^7)9JqvN;{q8>07~3I zPw6l;C&am>RCu{?VW>o5a;Y_q$@Oc+50s=AtH2wLj1!yz z>?C>0@jyKQA+dkD0^#U-6(?I66QoOU==Bd*H*!1Mx zp~@+MCE9t;yv+2I`;L%(#vgC-$1XW;C2+xl-IK(=nzVnbk&*RQ*<@QQnG{Sn0aTmk zQ5ETIhdRyUL@T(wzMlSpFVicyD3$Y;n`M>@*16i}^6R!#9eyc>JxjI4O1s4YQXQ5( zJ^i$Mmk+h~%>g!0)_AE665GmoX%rsC4b?K^-7@dr6xT>31AF*+K80ASdv*g0wrRs6 zE{}OL4>x~EnV{PC6(vHg+r4C*P)omKCqmjXOTOWyXF@iwNydftmwEgqGqKSAE|2@u z&^7+5987tmtVl2J{F}@}R?+LvMPJObM@}iHtL0SuVE-AWs&!5^A%>k<%{c?QSyi6B zJK`P?Cy1w&SFgQeu1jgpk;ewO)3NpFo3I1zv=EB^~x~HZTocYVKL~ISb3(mKfP?7_SFCBpso%$+(-^-iR9j?mD-j1 zH%I*iv)~8@0)IP84qfKd0QWis0034G000#L003ieZggR3Ze?;UX>N0LVQyn(E_7za zS8Z?GHW2>qUvUtM!Y(2$9-OpYmjOxYb^%t~NW4!)LC_LybC5`lr1F9^|9wYFq9w;^ zY^T7SIEZ-f&vQqL2{jY~8FLa#VJH_&5iYc$B4&ES8h_DCnll|hK(8rJ=`3Xd+;;$C zndMZOj0r<{0`7AH*HlGK2?k0~xw7RcH+LkF8OErTH%K}UosME!7Aj`Dq>4FaaZWT- z*G%a$%iRL+SXIJxp3*xaXvP3)<>1wQDr1^PcA4ag&H3#^=+g~-y69o97@JF#5knW+ zdnnB@V1FhBX{n%z;{)2CI>QW8I*gTDnwG5mc*Ca1yDUS)y3Due}1WVA)sDe2KUCabqk>eegeWI{o zokH}uKO#q(%hU^hoNhC3!<3?yh!C4BESs!KIe()IMhcZ~F!T_6b7Fhcnbh^xy^d&b z5%Zj;1kn*THz2lP!8&+Fig2NOoU4WVzpdj-O#ge_~#aEjWiu*4Wk*=EVSHu%O|*R*dTpL zC`Ec#*?fBe`c{0S_+n}HV_;?flUWGEC4ZKQBc+=d&y>8u99K*6Qpt=LSyM^KP*ai2 z-!yf&mMJwXS*I-Ja~9vlDXSR6Ef`r$xZzUxtz@-Q z0$=g0yJfHVR{^qi<~ZK_<(VGA`BF;FKnncMe}h-||6q%Eg9E_dt;*>=L_xLbY=hVdbUSs+ zb~gY4Cs_3PMRbLSQbJV{4F}Pul-&mK`t_#M7=Ay2<3SI;f8WvV(X(#f@8R~e|E$}$ z$2HkclD=-mZezw0-KO`vR)UM_2!F5Meo~%s6Txr*ZW1&UISv@21mp9`-`Ib{qqgL| zm)JHr?YE_Gk4x$PH2eQxQ|teT@h2_Y?_YnMKt^x7{q@mu==HYugY0D3{r$sV2OC|% z8{2E%S-U-Ut@3D(D*HRt(my&J{G4PPZT - + 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", "")