Kubernetes setup on Oracle Cloud Infrastructure (OCI) using Always Free tier resources.
Default setup manages the following components:
- Kubernetes Cluster (OKE) with 1 worker node
- Load Balancer for ingress traffic management
- NAT Gateway for outbound internet connectivity
- OpenVPN Instance for secure access to private Kubernetes API
- DNS Zones for internal and external DNS management
- cert-manager for automated TLS certificate management
- F5 NGINX Gateway Fabric as a Kubernetes Gateway API implementation
- ExternalDNS for automatic DNS record management for Kubernetes services
- Cloudflare Origin CA issuer (optional) for issuing origin certificates to hosts served behind Cloudflare's proxy
This repo is published on GitHub. No domain names, hostnames, project names, or other identifying values may appear as defaults or in comments in any tracked file (*.tf, *.md, *.yaml). Such values are declared as required variables with no default and supplied only through each stack's gitignored terraform.tfvars. When adding a variable whose value is environment- or domain-specific, give it no default and document it generically — never bake the real value into tracked code.
- Active OCI account with appropriate permissions
- OCI CLI configured with authentication profile
- SSH key pair for VPN instance access
Configure OpenTofu backend for each module. See examples in backend.tf.template.
task oci-platform:install-networktask oci-platform:install-identitytask oci-platform:generate-ssh-keys
task oci-platform:install-oketask oci-platform:generate-oci-keys
task oci-platform:install-dns-
Launch OpenVPN Instance from OCI Marketplace
- Follow the guide: https://openvpn.net/as-docs/oracle.html#oracle-vpn--cloud-vpn-quick-start-guide-from-openvpn
- Use these parameters:
- Network Security Group:
vpn_nsg - Subnet:
vpn_subnet - Shape:
VM.Standard.E2.1.Micro(Always Free-eligible)
- Network Security Group:
-
Install OpenVPN Client
- Download and install from: https://openvpn.net/client/
-
Configure OpenVPN Server Settings
Access the OpenVPN Admin UI and configure:
Routing Settings:
- Should VPN clients have access to private subnets?: Yes
- Specify the private subnets to which all clients should be given access (one per line):
- CIDR block of
k8s_pod_subnet - CIDR block of
k8s_api_subnet - CIDR block of
k8s_worker_subnet
- CIDR block of
- Should client Internet traffic be routed through the VPN?: No
DNS Settings:
- Do not alter clients' DNS server settings: No
- Have clients use the same DNS servers as the Access Server host: No
- Have clients use specific DNS servers: Yes
- Primary DNS Server:
169.254.169.254 - Secondary DNS Server:
8.8.8.8
-
Connect to VPN
- Download the VPN profile from OpenVPN Access Server
- Import the profile into OpenVPN Client
- Connect to verify access to private subnets
If you already know the OpenVPN image OCID and have added it to 05-vpn/terraform.tfvars:
task oci-platform:install-vpnAfter deployment, follow steps 2-4 from Option A to configure the OpenVPN server and connect.
Important: Before proceeding, ensure:
- OpenVPN is connected and you have access to private subnets
- Configure access to your OKE cluster following the guide: https://docs.oracle.com/en-us/iaas/Content/ContEng/Tasks/contengdownloadkubeconfigfile.htm
task oci-platform:install-cert-managertask oci-platform:install-nginx-gatewaytask oci-platform:install-external-dnstask oci-platform:install-external-secretstask oci-platform:install-gitlab-agenttask oci-platform:install-filesystem-storage-classThis creates a Kubernetes StorageClass backed by the OCI File Storage CSI driver (fss.csi.oraclecloud.com).
Before applying it:
- Ensure the Step 2 identity layer has been reapplied so the new
oke-fss-csi-policyexists. - In
11-filesystem-storage-class/terraform.tfvars, setmount_target_subnet_ocidto the dedicatedsubnet_ids.fss_mount_targetoutput from01-network. - This stack now creates one shared mount target up front and passes its
mountTargetOcidinto the StorageClass, so dynamically provisioned PVCs reuse the same mount target instead of creating new ones. - Configure the File Storage network security rules for the worker nodes and mount target as described in Oracle's manual: https://docs.oracle.com/en-us/iaas/Content/ContEng/Tasks/contengcreatingpersistentvolumeclaim_Provisioning_PVCs_on_FSS.htm
task oci-platform:install-origin-ca-issuerInstalls the Cloudflare Origin CA issuer and issues an origin certificate for a host served behind Cloudflare's proxy (Full/strict SSL). Use this when a public hostname on the shared Load Balancer is fronted by Cloudflare rather than getting a public Let's Encrypt certificate directly.
Depends on Steps 6.2 (NGINX Gateway) and 6.4 (External Secrets): the origin certificate's Secret is written into the gateway namespace, and the scoped Cloudflare Origin CA API token is pulled from OCI Vault through the existing oci-secret-store ClusterSecretStore.
Two-phase apply. Like External Secrets (Step 6.4), the task runs tofu apply twice: the first pass (with -exclude on the ExternalSecret, ClusterOriginIssuer, and Certificate) installs the namespace, the issuer CRDs, and the Helm chart; the second unrestricted pass creates the custom resources once their CRDs exist. Run it via the task above — do not run a single tofu apply.
Before applying it:
- Create
12-origin-ca-issuer/terraform.tfvars(gitignored) and set the required variables — notably the name of the OCI Vault secret holding the scoped Origin CA token, the origin certificate's Secret name (must match the name the gateway listener expects,replace(host, ".", "-") + "-cert"), and its SANs. None of these have defaults, by design (see the public-repo rule above). - The origin certificate is intentionally named identically to its target Secret and carries no owner reference, so cert-manager's Gateway shim detects it and does not issue a competing certificate for the same listener.
The shared Load Balancer NSG (
modules/network) intentionally keeps its world-open:443ingress rule — a Cloudflare-only firewall lockdown was considered and deliberately not adopted, to keep the LB flexible for non-Cloudflare-fronted traffic. The origin is reachable directly, not only through Cloudflare's proxy; that tradeoff is a conscious choice, not an oversight.
task oci-platform:install-block-storage-classCreates a Kubernetes StorageClass backed by the OCI Block Volume CSI driver (blockvolume.csi.oraclecloud.com) with reclaimPolicy: Retain, and annotates it as the cluster default so that a PVC created without an explicit storageClassName gets a volume that survives PVC deletion.
Notes:
- A
StorageClass'sreclaimPolicyis immutable and is read only at provisioning time. Changing it therefore requires a new class (or a delete-and-recreate), and it has no effect on already-provisionedPersistentVolumes — those carry their own copy of the field. - The block volume class shipped by the managed control plane is left in place, untouched, as the explicit opt-in for genuinely disposable volumes. This stack only patches its
storageclass.kubernetes.io/is-default-classannotation tofalseviademote_storage_class_name, so the cluster has exactly one default class. - The control plane's CSI addon may reconcile that annotation back. After applying, verify with
kubectl get scthat exactly one class is marked(default), and re-check after cluster or addon upgrades. - Destroying this stack restores the demoted class's default-class annotation instead of just dropping the key, so a teardown does not leave the cluster with no default class. That restore shells out to
kubectl, sokubectlmust be onPATHandk8s_contextmust name a reachable context at destroy time.
task oci-platform:install-metrics-serverInstalls metrics-server into kube-system, which registers the metrics.k8s.io aggregated API. OKE does not ship it, so without this stack kubectl top node / kubectl top pods fail with Metrics API not available, and any HorizontalPodAutoscaler cannot read CPU or memory.
Notes:
- Runs with
--kubelet-insecure-tls. OKE kubelets serve a node-local self-signed certificate rather than one issued by the cluster CA (serverTLSBootstrapis off and no kubelet-serving CSRs are issued), so there is nothing for metrics-server to verify the kubelet against; without the flag every scrape fails withx509: certificate signed by unknown authority. - The chart's default
--kubelet-preferred-address-typesalready prefersInternalIP, which is what OKE nodes publish, so no override is needed. - The container has a memory limit but deliberately no CPU limit — CPU throttling stalls the scrape loop and leaves gaps in the reported metrics.
- Metrics are in-memory and short-lived; this provides live utilization only, not historical trend data. A restart resets it, and it is not a substitute for Prometheus.