[{"content":" TL;DR # This is my personal Kubernetes lab running on Oracle Cloud. It is not just a local test setup, but a small live cloud environment where I practice all things Kubernetes.\nThe infrastructure is built with OpenTofu, and Kubernetes resources are managed from Git with FluxCD. Public HTTPS traffic goes through an OCI Network Load Balancer and Envoy Gateway. TLS, DNS, storage, metrics, logs and external status monitoring are part of the same Git-managed setup.\nIn practice, the lab shows how a cloud-based Kubernetes environment can be built repeatably, managed through GitOps and monitored from the outside. It also gives me a realistic place to test upgrades, deployment patterns and infrastructure changes without treating Kubernetes as a purely local sandbox.\nLinks # If you want to explore the environment in more detail, here are the most important links. The repositories show how the lab is built, Grafana exposes selected metrics and the status page shows external availability monitoring.\nPart Link Description Cluster repository oke-gitops-cluster The infrastructure and GitOps structure of the running OKE lab. Template oke-gitops-template A more reusable starting point for building a similar cluster. Grafana public dashboard A limited public view into selected cluster metrics. Status status.hanhela.org External uptime monitoring for selected services. Why this lab exists # I wanted an environment where Kubernetes is not just a set of isolated commands or local examples. The goal is to keep a small but real setup running, with the same basic concerns that appear in larger environments: networking, application delivery, certificates, secrets, storage, monitoring and documentation.\nOracle Kubernetes Engine is a good fit for this because it makes the lab inexpensive to run, and in some cases it can even be run for free. At the same time, the setup is still close enough to a normal cloud environment, because it runs on a managed Kubernetes service instead of a local test cluster.\nI also wanted the lab to be operated mostly from the command line instead of through individual clicks in a cloud console. The cloud console is still useful for inspection and management, but the main workflow for this lab is built around code, the command line and Git. The cluster can be initialized, validated, brought up, torn down and rebuilt in a repeatable way. Node updates are also handled with a separate script.\nWhat this demonstrates # Through this lab, I have practiced especially:\nbuilding cloud infrastructure as code managing Kubernetes resources with a GitOps workflow routing HTTPS traffic with Gateway API automating certificates, DNS and secrets using persistent storage inside Kubernetes building metrics, logs and external availability monitoring documenting the environment so that it can be understood and rebuilt The important part is not any single tool, but how the pieces work together. Infrastructure, GitOps, application delivery, monitoring and documentation all support the same environment instead of remaining separate experiments.\nHow it fits together # The basic idea is simple: infrastructure is built as code, applications are described as manifests, and changes are applied through Git.\nChanges: GitHub → FluxCD → Kubernetes resources Public traffic: Internet → OCI Network Load Balancer → Envoy Gateway → Kubernetes Service → Pod Changes are made in Git first. FluxCD watches the repository and moves the cluster towards the desired state described there. Public traffic first reaches the OCI Network Load Balancer. From there, Envoy Gateway routes HTTPS traffic to the right Kubernetes services.\nThis keeps application delivery and traffic routing as separate but understandable parts of the same system.\nCore components # Part Components Description Kubernetes platform OKE Managed Kubernetes cluster in Oracle Cloud. IaC OpenTofu Builds the OCI network, OKE cluster and supporting resources. GitOps FluxCD Keeps the cluster state aligned with the Git repository. Public traffic OCI Network Load Balancer, Envoy Gateway, Gateway API Routes public HTTPS traffic to services without traditional Ingress resources. TLS and DNS cert-manager, ExternalDNS, Cloudflare DNS-01 Creates TLS certificates and manages DNS records automatically. Secrets OCI Vault, External Secrets Operator Stores secrets in OCI Vault and syncs them into Kubernetes. Storage Longhorn Provides the persistent storage layer inside the cluster. Metrics and logs Prometheus, Grafana, Loki, Alloy Collects metrics and logs and makes them visible in dashboards. Uptime monitoring Better Stack Monitors service availability from outside the cluster. GitOps workflow # Changes are made in GitHub first. Flux watches the repository and keeps the cluster aligned with the desired state described there.\nIn practice, a new service is added by creating its manifests in the repository and merging the change to the main branch. Once the change is merged, Flux detects it and starts reconciling the cluster toward the new state. If something goes wrong, it shows up in both Flux status and the monitoring tools.\nThe repository is split mainly into two parts. terraform/ contains the infrastructure side of the lab: the base cloud resources, the OKE cluster and the Flux installation. gitops/ contains the Kubernetes side: cluster-specific settings, infrastructure controllers, addons and application manifests.\nWith Flux, the repository structure is flexible as long as the Flux Kustomizations point to the right paths (read more in the Flux documentation). In this repository, the cluster-specific clusters directory collects the Kustomizations for this environment, and the actual cluster content is split into four layers.\nPart Description Controllers Cluster controllers such as cert-manager, Envoy Gateway, External Secrets Operator, Longhorn and metrics-server. Configs Configuration used by the controllers, such as Gateways, ClusterIssuers, ExternalSecrets and StorageClasses. Addons Supporting components such as ExternalDNS, kube-prometheus-stack, Loki, Alloy and Better Stack heartbeat. Apps Actual applications and test services. This keeps the dependencies clear. Controllers are installed first, then their configuration, then supporting addons and finally the applications.\nTemplate repository # The actual cluster repository describes my own running environment. Alongside it, I made a separate OKE GitOps template, which works as a cleaner starting point for building a similar cluster.\nThe template is not meant to be a black box. The structure should make it visible what OpenTofu builds, what Flux installs into the cluster and how the different layers fit together.\nScope # This is a personal lab, not a finished production platform. I keep the environment deliberately small so that I can understand it, maintain it and document it properly.\nThe repository is useful as a reference, but it is not meant to be copied blindly into production. It reflects my own learning environment, tradeoffs and constraints. Use the ideas, not the exact setup.\n","externalUrl":null,"permalink":"/posts/kubernetes-lab/","section":"Posts","summary":"An OKE-based Kubernetes and GitOps lab with Gateway API-based HTTPS routing, TLS, metrics, logs and public status monitoring.","title":"Kubernetes Lab","type":"posts"},{"content":"This blog could be published in a much simpler way. Hugo works well with GitHub Pages or Cloudflare Pages, and a static site does not need a Kubernetes cluster just to be online.\nIn this case, I wanted to run the blog the same way I run other small services in the cluster. The site is built into a container image, the image is published to GitHub Container Registry, and Flux installs the application into Kubernetes through a Helm chart.\nThe code lives in two repositories:\nhanhela.org oke-gitops-cluster The first repository contains the blog itself: Hugo content, theme configuration, Dockerfile, GitHub Actions workflow and the Helm chart. The second repository contains the cluster GitOps configuration, meaning how the application is actually run in my OKE cluster.\nI am not going through every YAML file in this post. The exact implementation is in the repositories. This is a short explanation of how the setup works and why the pieces are split this way.\nThe basic flow # The deployment path is straightforward:\nHugo source -\u0026gt; GitHub Actions -\u0026gt; GHCR -\u0026gt; Flux -\u0026gt; HelmRelease -\u0026gt; Kubernetes When the blog source changes, GitHub Actions builds a new image and publishes it to GitHub Container Registry. My cluster runs on Oracle Cloud Ampere A1 nodes, so the image is also built for linux/arm64.\nOn the cluster side, Flux watches the GitOps repository. When the HelmRelease or its values change, Flux reconciles the cluster toward that state. In practice, I do not install the blog manually from my laptop. The change goes through Git.\nFrom Hugo to a runnable image # Hugo builds the site into static files. At runtime, the blog does not need Hugo, Go tooling or the source tree. It only needs a web server that can serve the generated public/ directory.\nThat is why the image is built in two stages. Hugo builds the static site first. Then the finished site is copied into an nginx image. At runtime, the container is just nginx and static files.\nThis keeps the running image simple. From Kubernetes\u0026rsquo; point of view, the blog is a normal HTTP service: a pod listens on port 8080, a Service points traffic to the pod, and the Gateway handles public ingress.\nThe Helm chart lives with the application # The blog\u0026rsquo;s Helm chart is stored in the same repository as the application. It describes the basic shape of the app: Deployment, Service, probes, resource requests and a PodDisruptionBudget.\nCluster-specific values live in the GitOps repository. That is where I set things like the image repository, tag, replica count, resource limits and public hostname. The split is clean: the application repository describes what the application is, and the GitOps repository describes how this cluster runs it.\nThe chart can also create the HTTPRoute for the blog. In the GitOps repository I only need to give the HelmRelease the values that belong to this cluster: the Gateway, the hostname and the image settings.\nThe relevant HelmRelease values for a simple blog.example.com setup could look like this:\nvalues: fullnameOverride: blog image: repository: ghcr.io/example/blog tag: main pullPolicy: Always gateway: enabled: true routes: - name: public parentRefs: - name: public namespace: envoy-gateway-system sectionName: https hostnames: - blog.example.com The same mechanism can also handle an apex domain. In my setup, one route can point the apex domain to the blog Service, while another route redirects blog.example.com or www.example.com back to the apex domain.\nHelm is not strictly required for a blog like this. A simple Deployment and Service would work. I still use it because the same pattern is useful for other applications later. A small blog is a good place to keep the deployment model in shape without the application itself getting in the way.\nRouting and DNS # The blog container only serves HTTP inside the cluster. TLS, public hostnames and incoming traffic are handled through Gateway API.\nThe traffic path for a simple setup looks like this:\nblog.example.com -\u0026gt; Cloudflare DNS -\u0026gt; OCI Load Balancer -\u0026gt; Envoy Gateway -\u0026gt; Service -\u0026gt; Pod The Gateway itself belongs to the cluster. The blog chart only creates the application route when the GitOps repository enables it through Helm values. That keeps the blog layer small: namespace, Flux GitRepository, HelmRelease and Kustomization are enough.\nExternalDNS creates the required DNS record in Cloudflare based on Kubernetes resources. That is useful in everyday use because the hostname is not a manual setting hidden in the Cloudflare UI. It follows the same GitOps process as the rest of the routing.\nWhat I got out of it # A static site does not need a Kubernetes cluster, and this post is not trying to argue otherwise. For many Hugo blogs, GitHub Pages or Cloudflare Pages would be a simpler and lighter option.\nIn this project, the blog is a small public application that lets me test the whole deployment path. A change starts in Git, the image builds automatically, Flux applies it to the cluster, and traffic is routed out through the Gateway.\nThe useful part is the separation of responsibilities. The blog repository contains the application. The GitOps repository contains the cluster\u0026rsquo;s way of running it. The Helm chart gives the application a repeatable shape, including optional routing. Gateway and ExternalDNS handle publishing it to the outside world.\nOnce this works with a simple Hugo blog, the same model can be reused for other small services. The blog is a good target exactly because it is simple and easy to verify in a browser.\n","date":"6 June 2026","externalUrl":null,"permalink":"/posts/deploying-a-hugo-blog-to-kubernetes-with-helm-and-gitops/","section":"Posts","summary":"How a Hugo blog moves from source code to a container image, a Helm chart and finally a Flux-managed application in Kubernetes.","title":"Running a Hugo blog on Kubernetes with Helm and GitOps","type":"posts"},{"content":"I wanted to test what actually happens in my Kubernetes cluster when one worker node is replaced while the cluster is running. The final pod status alone does not tell what the maintenance looks like from the user\u0026rsquo;s point of view. That is why I tested it from the outside, against a public service.\nThe test was done in my Kubernetes Lab. Public traffic came through an OCI Network Load Balancer to Envoy Gateway, and from there to a small whoami service.\nThe cluster had three worker nodes, Longhorn storage and FluxCD reconciling the manifests. The whoami service itself does not use Longhorn, but Longhorn mattered for this test because replacing a node affects the whole cluster. If storage is left in a bad state, the node replacement has not really succeeded, even if the small test service happens to answer.\nThe goal was simple: replace one node and see whether public HTTP traffic gets interrupted.\nStarting point # Before the test I checked that the cluster was healthy:\nkubectl get nodes -o wide kubectl -n envoy-gateway-system get ds,pods -o wide kubectl -n whoami get ds,pods -o wide kubectl -n longhorn-system get volumes.longhorn.io flux get kustomizations -n flux-system Everything looked as expected. The nodes were Ready, Envoy Gateway was running as a DaemonSet, there were three whoami pods, and the Longhorn volumes were healthy.\nAgainst the whoami service I ran a small shell curl loop:\nwhile true; do ts=\u0026#34;$(date --iso-8601=seconds)\u0026#34; tmp=\u0026#34;$(mktemp)\u0026#34; metrics=\u0026#34;$(curl -sS -m 3 -o \u0026#34;$tmp\u0026#34; \\ -w \u0026#39;code=%{http_code} time=%{time_total}\u0026#39; \\ https://whoami.hanhela.org/ 2\u0026gt;\u0026amp;1)\u0026#34; rc=$? if [ \u0026#34;$rc\u0026#34; -eq 0 ] \u0026amp;\u0026amp; printf \u0026#39;%s\u0026#39; \u0026#34;$metrics\u0026#34; | grep -q \u0026#39;code=200\u0026#39;; then host=\u0026#34;$(sed -n \u0026#39;s/^Hostname: //p\u0026#39; \u0026#34;$tmp\u0026#34; | head -1)\u0026#34; echo \u0026#34;$ts OK $metrics host=$host\u0026#34; else echo \u0026#34;$ts FAIL rc=$rc $metrics\u0026#34; fi rm -f \u0026#34;$tmp\u0026#34; sleep 1 done | tee /tmp/whoami-ha-test.log This should show once per second whether the request returned HTTP 200 or timed out, and which whoami pod responded.\nReplacing the node # The node replacement was done with a small script that replaces one OKE node pool node at a time. Before doing anything, the script runs a dry-run, checks the target version and verifies the Longhorn state. The code is on GitHub.\nThe actual run looked like this:\n./scripts/replace-outdated-nodes.sh --force --max-replacements 1 \\ | tee /tmp/oke-node-replacement.log From the script\u0026rsquo;s point of view the test eventually went well. The old node was replaced, the new node joined the cluster, and Longhorn returned to a healthy state. Stale replicas left behind by the removed node were cleaned up.\nThe end of the log showed what I was looking for:\nDeleting stale Longhorn replica ... on removed node ... Removing stale Longhorn node ... after Kubernetes node removal Longhorn is healthy Replacement completed for ocid1.instance... Done. Replaced nodes in this run: 1 So from the cluster\u0026rsquo;s internal point of view, the node replacement succeeded.\nWas it uninterrupted? # Not completely.\nDuring the node replacement the test saw a few timeouts:\nFAIL rc=28 curl: (28) Operation timed out after 3002 milliseconds with 0 bytes received That was the most important result of the test. From Kubernetes\u0026rsquo; point of view everything recovered cleanly, but from the user\u0026rsquo;s point of view some requests failed during the maintenance window.\nI cannot prove the exact cause from this test alone. My best guess is that the short interruption was caused by timing between several moving parts: the old node leaving, the new node joining, the OCI Network Load Balancer updating its backends, and Envoy Gateway pods starting on the new node. Early in the new node\u0026rsquo;s lifecycle I also saw a Flannel-related error:\nplugin type=\u0026#34;flannel\u0026#34; failed (add): loadFlannelSubnetEnv failed: open /run/flannel/subnet.env: no such file or directory That fixed itself, but it is a useful reminder that a node being Ready does not always mean it is immediately ready to receive public traffic.\nWhat did I learn? # The test did not prove perfect zero-downtime HA. It was still a useful test.\nThe node was replaced successfully, the new node joined the cluster and Longhorn returned to a healthy state. After the maintenance the internal cluster state looked good. From the public traffic point of view the result was not perfect, because a few requests timed out.\nThat is the main lesson. High availability should not be judged only from inside the cluster. kubectl get pods can look good in the end, while a user may still have seen an error during maintenance. A simple curl loop from the outside told me more here than Kubernetes status alone.\nFor me, the test was successful even though the result was not perfect. I now have a better idea of how this cluster behaves during node maintenance, and where its limits start to show.\n","date":"5 June 2026","externalUrl":null,"permalink":"/posts/testing-high-availability-in-kubernetes/","section":"Posts","summary":"A realistic HA test in Oracle Kubernetes Engine: the node replacement worked, but fully uninterrupted public traffic was harder than it first looked.","title":"Testing high availability in Kubernetes","type":"posts"},{"content":"I am soon graduating with a business information technology degree, and I am especially interested in Linux, Kubernetes, DevOps, GitOps and cloud native environments.\nMy earlier background is in production and laboratory environments, where accuracy, systematic work, quality control and following processes were essential parts of the job. I want to combine that practical experience with my new IT skills.\nThis site is a personal technical blog. The main practical example is the OKE-based Kubernetes and GitOps environment described in the Kubernetes Lab post. More detailed documentation and manifests belong in GitHub repositories.\n","externalUrl":null,"permalink":"/about/","section":"Antti Hanhela","summary":"I am soon graduating with a business information technology degree, and I am especially interested in Linux, Kubernetes, DevOps, GitOps and cloud native environments.\nMy earlier background is in production and laboratory environments, where accuracy, systematic work, quality control and following processes were essential parts of the job. I want to combine that practical experience with my new IT skills.\n","title":"About","type":"page"},{"content":" Profile # I am a career changer soon graduating with a degree in business information technology. My current professional direction is focused on Linux, Kubernetes environments, GitOps, DevOps practices and cloud native infrastructure.\nBefore moving into IT, I worked for more than ten years in production and laboratory environments. That background has developed my systematic way of working, attention to detail, process discipline, quality control mindset and practical troubleshooting skills. I want to combine these strengths with technical skills in cloud infrastructure, DevOps and platform environments.\nRecent work experience # Time Employer Role Summary 2026 Kabei Oy Systems Specialist, internship Cloud infrastructure, Linux servers, container services, VPN connectivity and technical documentation. 2019– Ultraprint Oy Operator Printed electronics production, process monitoring and quality assurance. 2015–2018 Endomines Oy Laboratory Technician Geological sample preparation, analysis, quality control and documentation. 2014 Outokumpu Stainless Oy Laboratory Technician Metallurgical analyses, sample handling and laboratory work in an industrial environment. Education # Time Institution Degree Key studies 2023– Karelia University of Applied Sciences Bachelor of Business Administration, Business Information Technology Software development, IT infrastructure, information security, automation and systems integration. 2013–2014 Vocational College Lappia Vocational Qualification in Laboratory Technology Laboratory work, chemical analytics, instrumental analysis, bioanalytics, quality control and industrial processes. 2008 Merikoski Upper Secondary School Matriculation Examination General upper secondary education. Technical direction # My goal is to move into IT roles where I can combine my technical skills with my previous experience from production and laboratory environments. I am especially interested in industrial DevOps, cloud native environments, Linux, Kubernetes, GitOps, automation and practical systems operations.\nDownload the PDF version of my CV\n","externalUrl":null,"permalink":"/cv/","section":"Antti Hanhela","summary":"Profile # I am a career changer soon graduating with a degree in business information technology. My current professional direction is focused on Linux, Kubernetes environments, GitOps, DevOps practices and cloud native infrastructure.\n","title":"Curriculum Vitae","type":"page"}]