Home / OCI

OCIHow-toBeginner

Build your first OCI VCN with the CLI: public and private subnets, gateways and routes

Build a VCN one piece at a time so you understand it. Add an internet gateway, a NAT gateway, route tables, a security list and two subnets, then see what makes each subnet public or private and clean it up.

Deepak Bhardwaj5 min read

Applies toOCI Networking, OCI CLI

The goal

The console's VCN wizard does all this in one click, but it doesn't teach you much. You'll build the same thing by hand with the CLI: one public subnet for things reached from the internet, and one private subnet for everything else.

If the CLI isn't working yet, follow OCI CLI setup. Then set a variable for your compartment. Use a lab compartment if you have one:

export C=ocid1.compartment.oc1..your-compartment-id
oci network vcn list --compartment-id "$C" --query 'data[]."display-name"'

For background on these terms, see compartments and OCIDs.

What you're building

VCN 10.0.0.0/16
 ├─ Public subnet  10.0.1.0/24 → route 0.0.0.0/0 to the internet gateway
 └─ Private subnet 10.0.2.0/24 → route 0.0.0.0/0 to the NAT gateway
Part What it does
VCN Your private network
Subnet A slice of the VCN's address range
Route table Decides where traffic goes
Internet gateway Two-way internet access, for public IPs
NAT gateway Outbound-only internet, for private instances
Security list Firewall rules for a subnet

Step 1: create the VCN

VCN=$(oci network vcn create \
  --compartment-id "$C" \
  --cidr-blocks '["10.0.0.0/16"]' \
  --display-name lab-vcn \
  --dns-label labvcn \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)
echo "$VCN"

Pick a range that doesn't overlap with your other VCNs or your office network, since you may need to connect them later. The DNS label can't be changed after creation.

Step 2: create the gateways

IGW=$(oci network internet-gateway create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --is-enabled true --display-name lab-igw \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

NAT=$(oci network nat-gateway create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --display-name lab-nat \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

Check your account's current pricing before leaving these running. For more about each gateway, read VCN gateways and routing.

Step 3: create the route tables

PUB_RT=$(oci network route-table create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --display-name lab-public-rt \
  --route-rules '[{"destination":"0.0.0.0/0","destinationType":"CIDR_BLOCK","networkEntityId":"'"$IGW"'"}]' \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

PRIV_RT=$(oci network route-table create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --display-name lab-private-rt \
  --route-rules '[{"destination":"0.0.0.0/0","destinationType":"CIDR_BLOCK","networkEntityId":"'"$NAT"'"}]' \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

The quote pattern '"$IGW"' looks odd. It ends the single-quoted JSON, adds the variable and starts the JSON again.

You don't need a route for traffic inside the VCN. It's automatic.

Step 4: create a security list

This allows SSH from your IP only, plus all outbound traffic:

MYIP=$(curl -s https://ifconfig.me)

SL=$(oci network security-list create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --display-name lab-public-sl \
  --egress-security-rules '[{"destination":"0.0.0.0/0","protocol":"all"}]' \
  --ingress-security-rules '[{"source":"'"$MYIP"'/32","protocol":"6","tcpOptions":{"destinationPortRange":{"min":22,"max":22}}}]' \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

Protocol 6 means TCP, and 17 means UDP. In production, prefer network security groups, which apply to specific instances instead of whole subnets.

Step 5: create the subnets

Unlike some other clouds, OCI has a real setting for this: --prohibit-public-ip-on-vnic.

PUB_SUBNET=$(oci network subnet create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --cidr-block 10.0.1.0/24 \
  --display-name lab-public \
  --dns-label pub \
  --route-table-id "$PUB_RT" \
  --security-list-ids '["'"$SL"'"]' \
  --prohibit-public-ip-on-vnic false \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

PRIV_SUBNET=$(oci network subnet create \
  --compartment-id "$C" --vcn-id "$VCN" \
  --cidr-block 10.0.2.0/24 \
  --display-name lab-private \
  --dns-label priv \
  --route-table-id "$PRIV_RT" \
  --security-list-ids '["'"$SL"'"]' \
  --prohibit-public-ip-on-vnic true \
  --wait-for-state AVAILABLE \
  --query 'data.id' --raw-output)

These are regional subnets, because we didn't name an availability domain. They span every availability domain in the region, which is what you normally want. OCI reserves 3 addresses in each subnet.

The private subnet shares a security list here to keep the lab short. On a real network, give it its own, with rules for your internal traffic.

Step 6: check your work

oci network subnet list --compartment-id "$C" --vcn-id "$VCN" \
  --query 'data[].{name:"display-name", cidr:"cidr-block", private:"prohibit-public-ip-on-vnic", state:"lifecycle-state"}' \
  --output table

oci network route-table get --rt-id "$PUB_RT" --query 'data."route-rules"'

To test it, launch an instance in the public subnet by following your first Linux instance. If you can't connect, the OS firewall may be the cause, even with the right rules. See port open but still timing out.

Common problems

Problem Likely cause Fix
NotAuthorizedOrNotFound No policy, wrong OCID or wrong compartment See the policy cheat sheet
LimitExceeded Service limit reached See service limits
Instance has no public IP Private subnet, or none was requested Check the subnet, and assign one
Private instance has no internet No NAT route, or egress rule missing Check the route table. See lost outbound
InvalidParameter on JSON Quoting problem in the shell echo the JSON string first

Step 7: clean up

Remove the pieces in this order, or you'll get a Conflict error. Terminate any instances first.

oci network subnet delete --subnet-id "$PUB_SUBNET" --force --wait-for-state TERMINATED
oci network subnet delete --subnet-id "$PRIV_SUBNET" --force --wait-for-state TERMINATED
oci network route-table delete --rt-id "$PUB_RT" --force --wait-for-state TERMINATED
oci network route-table delete --rt-id "$PRIV_RT" --force --wait-for-state TERMINATED
oci network security-list delete --security-list-id "$SL" --force --wait-for-state TERMINATED
oci network internet-gateway delete --ig-id "$IGW" --force --wait-for-state TERMINATED
oci network nat-gateway delete --nat-gateway-id "$NAT" --force --wait-for-state TERMINATED
oci network vcn delete --vcn-id "$VCN" --force --wait-for-state TERMINATED

Caution

--force skips the confirmation prompt. Check that the variables still hold your lab's OCIDs. In a new terminal they'll be empty or, worse, hold values you set for something else.

Prevent it next time

  • Plan your address ranges before building, and leave room to grow.
  • Use a separate subnet per tier, and keep databases in private subnets.
  • Add a service gateway so private instances can reach OCI services without internet access.
  • Build real networks with Terraform and Resource Manager.