<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://devopsdirective.com">
    <title>DevOps Directive</title>
    <link href="https://devopsdirective.com/feed.xml" rel="self" />
    <link href="https://devopsdirective.com" />
    <updated>2026-07-28T16:52:33Z</updated>
    <id>https://devopsdirective.com</id>
        <entry>
            <title>
                🤖 Deputies
                
            </title>
            <updated>2026-07-28T16:51:47.677Z</updated>
            <id>https://devopsdirective.com/deputies/</id>
            <content type="html">
                &lt;h1 id=&quot;deputies&quot; tabindex=&quot;-1&quot;&gt;Deputies&lt;/h1&gt;
&lt;h2 id=&quot;self-hosted-background-agents-for-engineering-teams&quot; tabindex=&quot;-1&quot;&gt;Self-Hosted Background Agents for Engineering Teams&lt;/h2&gt;
&lt;h3 id=&quot;the-problem&quot; tabindex=&quot;-1&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Coding agents are powerful when a developer is actively supervising them from a terminal. But using agents across an engineering organization requires much more than an LLM and a command-line tool.&lt;/p&gt;
&lt;p&gt;Teams need a way to assign work, run agents in isolated environments, preserve progress, share results, manage access, integrate with existing workflows, and review exactly what happened. Building that control plane requires queues, sandboxes, authentication, event streaming, artifact storage, integrations, and reliable recovery.&lt;/p&gt;
&lt;p&gt;Without that infrastructure, agent usage remains tied to individual laptops, difficult to audit, and disconnected from the systems where engineering work begins.&lt;/p&gt;
&lt;h3 id=&quot;what-is-deputies&quot; tabindex=&quot;-1&quot;&gt;What is Deputies?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Deputies&lt;/strong&gt; is an open-source control plane for delegating engineering work to background agents.&lt;/p&gt;
&lt;p&gt;Give it a task through the web UI, Slack, GitHub, or pretty much any system that can send a webhook. Deputies prepares an isolated sandbox with the appropriate repositories and tools, runs the agent asynchronously, and preserves its progress, diagnostics, artifacts, and final response in a durable session.&lt;/p&gt;
&lt;p&gt;It gives engineering teams the shared infrastructure needed to move coding agents out of individual terminals and into repeatable, observable workflows.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot; tabindex=&quot;-1&quot;&gt;How It Works&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Assign work.&lt;/strong&gt; Start a session from the web UI or trigger one from Slack, GitHub, a webhook, or a schedule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run in isolation.&lt;/strong&gt; Deputies prepares a sandbox with the selected repositories, environment, tools, and context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follow progress.&lt;/strong&gt; Watch messages, tool calls, diagnostics, generated artifacts, and application previews as the agent works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review and iterate.&lt;/strong&gt; Inspect the result, queue follow-up instructions, download artifacts, or have the agent prepare a pull request.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-s-included&quot; tabindex=&quot;-1&quot;&gt;What&#39;s Included&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Durable Agent Sessions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Persistent sessions with ordered messages, searchable history, replayable events, artifacts, and follow-ups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Web UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Assign work, follow live progress, inspect diagnostics, preview applications, and review results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Isolated Sandboxes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Run agents with Daytona, Docker, Tensorlake, Kubernetes Agent Sandbox, or AWS Lambda MicroVM environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository Environments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reusable, revisioned work contexts that can include multiple repositories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trigger work from GitHub, provide scoped repository access, and let agents prepare branches and pull requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slack Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Delegate work from Slack threads and deliver completion responses back to the originating conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Webhooks and Automations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create agent work from external events or scheduled workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Artifacts and Previews&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Preserve generated files, reports, logs, and links to applications running inside agent sandboxes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queued Follow-Ups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add instructions while work is running or return later and resume the same durable session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills and Prompt Snippets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Package repeatable team procedures and quickly reuse common instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team Access Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tenant-wide viewer, member, and admin roles, with support for private sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multiple Model Providers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use standard LLM API keys or supported subscriptions through the Pi agent runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Portable Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deploy with Railway, Docker Compose, Kubernetes, or the AWS reference architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenTelemetry support for control-plane traces and operational metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;deployment-options&quot; tabindex=&quot;-1&quot;&gt;Deployment Options&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Sandbox Provider&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Railway Template&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Daytona or another hosted provider&lt;/td&gt;
&lt;td&gt;Getting a hosted instance running with minimal infrastructure work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Small teams, a dedicated server, or production-style local deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kubernetes Helm Chart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes Agent Sandbox&lt;/td&gt;
&lt;td&gt;Organizations that want agent execution inside an existing Kubernetes environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Reference Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lambda MicroVM sandboxes&lt;/td&gt;
&lt;td&gt;Teams standardizing on ECS, RDS, S3, and AWS-native isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom Containers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any supported provider&lt;/td&gt;
&lt;td&gt;Organizations with specific networking, security, or infrastructure requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;key-design-decisions&quot; tabindex=&quot;-1&quot;&gt;Key Design Decisions&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deployability is a core feature.&lt;/strong&gt; Deputies uses portable infrastructure primitives—Node.js, Postgres, HTTP, containers, and optional S3-compatible storage—rather than depending on a single cloud platform.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Agent work is durable.&lt;/strong&gt; Sessions, messages, events, artifacts, sandbox state, and callback deliveries survive beyond an individual browser tab or developer laptop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sandboxes are provider-neutral.&lt;/strong&gt; The control plane owns the workflow while execution environments plug in through a stable provider interface. Teams can choose managed sandboxes, Docker, Kubernetes, or AWS infrastructure based on their requirements.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integrations create work; they do not own it.&lt;/strong&gt; Slack, GitHub, webhooks, and schedules all feed the same durable session model. Work can be reviewed and continued in Deputies regardless of where it started.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You control the trust boundary.&lt;/strong&gt; Deputies can run alongside private repositories, internal tools, CI systems, and observability platforms. Operators decide where the control plane, sandboxes, credentials, and model access live.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start simple and scale when needed.&lt;/strong&gt; Run the API and worker together for a small deployment, then split and scale web, API, worker, storage, and sandbox orchestration independently as usage grows.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The agent runtime is replaceable infrastructure.&lt;/strong&gt; Deputies currently uses &lt;a href=&quot;https://pi.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Pi&lt;/a&gt; for real agent work while keeping the durable control-plane concerns (sessions, queues, integrations, artifacts, and sandbox lifecycle) separate from the runner.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;example-workflows&quot; tabindex=&quot;-1&quot;&gt;Example Workflows&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Assign an issue from GitHub and have an agent investigate it, make a change, run tests, and prepare a pull request.&lt;/li&gt;
&lt;li&gt;Ask for a CI failure investigation from Slack and receive the result back in the same thread.&lt;/li&gt;
&lt;li&gt;Schedule recurring dependency, security, or maintenance checks.&lt;/li&gt;
&lt;li&gt;Launch an agent against multiple related repositories with a preconfigured development environment.&lt;/li&gt;
&lt;li&gt;Generate reports, migration plans, screenshots, or other artifacts and preserve them with the session.&lt;/li&gt;
&lt;li&gt;Run and review an application inside the sandbox before accepting the agent&#39;s changes.&lt;/li&gt;
&lt;li&gt;Give agents controlled access to internal tools and data through remote MCP servers such as Executor.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;who-is-this-for&quot; tabindex=&quot;-1&quot;&gt;Who Is This For?&lt;/h2&gt;
&lt;p&gt;Engineering and platform teams who:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Want to delegate real engineering work without tying execution to individual developer machines&lt;/li&gt;
&lt;li&gt;Need shared visibility into what agents are doing and how they reached their conclusions&lt;/li&gt;
&lt;li&gt;Work with private repositories, internal services, or custom development environments&lt;/li&gt;
&lt;li&gt;Want agents to participate in GitHub, Slack, CI, incident response, and scheduled workflows&lt;/li&gt;
&lt;li&gt;Need control over where agent infrastructure and persisted data are hosted&lt;/li&gt;
&lt;li&gt;Would rather customize an open-source control plane than build agent orchestration from scratch&lt;/li&gt;
&lt;li&gt;Want to start with a small deployment and retain a path to production-scale infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br /&gt;
Deputies is still early-stage and evolving quickly. Expect new capabilities and occasional breaking changes as the project develops.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;links&quot; tabindex=&quot;-1&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href=&quot;https://deputies.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;deputies.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href=&quot;https://demo.deputies.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;demo.deputies.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/sidpalas/deputies&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;github.com/sidpalas/deputies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment Guide:&lt;/strong&gt; &lt;a href=&quot;https://github.com/sidpalas/deputies/blob/main/docs/deployment.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Deputies deployment documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy on Railway:&lt;/strong&gt; &lt;a href=&quot;https://railway.com/deploy/deputies-monolith&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;railway.com/deploy/deputies-monolith&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://devopsdirective.com/deputies/" />
        </entry>
        <entry>
            <title>
                🚀 Kube Starter Kit
                
            </title>
            <updated>2026-07-28T16:48:56.979Z</updated>
            <id>https://devopsdirective.com/kube-starter-kit/</id>
            <content type="html">
                &lt;h1 id=&quot;kube-starter-kit&quot; tabindex=&quot;-1&quot;&gt;Kube Starter Kit&lt;/h1&gt;
&lt;h2 id=&quot;a-production-ready-kubernetes-platform-starter&quot; tabindex=&quot;-1&quot;&gt;A Production-Ready Kubernetes Platform Starter&lt;/h2&gt;
&lt;h3 id=&quot;the-problem&quot; tabindex=&quot;-1&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Every engineering team that adopts Kubernetes faces the same challenge: spinning up a cluster is easy, but building a production-ready platform is not. You need infrastructure-as-code, GitOps pipelines, secrets management, observability, compliance controls, and CI/CD automation, all wired together correctly across multiple environments.&lt;/p&gt;
&lt;p&gt;This takes months to build properly, and most teams end up with inconsistent, hard-to-maintain infrastructure because they&#39;re racing to ship features.&lt;/p&gt;
&lt;h3 id=&quot;what-is-kube-starter-kit&quot; tabindex=&quot;-1&quot;&gt;What is Kube Starter Kit?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kube Starter Kit&lt;/strong&gt; is an opinionated, production-ready Kubernetes platform for &lt;strong&gt;AWS and EKS&lt;/strong&gt;. It provides everything needed to run applications on Kubernetes: infrastructure, deployment pipelines, and baseline components, ready to deploy and customize.&lt;/p&gt;
&lt;p&gt;It&#39;s the platform you&#39;d build yourself if you had the time (and had done it a few times before).&lt;/p&gt;
&lt;h3 id=&quot;what-s-included&quot; tabindex=&quot;-1&quot;&gt;What&#39;s Included&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terraform Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Modular Terraform for AWS with multi-environment support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terramate Orchestration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stack-based orchestration with change detection and CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-account setup with VPC, EKS, and secure boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub + AWS IAM Identity Center integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitOps (ArgoCD)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Declarative, auditable deployments via app-of-apps pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;K8s Baseline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ingress, cert-manager, external-dns, external-secrets, observability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI/CD Pipelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated container builds and staging deployments on merge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image CVE Scanning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated vulnerability scanning with daily scheduled scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Release Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated release PRs with release-please&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Demo Applications&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully functional Go services demonstrating end-to-end patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;KinD + Tilt + mirrord for fast local iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;repository-structure&quot; tabindex=&quot;-1&quot;&gt;Repository Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kube-starter-kit/
├── .github/workflows/    # CI/CD pipelines (build, deploy, drift detection)
├── kubernetes/
│   ├── src/              # Source manifests (Kustomize, Helm, Timoni)
│   └── rendered/         # Generated manifests by environment
├── terraform/
│   ├── modules/          # Reusable modules (eks, networking, etc.)
│   └── live/             # Environment configs (staging, production)
├── services/             # Application source code
└── local/                # KinD cluster config + Tiltfile
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;key-design-decisions&quot; tabindex=&quot;-1&quot;&gt;Key Design Decisions&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;You own everything.&lt;/strong&gt; No managed service dependency. Infrastructure runs in your AWS account with full control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard tools, no abstraction layer.&lt;/strong&gt; Unlike OpenShift or Tanzu, there&#39;s no proprietary UI. It&#39;s standard Kubernetes with standard open-source tools (ArgoCD, cert-manager, external-secrets, etc.).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitOps from day one.&lt;/strong&gt; All changes flow through Git. ArgoCD handles deployments. Infrastructure changes go through Terramate-orchestrated Terraform with PR previews.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-environment by default.&lt;/strong&gt; Staging and production environments with proper separation, automated staging deploys, and controlled production releases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compliance-ready.&lt;/strong&gt; Features map directly to SOC2 controls; audit trails, access management, vulnerability scanning, and secrets management are built in.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;who-is-this-for&quot; tabindex=&quot;-1&quot;&gt;Who Is This For?&lt;/h2&gt;
&lt;p&gt;Engineering teams at early-stage companies who:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Know Kubernetes is the right choice for their infrastructure&lt;/li&gt;
&lt;li&gt;Want to reach production quickly without cutting corners&lt;/li&gt;
&lt;li&gt;Would rather customize a working system than build from scratch&lt;/li&gt;
&lt;li&gt;Need to meet compliance requirements (SOC2, etc.) without a dedicated platform team&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;links&quot; tabindex=&quot;-1&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href=&quot;https://kubestarterkit.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;kubestarterkit.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/DevOps-Directive/kube-starter-kit&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/DevOps-Directive/kube-starter-kit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://devopsdirective.com/kube-starter-kit/" />
        </entry>
        <entry>
            <title>
                📚 Courses
                
            </title>
            <updated>2026-07-28T16:45:59.314Z</updated>
            <id>https://devopsdirective.com/courses/</id>
            <content type="html">
                &lt;h1 id=&quot;courses&quot; tabindex=&quot;-1&quot;&gt;Courses&lt;/h1&gt;
&lt;p&gt;I create top tier DevOps tutorials and release them on &lt;a href=&quot;https://www.youtube.com/channel/UC4MdpjzjPuop_qWNAvR23JA&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;YouTube&lt;/a&gt;, &lt;a href=&quot;https://github.com/sidpalas/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub&lt;/a&gt;, and at &lt;a href=&quot;https://courses.devopsdirective.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://courses.devopsdirective.com/&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://courses.devopsdirective.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/static/images/courses-site-screenshot.png&quot; alt=&quot;courses-site-screenshot.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;take-your-dev-ops-skills-to-the-next-level&quot; tabindex=&quot;-1&quot;&gt;Take your DevOps skills to the next level!&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://courses.devopsdirective.com/terraform-beginner-to-pro/lessons/00-introduction/01-main&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🏗️ Terraform: Beginner to Pro (1M+ views)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://courses.devopsdirective.com/docker-beginner-to-pro/lessons/00-introduction/01-main&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🐳 Docker: Beginner to Pro (800k+ views)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://courses.devopsdirective.com/kubernetes-beginner-to-pro/lessons/00-introduction/01-main&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;☸️ Kubernetes: Beginner to Pro (500k+ views)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://courses.devopsdirective.com/github-actions-beginner-to-pro&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;⚙️ GitHub Actions: Beginner to Pro (250k+ views)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://devopsdirective.com/courses/" />
        </entry>
        <entry>
            <title>
                💼 Consulting
                
            </title>
            <updated>2026-01-05T17:20:35.432Z</updated>
            <id>https://devopsdirective.com/consulting/</id>
            <content type="html">
                &lt;h1 id=&quot;consulting&quot; tabindex=&quot;-1&quot;&gt;Consulting&lt;/h1&gt;
&lt;p&gt;I help companies build application platforms that enable them to ship quickly and safely.&lt;/p&gt;
&lt;p&gt;Much of my recent work has been with Kubernetes + AWS, but I also have experience with GCP, and deploying to airgapped on-prem networks.&lt;/p&gt;
&lt;p&gt;Interested in taking your DevOps to the next level? Reach out to &lt;a href=&quot;mailto:sid@devopsdirective.com&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;sid@devopsdirective.com&lt;/a&gt;!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;some-previous-engagements&quot; tabindex=&quot;-1&quot;&gt;Some Previous Engagements:&lt;/h2&gt;
&lt;h4 id=&quot;nominal&quot; tabindex=&quot;-1&quot;&gt;Nominal&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://nominal.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://nominal.io/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sid created the foundation for our infrastructure, setting up continuous deployment using best practices and state of the art open source tools, working with us 2023-2025 as we grew from 6 to 50 engineers. Even in the very early days of our engineering organization, developers had the leverage of master-crafted foundational tools and automation, something that we’ve seen larger and mature organizations take teams of people to do. We gave him the nickname of “Gandalf” because he so frequently made magic happen. Although he might be too humble to admit it, I suspect some of his work would still be crucial even if we hired 10x as many engineers.&lt;/p&gt;
&lt;p&gt;Despite only working part time, Sid was a clear owner when it came to devops and infrastructure; he was always incredibly responsive to any questions from developers and very often jumped in to help with incident response, even if unrelated to his domain.&lt;/p&gt;
&lt;p&gt;Sid is also an excellent teacher. He is passionate about spreading his knowledge in a scalable and repeatable way. He left a wake of fantastic documentation for everything he did.&lt;/p&gt;
&lt;p&gt;Sid very often sought and incorporated feedback from developers so that he was always pointed on the most important infrastructure and devops issues. He was also an essential thought partner for developers for when we had new expansion opportunities such as deploying on premises or in customer clouds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/static/images/jason-headshot.png&quot; alt=&quot;jason-headshot.png&quot; /&gt;&lt;br /&gt;
&lt;strong&gt;Jason Hoch,&lt;/strong&gt; &lt;em&gt;Co-founder&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id=&quot;gauntlet-networks&quot; tabindex=&quot;-1&quot;&gt;Gauntlet Networks&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://gauntlet.xyz/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://gauntlet.xyz/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sid started working with Gauntlet in April 2020 when the company was less than 10 people. We had a basic Kubernetes cluster set up to handle some manually triggered simulation jobs, and brought Sid on board to help take our DevOps and infrastructure to the next level. Over the following two years, Sid led a number of significant projects including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rolling out and upgrading Kubeflow for better management and automation of ETL, simulation, and optimization workflows on Kubernetes&lt;/li&gt;
&lt;li&gt;Leveraging cluster autoscaling and pre-emptible instances to efficiently serve bursty workloads at scale&lt;/li&gt;
&lt;li&gt;Designing and implementing a CI/CD workflow that fit the needs of our organization, including setting up IaaS via Terraform, cluster deployment and maintenance across multiple development environments, as well as observability and monitoring solutions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sid was able to independently own most of these projects over their entire lifecycle as a part-time contractor, while still collaborating effectively with others on the team to navigate complex and changing business requirements. Sid is always meticulous about code quality, documentation, and knowledge sharing, which resulted in a smooth transition at the end of the project. Much of Gauntlet’s infrastructure continues to rely on Sid’s work, which now supports a team of 50, running hundreds of daily jobs with tricky compute requirements and dependency management. I would recommend working with Sid in a heartbeat for any similar projects.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/static/images/rei-headshot.png&quot; alt=&quot;rei-headshot.png&quot; /&gt;&lt;br /&gt;
&lt;strong&gt;Rei Chiang,&lt;/strong&gt; &lt;em&gt;Chief Technology Officer&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id=&quot;ursa-space-systems&quot; tabindex=&quot;-1&quot;&gt;Ursa Space Systems&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://ursaspace.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://ursaspace.com/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sid worked with Ursa Space Systems from November 2021 through December 2022. We hired Sid to support our Systems Team as a DevOps Consultant while we were expanding our DevOps team. His impact on our operations cannot be overstated! Sid delivered great value, even as a part-time employee.&lt;/p&gt;
&lt;p&gt;His expertise with Kubernetes, AWS, and Gitlab CI is top-notch. He collaborated effectively with our development team and earned our highest trust as an external contractor. For all of these reasons, I would highly recommend Sid to any company looking for DevOps expertise.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/static/images/julie-headshot.png&quot; alt=&quot;julie-headshot.png&quot; /&gt;&lt;br /&gt;
&lt;strong&gt;Julie Baker,&lt;/strong&gt; &lt;em&gt;Chief Operating Officer&lt;/em&gt;&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/consulting/" />
        </entry>
        <entry>
            <title>
                👋 About
                
            </title>
            <updated>2026-01-05T17:20:39.242Z</updated>
            <id>https://devopsdirective.com/about/</id>
            <content type="html">
                &lt;h1 id=&quot;about&quot; tabindex=&quot;-1&quot;&gt;About&lt;/h1&gt;
&lt;p&gt;Hi, I&#39;m Sid.&lt;/p&gt;
&lt;p&gt;I am a technologist, engineer, athlete, beard grower, and tiny living enthusiast.&lt;/p&gt;
&lt;p&gt;My goal with DevOps Directive is to provide educational and entertaining (hopefully?...) content to enable engineers to level up their skills and keep up with the continuously evolving DevOps and Cloud Infrastructure landscape.&lt;/p&gt;
&lt;p&gt;I will be exploring many different types of media, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/channel/UC4MdpjzjPuop_qWNAvR23JA&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Video Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sidpalas&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Open Source Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=vTNv2RKa5LU&amp;amp;list=PLFzuOAehUPHFMFSe7OeKLecTcbjydSUHa&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Interviews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=DftsReyhz2Q&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Live &amp;amp; Recorded Screen Casts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qGRRndZCKpE&amp;amp;list=PLFzuOAehUPHHymg68r0DNdc05rJqbD4Yg&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Internet Office Hours&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=oHg5SJYRHA0&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Interpretive Song and Dance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Topics include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Infrastructure as Code&lt;/li&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;Networking&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Testing&lt;/li&gt;
&lt;li&gt;Continuous Integration&lt;/li&gt;
&lt;li&gt;Continuous Deployment&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Cloud Providers&lt;/li&gt;
&lt;li&gt;Career Advancement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I am not an expert in all of these topics. That being said, I am good at &lt;a href=&quot;https://en.wikipedia.org/wiki/RTFM&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Reading the Docs&lt;/a&gt;, asking the right questions, and explaining what I learn along the way.&lt;/p&gt;
&lt;p&gt;Join me!&lt;/p&gt;
&lt;p&gt;-Sid&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/static/images/tedx-talk.png&quot; alt=&quot;tedx-talk.png&quot; /&gt;&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/about/" />
        </entry>
        <entry>
            <title>
                forking-the-planetscale-terraform-provider
                
            </title>
            <updated>2026-03-10T19:21:50.370Z</updated>
            <id>https://devopsdirective.com/posts/2026/03/forking-the-planetscale-terraform-provider/</id>
            <content type="html">
                &lt;h1 id=&quot;i-forked-the-planet-scale-terraform-provider&quot; tabindex=&quot;-1&quot;&gt;I forked the PlanetScale Terraform Provider&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt;&lt;/em&gt; I am bullish on PlanetScale. I also have strong opinions about Infrastructure as Code.&lt;/p&gt;
&lt;p&gt;Last week, I was excited to see the &lt;a href=&quot;https://planetscale.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;PlanetScale&lt;/a&gt; Terraform provider &lt;code&gt;v1&lt;/code&gt; (&lt;a href=&quot;https://registry.terraform.io/providers/planetscale/planetscale/1.0.0&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;LINK&lt;/a&gt;) was released and now includes support for their Postgres product! 🎉&lt;br /&gt;
&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2026/03/forking-the-planetscale-terraform-provider/provider-release-announcement.png&quot; alt=&quot;provider-release-announcement.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, it only includes resources for managing branches (not the databases themselves). See: &lt;a href=&quot;https://planetscale.com/docs/terraform#conceptual-model&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://planetscale.com/docs/terraform#conceptual-model&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING] PlanetScale databases themselves (the logical database containers) are &lt;strong&gt;not&lt;/strong&gt; managed directly as Terraform resources in v1.0. Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create databases in PlanetScale first (via the PlanetScale UI or API).&lt;/li&gt;
&lt;li&gt;Use Terraform to manage &lt;strong&gt;branches&lt;/strong&gt; and related resources (roles, passwords, etc.) within those databases.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my opinion, this makes the provider totally unusable. Having to diverge from Infrastructure as Code to click-ops the database and then import the default branch breaks any fully automated workflow and results in a less than ideal solution.&lt;/p&gt;
&lt;p&gt;After chatting with some folks from PlanetScale (shoutout to @josh and @Josh 😅) they helped me understand the reasoning behind this design choice:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2026/03/forking-the-planetscale-terraform-provider/feedback-from-josh-narrow.png&quot; alt=&quot;feedback-from-josh-narrow.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TL;DR:&lt;/code&gt; The PlanetScale data model and lifecycle for databases and branches doesn&#39;t map cleanly onto the Terraform resource model and they didn&#39;t want to lock in an approach until they were confident it was the right one.&lt;/p&gt;
&lt;h2 id=&quot;challenges&quot; tabindex=&quot;-1&quot;&gt;Challenges&lt;/h2&gt;
&lt;p&gt;There are three main challenges with how PlanetScale databases/branches behave when designing the Terraform provider:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A default branch is auto-created when a database is created&lt;/li&gt;
&lt;li&gt;A database cannot exist without at least one branch&lt;/li&gt;
&lt;li&gt;The default branch can be modified after creation&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!INFO] Note:&lt;/p&gt;
&lt;p&gt;For the sake of this article I will use &lt;code&gt;postgres&lt;/code&gt; for all examples. Analogous resources would exist for PlanetScale &lt;code&gt;vitess&lt;/code&gt; databases/branches.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Together, these raise questions about how to design the Terraform provider:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who owns the default branch in Terraform?&lt;/li&gt;
&lt;li&gt;How is the auto-created branch adopted without import?&lt;/li&gt;
&lt;li&gt;How is a change in default branch represented?&lt;/li&gt;
&lt;li&gt;What should happen during terraform destroy?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A naive implementation would either lead to resource contention between the database and branch resources or require a two-step create-then-import process for the default branch.&lt;/p&gt;
&lt;h2 id=&quot;prior-art&quot; tabindex=&quot;-1&quot;&gt;Prior Art&lt;/h2&gt;
&lt;p&gt;To understand how to best approach this, I looked at existing Terraform providers that face similar challenges.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;AWS&lt;/a&gt;, &lt;a href=&quot;https://registry.terraform.io/providers/integrations/github/latest&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub&lt;/a&gt; , and &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/kubernetes/latest&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kubernetes&lt;/a&gt; providers contain a handful of resources which must account for pre-existing and default lifecycle behaviors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/default_vpc&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;aws_default_vpc&lt;/a&gt;  and &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/default_subnet&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;aws_default_subnet&lt;/a&gt;: These resources have different behaviors depending if the defaults they represent exist within AWS. If they do not exist, Terraform creates them, but if they do exist, Terraform adopts them.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_default&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;github_branch_default&lt;/a&gt;: This resource models the default branch as its own terraform resource which acts as a pointer to the repo/branch.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/default_service_account_v1&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;kubernetes_default_service_account_v1&lt;/a&gt;: The default service account for each namespace is always created by kubernetes, so this resource is always used to adopt/manage that service account (e.g. to add labels/annotations)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;user-stories&quot; tabindex=&quot;-1&quot;&gt;User Stories&lt;/h1&gt;
&lt;p&gt;To capture my goals I came up with the following user stories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As a Terraform user, I want to manage PlanetScale database resources directly, not just branches, so that I can model the full database lifecycle in Terraform.&lt;/li&gt;
&lt;li&gt;As a Terraform user, I want to manage a database’s default branch properties without a separate two-step import workflow, so that I can bring new and existing databases under management more smoothly.&lt;/li&gt;
&lt;li&gt;As a Terraform user, I want to change a database’s default branch with clear, predictable lifecycle behavior, so that repointing the default branch works cleanly during apply and does not create confusing destroy or drift semantics.&lt;/li&gt;
&lt;li&gt;As a Terraform user, I want terraform destroy to cleanly remove both database and branch resources, so that ephemeral environments can be created and torn down without manual cleanup.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;my-solution&quot; tabindex=&quot;-1&quot;&gt;My Solution&lt;/h2&gt;
&lt;p&gt;I decided to fork the provider repo and experiment with implementing a proposed solution. Here is what I ended up with: &lt;a href=&quot;https://github.com/sidpalas/terraform-provider-planetscale/pull/1&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;PULL REQUEST&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ADD&lt;/strong&gt; &lt;code&gt;planetscale_database_postgres&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADD&lt;/strong&gt; &lt;code&gt;planetscale_database_default_branch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MODIFY&lt;/strong&gt; &lt;code&gt;planetscale_postgres_branch&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;New field: &lt;code&gt;adopt_if_exists (default false)&lt;/code&gt; so Terraform can adopt a pre-existing branch instead of failing creation, which is especially useful for auto-created branches like main.&lt;/li&gt;
&lt;li&gt;New field: &lt;code&gt;on_default_delete&lt;/code&gt; to define behavior when a branch being destroyed is still the database’s current default branch:&lt;br /&gt;
- &lt;code&gt;error (default)&lt;/code&gt;: fail with an actionable message explaining that the default branch must be repointed first&lt;br /&gt;
- &lt;code&gt;remove_from_state&lt;/code&gt;: if the API refuses deletion because the branch is still the default, Terraform removes the branch from state and continues&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, these enable all of the desired user stories and I was able to use the modified provider to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Provision a new database, adopt the default branch, create a new branch, and modify the default branch to point to that new branch in a single apply command&lt;/li&gt;
&lt;li&gt;Cleanly tear down all of those resources with a single destroy command&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is my config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;terraform {
  required_providers {
    planetscale = {
      source  = &amp;quot;sidpalas/planetscale&amp;quot;
      version = &amp;quot;1.1.0-postgres-db&amp;quot;
    }
  }
}

provider &amp;quot;planetscale&amp;quot; {

}

resource &amp;quot;planetscale_database_postgres&amp;quot; &amp;quot;db&amp;quot; {
  organization = &amp;quot;sid-devopsdirective&amp;quot;
  name         = &amp;quot;with-branch&amp;quot;
  cluster_size = &amp;quot;PS_5_AWS_ARM&amp;quot;
}

resource &amp;quot;planetscale_postgres_branch&amp;quot; &amp;quot;main&amp;quot; {
  organization      = &amp;quot;sid-devopsdirective&amp;quot;
  database          = planetscale_database_postgres.db.id
  name              = &amp;quot;main&amp;quot;
  cluster_size      = &amp;quot;PS_5_AWS_ARM&amp;quot;
  adopt_if_exists   = true
}

resource &amp;quot;planetscale_postgres_branch&amp;quot; &amp;quot;someotherbranch&amp;quot; {
  organization      = &amp;quot;sid-devopsdirective&amp;quot;
  database          = planetscale_database_postgres.db.id
  name              = &amp;quot;someotherbranch&amp;quot;
  cluster_size      = &amp;quot;PS_5_AWS_ARM&amp;quot;
  on_default_delete = &amp;quot;remove_from_state&amp;quot;
}

resource &amp;quot;planetscale_database_default_branch&amp;quot; &amp;quot;this&amp;quot; {
  organization = &amp;quot;sid-devopsdirective&amp;quot;
  database     = planetscale_database_postgres.db.id
  branch       = planetscale_postgres_branch.someotherbranch.name
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;terraform-apply&quot; tabindex=&quot;-1&quot;&gt;terraform apply 🚀&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ terraform apply  

...

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only &#39;yes&#39; will be accepted to approve.

  Enter a value: yes

planetscale_database_postgres.db: Creating...
planetscale_database_postgres.db: Still creating... [00m10s elapsed]
...
planetscale_database_postgres.db: Still creating... [01m50s elapsed]
planetscale_database_postgres.db: Creation complete after 1m51s [id=p4wvn79boc9j]
planetscale_postgres_branch.someotherbranch: Creating...
planetscale_postgres_branch.main: Creating...
planetscale_postgres_branch.main: Creation complete after 3s [id=2bqz7zmvhm33]
planetscale_postgres_branch.someotherbranch: Still creating... [00m10s elapsed]
...
planetscale_postgres_branch.someotherbranch: Still creating... [02m10s elapsed]
planetscale_postgres_branch.someotherbranch: Creation complete after 2m20s [id=xgi6gwxpmafo]
planetscale_database_default_branch.this: Creating...
planetscale_database_default_branch.this: Creation complete after 0s [id=p4wvn79boc9j]
╷
│ Warning: existing branch adopted
│ 
│   with planetscale_postgres_branch.main,
│   on main.tf line 21, in resource &amp;quot;planetscale_postgres_branch&amp;quot; &amp;quot;main&amp;quot;:
│   21: resource &amp;quot;planetscale_postgres_branch&amp;quot; &amp;quot;main&amp;quot; {
│ 
│ The PostgreSQL branch already exists and was adopted because adopt_if_exists is true.
╵

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2026/03/forking-the-planetscale-terraform-provider/ps-dashboard.png&quot; alt=&quot;ps-dashboard.png&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;terraform-destroy&quot; tabindex=&quot;-1&quot;&gt;terraform destroy 🧨&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform destroy

...

Plan: 0 to add, 0 to change, 4 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only &#39;yes&#39; will be accepted to confirm.

  Enter a value: yes

planetscale_database_default_branch.this: Destroying... [id=p4wvn79boc9j]
planetscale_postgres_branch.main: Destroying... [id=2bqz7zmvhm33]
planetscale_database_default_branch.this: Destruction complete after 0s
planetscale_postgres_branch.someotherbranch: Destroying... [id=xgi6gwxpmafo]
planetscale_postgres_branch.someotherbranch: Destruction complete after 0s
planetscale_postgres_branch.main: Destruction complete after 1s
planetscale_database_postgres.db: Destroying... [id=p4wvn79boc9j]
planetscale_database_postgres.db: Destruction complete after 0s
╷
│ Warning: default branch not deleted remotely
│ 
│ The branch is currently the database default branch and cannot be deleted by PlanetScale. The resource was
│ removed from Terraform state because on_default_delete is set to remove_from_state.
╵

Destroy complete! Resources: 4 destroyed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2026/03/forking-the-planetscale-terraform-provider/ps-dashboard-destroy.png&quot; alt=&quot;ps-dashboard-destroy.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;limitations&quot; tabindex=&quot;-1&quot;&gt;Limitations&lt;/h2&gt;
&lt;p&gt;There are some important limitations in the current implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This provider is generated from the OpenAPI API specification using &lt;a href=&quot;https://www.speakeasy.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Speakeasy&lt;/a&gt;, but &lt;code&gt;adopt_if_exists&lt;/code&gt; plus &lt;code&gt;on_default_delete&lt;/code&gt; are currently implemented as manual edits in generated code in &lt;code&gt;internal/provider/postgresbranch_resource.go&lt;/code&gt;. As a result, regenerating the provider will overwrite those changes.&lt;/li&gt;
&lt;li&gt;The current solution only covers &lt;code&gt;PostgreSQL&lt;/code&gt; resources. Equivalent lifecycle support would need to be added for &lt;code&gt;Vitess&lt;/code&gt; databases and branches.&lt;/li&gt;
&lt;li&gt;This implementation was produced largely with OpenCode and GPT-assisted development. It should be treated as a prototype and should receive additional engineering review, testing, and validation before being considered production-ready.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;(Unrelated to the specific challenges addressed here)&lt;/strong&gt;&lt;/em&gt; Modifying the cluster configuration (e.g. # of replicas) for the database currently forces it to be recreated. This behavior is different from the PlanetScale UI where the cluster configuration can be modified in-place, but appears to be a limitation of the public API (based on the OpenAPI spec).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;input-requested&quot; tabindex=&quot;-1&quot;&gt;Input Requested! 🙏&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!INFO] NOTE&lt;/p&gt;
&lt;p&gt;The PlanetScale team wants to get to a clean long-term solution here, but wanted to avoid locking in a pattern they would eventually come to regret! My goal with the fork + article was to help make progress towards such a solution!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you have experience working with PlanetScale and using (or authoring) Terraform providers, I’d love your input on the lifecycle tradeoffs discussed here, especially around default branch management, adoption of pre-existing resources, and destroy behavior for ephemeral environments.&lt;/p&gt;
&lt;p&gt;Please share your feedback, edge cases, or preferred UX on the GitHub issue: &lt;a href=&quot;https://github.com/planetscale/terraform-provider-planetscale/issues/260&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/planetscale/terraform-provider-planetscale/issues/260&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP] Try it out!&lt;/p&gt;
&lt;p&gt;I published my version of the provider here if you want to take a look and experiment with its ergonomics! &lt;a href=&quot;https://registry.terraform.io/providers/sidpalas/planetscale/latest&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://registry.terraform.io/providers/sidpalas/planetscale/latest&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The source for my fork can be seen here: &lt;a href=&quot;https://github.com/sidpalas/terraform-provider-planetscale&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/terraform-provider-planetscale&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2026/03/forking-the-planetscale-terraform-provider/" />
        </entry>
        <entry>
            <title>
                rendered-manifests-pattern
                
            </title>
            <updated>2026-01-15T14:13:57.381Z</updated>
            <id>https://devopsdirective.com/posts/2026/01/rendered-manifests-pattern/</id>
            <content type="html">
                &lt;h1 id=&quot;the-rendered-manifests-pattern&quot; tabindex=&quot;-1&quot;&gt;The Rendered Manifests Pattern&lt;/h1&gt;
&lt;p&gt;As I built out &lt;a href=&quot;https://kubestarterkit.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kube Starter Kit&lt;/a&gt;, I considered a variety of different approaches for implementing GitOps.&lt;/p&gt;
&lt;p&gt;I considered the tradeoffs between &lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;ArgoCD&lt;/a&gt;, &lt;a href=&quot;https://fluxcd.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Flux&lt;/a&gt;, and &lt;a href=&quot;https://kluctl.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kluctl&lt;/a&gt;, eventually deciding on ArgoCD based on its large community and mature UI. During this process I came across this article from Akuity (the creators of ArgoCD) &lt;a href=&quot;https://akuity.io/blog/the-rendered-manifests-pattern&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&amp;quot;The Rendered Manifests Pattern&amp;quot;&lt;/a&gt; and ultimately decided to implement the pattern (with a few tweaks).&lt;/p&gt;
&lt;h2 id=&quot;what-is-the-rendered-manifests-pattern&quot; tabindex=&quot;-1&quot;&gt;What is the rendered manifests pattern?&lt;/h2&gt;
&lt;p&gt;The rendered manifests pattern avoids runtime abstractions or mutations between the source of truth (in Git) and the resources deployed to Kubernetes (etcd). Abstractions such as Helm or Kustomize are still used, but only during a rendering step that produces fully hydrated manifests, which are then stored in Git and applied to clusters.&lt;/p&gt;
&lt;h3 id=&quot;pros&quot; tabindex=&quot;-1&quot;&gt;Pros&lt;/h3&gt;
&lt;h4 id=&quot;full-visibility-and-reviewability&quot; tabindex=&quot;-1&quot;&gt;Full visibility and reviewability&lt;/h4&gt;
&lt;p&gt;All changes to resources are visible as plain YAML in Git and can be reviewed directly, without needing to reason about how a change to values or overlays might affect the final rendered output.&lt;/p&gt;
&lt;h4 id=&quot;tooling-agnostic&quot; tabindex=&quot;-1&quot;&gt;Tooling agnostic&lt;/h4&gt;
&lt;p&gt;Any tooling can be used to generate the final manifests, including newer or more expressive tools such as Timoni or CUE. By decoupling rendering from reconciliation, you are no longer constrained by the set of tools supported by your GitOps engine, and clusters no longer need access to those tools.&lt;/p&gt;
&lt;h4 id=&quot;simpler-break-glass-and-safer-recovery&quot; tabindex=&quot;-1&quot;&gt;Simpler break glass and safer recovery&lt;/h4&gt;
&lt;p&gt;In an emergency, it is possible to modify the deployed manifests directly to apply a hotfix. Because future changes must still go through the rendering process, folding those fixes back into the source configuration is a safe and explicit operation rather than an implicit state reconstruction.&lt;/p&gt;
&lt;h4 id=&quot;improved-git-ops-engine-performance&quot; tabindex=&quot;-1&quot;&gt;Improved GitOps engine performance&lt;/h4&gt;
&lt;p&gt;Moving rendering out of the GitOps reconciliation loop reduces the amount of work the GitOps engine needs to perform, improving performance and making it easier to scale across many applications or clusters.&lt;/p&gt;
&lt;h3 id=&quot;cons&quot; tabindex=&quot;-1&quot;&gt;Cons&lt;/h3&gt;
&lt;h4 id=&quot;requires-additional-ci-logic-and-validation&quot; tabindex=&quot;-1&quot;&gt;Requires additional CI logic and validation&lt;/h4&gt;
&lt;p&gt;The primary disadvantage of the rendered manifests pattern is the need for additional CI logic to either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;render and commit manifests, or&lt;/li&gt;
&lt;li&gt;validate that pre-rendered manifests are up to date and correct&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This adds extra pipeline code and conventions that must be maintained.&lt;/p&gt;
&lt;h4 id=&quot;inability-to-use-helm-hooks&quot; tabindex=&quot;-1&quot;&gt;Inability to use helm hooks&lt;/h4&gt;
&lt;p&gt;Because Helm is not executed at apply time, Helm lifecycle features such as hooks are unavailable. Some GitOps tools (e.g. ArgoCD) offer similar mechanisms, but they are not fully equivalent and may require chart-specific adjustments.&lt;/p&gt;
&lt;h4 id=&quot;rendered-secrets-would-contain-sensitive-values&quot; tabindex=&quot;-1&quot;&gt;Rendered Secrets would contain sensitive values&lt;/h4&gt;
&lt;p&gt;Because rendered manifests are stored in Git, embedding raw Kubernetes Secret resources is problematic. Instead should be moved to a dedicated secrets manager and injected at runtime using tools such as External Secrets Operator. While this removes some convenience, it enforces a more secure approach.&lt;/p&gt;
&lt;h4 id=&quot;larger-git-repositories&quot; tabindex=&quot;-1&quot;&gt;Larger git repositories&lt;/h4&gt;
&lt;p&gt;Rendering all Kubernetes resources results in additional files being stored in the repository. Over time this can lead to larger repos, increased diff noise, and slower checkouts if not managed carefully.&lt;/p&gt;
&lt;h2 id=&quot;comparing-implementations&quot; tabindex=&quot;-1&quot;&gt;Comparing Implementations&lt;/h2&gt;
&lt;h3 id=&quot;unrendered-not-using-the-rendered-manifests-pattern&quot; tabindex=&quot;-1&quot;&gt;Unrendered (not using the rendered manifests pattern)&lt;/h3&gt;
&lt;p&gt;Before implementing the rendered manifests pattern, it’s useful to understand how a system without it typically works and how it fails to benefit from the advantages described above.&lt;/p&gt;
&lt;p&gt;Popular GitOps engines such as ArgoCD and Flux support Kustomize and Helm directly. Because of this, you can store a reference to a Helm chart along with a &lt;code&gt;values.yaml&lt;/code&gt; file, and the GitOps engine will render and apply the resources to the cluster.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/attachments/Pasted%20image%2020260115091356.png&quot; alt=&quot;Pasted image 20260115091356.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This approach keeps configuration minimal and DRY. However, it makes it difficult to review or reason about how a change in Git will impact deployed resources. Upgrading a Helm chart version or modifying a single value can result in large and unexpected changes to the rendered manifests. A reviewer may not realize the scope of those changes, leading to potentially dangerous deployments.&lt;/p&gt;
&lt;h3 id=&quot;akuity-suggested-implementation&quot; tabindex=&quot;-1&quot;&gt;Akuity Suggested Implementation&lt;/h3&gt;
&lt;p&gt;The Akuity article describes the following implementation of the pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A user commits configuration changes to the main branch (typically via a PR)&lt;/li&gt;
&lt;li&gt;A continuous integration pipeline renders the manifests and commits them to an environment-specific branch&lt;/li&gt;
&lt;li&gt;Updates to environment-specific branches deploy the configuration automatically or via manual sync (with ArgoCD configured to use those branches as its source of truth)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/attachments/Pasted%20image%2020260113101432.png&quot; alt=&quot;Pasted image 20260113101432.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is a reasonable improvement, but it still limits reviewability: the rendered output is produced &lt;em&gt;after&lt;/em&gt; the primary review step unless an additional manual gate is introduced.&lt;/p&gt;
&lt;h3 id=&quot;kube-starter-kit-s-implementation&quot; tabindex=&quot;-1&quot;&gt;Kube Starter Kit&#39;s implementation&lt;/h3&gt;
&lt;p&gt;My goal was to maximize reviewability &lt;em&gt;before&lt;/em&gt; merge while still enforcing correctness &lt;em&gt;after&lt;/em&gt; merge.&lt;/p&gt;
&lt;p&gt;To achieve this, I chose to implement the pattern differently:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A user makes configuration changes on a branch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Before&lt;/strong&gt; creating a PR, the user renders the manifests locally and commits the changes to an environment-specific path in the repository&lt;/li&gt;
&lt;li&gt;PRs against the main branch trigger a CI pipeline that validates all manifests are properly rendered (i.e. re-rendering produces no diff)&lt;/li&gt;
&lt;li&gt;Merging to main deploys the new configuration (with ArgoCD configured to use environment-specific paths on the main branch)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/attachments/Pasted%20image%2020260113101720.png&quot; alt=&quot;Pasted image 20260113101720.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This approach provides full visibility during review while maintaining safety by enforcing correct rendering through CI.&lt;/p&gt;
&lt;p&gt;One downside is that users must have the required tooling installed locally and remember to render manifests before opening a PR. Tooling such as &lt;a href=&quot;https://mise.jdx.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;mise&lt;/a&gt; and pre-push Git hooks can reduce this burden. This has proven to be a worthwhile trade-off given the improvements to reviewability and operational safety.&lt;/p&gt;
&lt;p&gt;In practice, the difference between these approaches is not where rendering happens, but &lt;em&gt;when&lt;/em&gt; correctness and visibility are enforced.&lt;/p&gt;
&lt;h2 id=&quot;strong-conclusion-strong&quot; tabindex=&quot;-1&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The rendered manifests pattern makes the full configuration of resources being deployed explicit before it reaches a cluster. Rendering manifests ahead of time makes changes easier to review, reason about, and recover from.&lt;/p&gt;
&lt;p&gt;It does require some additional continuous integration work, but the benefits in visibility, safety, and operational clarity make it worthwhile.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2026/01/rendered-manifests-pattern/" />
        </entry>
        <entry>
            <title>
                github-actions-required-checks-for-conditional-jobs
                
            </title>
            <updated>2025-08-20T22:02:46.000Z</updated>
            <id>https://devopsdirective.com/posts/2025/08/github-actions-required-checks-for-conditional-jobs/</id>
            <content type="html">
                &lt;h1 id=&quot;git-hub-actions-required-checks-for-conditional-jobs&quot; tabindex=&quot;-1&quot;&gt;GitHub Actions Required Checks for Conditional Jobs&lt;/h1&gt;
&lt;p&gt;GitHub has a feature called &lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;rulesets&lt;/a&gt; which allows you to define rulesets to control various behaviors within a repo.&lt;/p&gt;
&lt;p&gt;One common use case for rulesets is to define required status checks which must pass before a PR can be merged into the default branch.&lt;/p&gt;
&lt;p&gt;For standard workflows, this is pretty straightforward. You choose the GitHub Actions workflow step you want to validate, and select it. For a workflow with a conditional job, or one which uses a matrix strategy to fan out over a variety of configurations, things get more complex.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP] Upcoming Course&lt;br /&gt;
This article was written as part of my preparations for the upcoming &amp;quot;GitHub Actions: Beginner to Pro&amp;quot; course I am working on.&lt;/p&gt;
&lt;p&gt;See: &lt;a href=&quot;https://github.com/sidpalas/devops-directive-github-actions-course&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/devops-directive-github-actions-course&lt;/a&gt; for more info!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example, &lt;a href=&quot;https://github.com/sidpalas/capstone&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;this repo&lt;/a&gt; conditionally runs tests for services based on which changed in the PR using a dynamic matrix (see: &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/08/advanced-github-actions-matrix/&quot;&gt;advanced-github-actions-matrix&lt;/a&gt;). For any given PR, from 0 to N run-tests steps will have executed and we want to validate any that ran passed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/08/github-actions-required-checks/gha-select-required-status-checks.png&quot; alt=&quot;gha-select-required-status-checks.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Which step should I choose?!&lt;/em&gt; 😱&lt;/p&gt;
&lt;p&gt;Before we continue it is also important to highlight the following (somewhat counterintuitive) behavior:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;br /&gt;
A job that is skipped will report its status as &amp;quot;Success&amp;quot;. It will not prevent a pull request from merging, even if it is a required check.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(See: &lt;a href=&quot;https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;This &lt;a href=&quot;https://github.com/actions/runner/issues/2566&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub Issue&lt;/a&gt; contains lots of useful discussion about this behavior and how to work with/around it and was the inspiration for Option 3 below.&lt;/p&gt;
&lt;h2 id=&quot;option-1-require-all-the-checks&quot; tabindex=&quot;-1&quot;&gt;Option 1: Require ALL THE CHECKS!&lt;/h2&gt;
&lt;p&gt;We could select each of the &lt;code&gt;run-tests&lt;/code&gt; steps as required.&lt;/p&gt;
&lt;p&gt;As noted, if a check is skipped, it reports its status as &amp;quot;success&amp;quot;, so this would work for ensuring all checks that ran passed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does not require modifying the workflow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a new services is added, you must remember to update the status checks&lt;/li&gt;
&lt;li&gt;The UI will have lots of required checks, many of which are skipped&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;option-2-add-downstream-check-to-compile-the-result&quot; tabindex=&quot;-1&quot;&gt;Option 2: Add Downstream Check to Compile the Result&lt;/h2&gt;
&lt;p&gt;We could also add a job to the workflow which depends on our dynamic matrix but runs even if it fails or is skipped by specifying &lt;code&gt;job.&amp;lt;job_id&amp;gt;.if: always()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;  run-tests-status-check:
    runs-on: ubuntu-24.04
    if: always()
    needs: [filter, run-tests]
    steps:
      - name: Run Tests Status Check
        run: |
          svcs=&#39;${{ needs.filter.outputs.services }}&#39;
          echo &amp;quot;services=${svcs}&amp;quot;
          echo &amp;quot;run-tests.result=${{ needs.run-tests.result }}&amp;quot;
          
          # If no services changed, treat as success (no tests to run)
          if &lt;a class=&quot;internal-link is-unresolved&quot; href=&quot;https://devopsdirective.com/404&quot; target=&quot;&quot;&gt; -z &amp;quot;$svcs&amp;quot; &lt;/a&gt;; then
            echo &amp;quot;No matching services changed; passing status check.&amp;quot;
            exit 0
          fi
          
          # Otherwise, require the matrix job to have succeeded
          if [[ &amp;quot;${{ needs.run-tests.result }}&amp;quot; == &amp;quot;success&amp;quot; ]]; then
            echo &amp;quot;All test matrix jobs passed.&amp;quot;
            exit 0
          else
            echo &amp;quot;::error One or more test matrix jobs failed or were cancelled.&amp;quot;
            exit 1
          fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can then use this single status check as the required check for our workflow:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/08/github-actions-required-checks/compiled-status-check.png&quot; alt=&quot;compiled-status-check.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A single status check keeps PR UI clean&lt;/li&gt;
&lt;li&gt;Continues working with no modifications when adding services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requires modifying the workflow&lt;/li&gt;
&lt;li&gt;Adds a few seconds to the critical path (even in the success case)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;option-3-add-downstream-check-which-raises-failures&quot; tabindex=&quot;-1&quot;&gt;Option 3: Add Downstream Check Which Raises Failures&lt;/h2&gt;
&lt;p&gt;A third option is to take advantage of the &amp;quot;skipped = success&amp;quot; behavior and add a job which is skipped UNLESS the upstream jobs fail or are cancelled.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;  run-tests-failure-alert:
    runs-on: ubuntu-latest
    needs: [filter, run-tests]
    if: ${{ cancelled() || contains(needs.*.result, &#39;cancelled&#39;) || contains(needs.*.result, &#39;failure&#39;) }}
    steps:
      - name: Conditionally Fail Required Check
        shell: bash
        run: |
          echo &amp;quot;::error Some required job has failed!&amp;quot;
          exit 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/08/github-actions-required-checks/failure-alert-skip.png&quot; alt=&quot;failure-alert-skip.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Successful Run (failure alert step is skipped)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/08/github-actions-required-checks/failure-alert-fail.png&quot; alt=&quot;failure-alert-fail.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Failed Run (failure alert runs and fails)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A single status check keeps PR UI clean&lt;/li&gt;
&lt;li&gt;Continues working with no modifications when adding services&lt;/li&gt;
&lt;li&gt;Doesn&#39;t add time to the critical path in the success case&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requires modifying the workflow&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;if: ${{ cancelled() || contains(needs.*.result, &#39;cancelled&#39;) || contains(needs.*.result, &#39;failure&#39;) }}&lt;/code&gt; is relatively complex&lt;/li&gt;
&lt;li&gt;Relies on understanding the skipped = success behavior (job naming + comments can help)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;final-thoughts&quot; tabindex=&quot;-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The fact that option 1 requires remembering to make a manual update as services are added/removed makes it inherently unsafe.&lt;/p&gt;
&lt;p&gt;In my opinion, options 2 &amp;amp; 3 are both reasonable approaches and choosing one over the other is a matter of personal preference. I like option 3 slightly more as implemented &lt;a href=&quot;https://github.com/sidpalas/capstone/blob/1e0f70247660f23280823564ee2529f45a2e4a07/.github/workflows/test.yaml#L52-L64&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;here&lt;/a&gt; because it avoids inline bash and doesn&#39;t extend the the critical path runtime.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2025/08/github-actions-required-checks-for-conditional-jobs/" />
        </entry>
        <entry>
            <title>
                advanced-github-actions-matrix
                
            </title>
            <updated>2025-08-20T15:19:02.336Z</updated>
            <id>https://devopsdirective.com/posts/2025/08/advanced-github-actions-matrix/</id>
            <content type="html">
                &lt;h1 id=&quot;advanced-usage-of-git-hub-actions-matrix-strategy-dynamic-fan-out&quot; tabindex=&quot;-1&quot;&gt;Advanced Usage of GitHub Actions Matrix Strategy (Dynamic Fan Out)&lt;/h1&gt;
&lt;p&gt;GitHub Actions has a feature called a &amp;quot;&lt;a href=&quot;https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;matrix strategy&lt;/a&gt;&amp;quot; that lets you run multiple copies of a job in parallel across different configurations.&lt;/p&gt;
&lt;p&gt;Most people use it for static combinations like operating system and language versions, but here’s the fun part: you can also use it to &lt;strong&gt;fan out over a set of job configurations dynamically generated at runtime&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP] Upcoming Course&lt;br /&gt;
This article was written as part of my preparations for the upcoming &amp;quot;GitHub Actions: Beginner to Pro&amp;quot; course I am working on.&lt;/p&gt;
&lt;p&gt;See: &lt;a href=&quot;https://github.com/sidpalas/devops-directive-github-actions-course&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/devops-directive-github-actions-course&lt;/a&gt; for more info!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;standard-usage&quot; tabindex=&quot;-1&quot;&gt;Standard Usage&lt;/h2&gt;
&lt;p&gt;The canonical use case for the matrix strategy feature is to handle testing or building for multiple language versions and operating system combinations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  example_matrix:
    strategy:
      matrix:
        version: [10, 12, 14]
        os: [ubuntu-latest, windows-latest]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, a job will be created for EACH combination of the input arrays. The above example would produce six jobs (3 &lt;code&gt;versions&lt;/code&gt; x 2 &lt;code&gt;os&lt;/code&gt; options).&lt;/p&gt;
&lt;p&gt;You can exclude specific combinations that &lt;em&gt;are&lt;/em&gt; covered by the input arrays:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  example_matrix_with_exclude:
	strategy:
	  matrix:
		version: [10, 12, 14]
		os: [ubuntu-latest, windows-latest]
		exclude:
			# Here we SKIP the version 14 windows configuration
			- version: 14
			  os: windows-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can include additional combinations which &lt;em&gt;are not&lt;/em&gt; covered by the input arrays:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  example_matrix_with_include:
	strategy:
	  matrix:
		version: [10, 12, 14]
		os: [ubuntu-latest, windows-latest]
		include:
			# Here we test version 16 ONLY for ubuntu
			- version: 16
			  os: ubuntu-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your desired job configurations aren&#39;t shaped like a combinatorial set, you can skip passing any input arrays and instead pass ONLY an &lt;code&gt;include&lt;/code&gt; list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  example_matrix_only_include:
	strategy:
	  matrix:
		include:
			# Here we run two specific configurations
			# which have no common elements
			- version: 12
			  os: ubuntu-latest
			- version: 14
			  os: windows-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dynamic-job-fan-out&quot; tabindex=&quot;-1&quot;&gt;Dynamic Job Fan Out&lt;/h2&gt;
&lt;p&gt;While many CI tasks do make sense to run across static combinations of individual configuration sets, others do not. One pattern in particular which I have found to be extremely useful is to have one upstream job which determines what set of downstream jobs need to be run.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/08/advanced-github-actions-matrix/filtering-job-pattern-diagram.png&quot; alt=&quot;filtering-job-pattern-diagram.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The upstream job may check which file paths have changed, or use a &lt;a href=&quot;https://github.com/bazel-contrib/target-determinator&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;build system determinator&lt;/a&gt; to determine which set of downstream jobs should run and with what configurations.&lt;/p&gt;
&lt;p&gt;For simplicity, let&#39;s use the names &lt;code&gt;&amp;quot;filter&amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;quot;execute&amp;quot;&lt;/code&gt; to refer to the the upstream and downstream jobs respectively.&lt;/p&gt;
&lt;h3 id=&quot;non-matrix-solution&quot; tabindex=&quot;-1&quot;&gt;Non-matrix Solution&lt;/h3&gt;
&lt;p&gt;One way to implement this pattern is to have a fixed set of jobs, and to have named outputs from the &lt;code&gt;filter&lt;/code&gt; job which can be passed to each of the &lt;code&gt;execute&lt;/code&gt; jobs explicitly.&lt;/p&gt;
&lt;p&gt;An example of this approach is highlighted in this recent Reddit Engineering blog post: &lt;a href=&quot;https://www.reddit.com/r/RedditEng/comments/1megwf1/our_buildkite_brings_all_the_devs_to_the_yard/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://www.reddit.com/r/RedditEng/comments/1megwf1/our_buildkite_brings_all_the_devs_to_the_yard/&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;unit-tests-1:    
  uses: ./.github/workflows/unit-test-shard.yml    
  secrets: inherit    
  needs: [build-selector]    
  if: ${{ needs.build-selector.outputs.unit-test-shard-1 != &#39;&#39; }}    
  with:      
    shard-index: 1      
    gradle-task: ${{ needs.build-selector.outputs.unit-test-shard-1 }}      
    total-shards: ${{ needs.build-selector.outputs.total-test-shards }}  
    
unit-tests-2:    
  uses: ./.github/workflows/unit-test-shard.yml    
  secrets: inherit
  needs: [build-selector]
  if: ${{ needs.build-selector.outputs.unit-test-shard-2 != &#39;&#39; }}    
  with:      
     shard-index: 2      
     gradle-task: ${{ needs.build-selector.outputs.unit-test-shard-2 }}      
     total-shards: ${{ needs.build-selector.outputs.total-test-shards }}  
     
unit-tests-3: 
   uses: ./.github/workflows/unit-test-shard.yml    
   secrets: inherit    
   needs: [build-selector]    
   if: ${{ needs.build-selector.outputs.unit-test-shard-3 != &#39;&#39; }}    
   with:      
      shard-index: 3
      gradle-task: ${{ needs.build-selector.outputs.unit-test-shard-3 }}     
      total-shards: ${{ needs.build-selector.outputs.total-test-shards }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This solution, while functional, has two major downsides. Namely, you must:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;define a fixed set of jobs&lt;/li&gt;
&lt;li&gt;copy/paste lots of yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In fact, these developer experience pain points were one of the big drawbacks the Reddit Engineering team cited that ultimately led them to choose Buildkite over GitHub Actions.&lt;/p&gt;
&lt;h3 id=&quot;matrix-solution&quot; tabindex=&quot;-1&quot;&gt;Matrix Solution&lt;/h3&gt;
&lt;p&gt;Fortunately, there is a way to achieve this type of job fan out which solves both of those downsides. The key enabler is the &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#fromjson&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;fromJSON()&lt;/code&gt; function&lt;/a&gt; supported within GitHub Action Expressions!&lt;/p&gt;
&lt;p&gt;We can combine &lt;code&gt;fromJSON&lt;/code&gt; with the ability to specify a matrix strategy containing only an &lt;code&gt;include&lt;/code&gt; key to dynamically generate and execute an arbitrary set of job configurations!&lt;/p&gt;
&lt;p&gt;Using the reddit example above, we could refactor our upstream filtering job (aka &lt;code&gt;build-selector&lt;/code&gt;) to output something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &amp;quot;shard-index&amp;quot;: 1,
    &amp;quot;unit-test-this-shard&amp;quot;: true,
    &amp;quot;gradle-task&amp;quot;: &amp;quot;test-shard-1&amp;quot;,
    &amp;quot;total-shards&amp;quot;: 3
  },
  {
    &amp;quot;shard-index&amp;quot;: 2,
    &amp;quot;unit-test-this-shard&amp;quot;: false,
    &amp;quot;gradle-task&amp;quot;: &amp;quot;test-shard-2&amp;quot;,
    &amp;quot;total-shards&amp;quot;: 3
  },
  {
    &amp;quot;shard-index&amp;quot;: 3,
    &amp;quot;unit-test-this-shard&amp;quot;: true,
    &amp;quot;gradle-task&amp;quot;: &amp;quot;test-shard-3&amp;quot;,
    &amp;quot;total-shards&amp;quot;: 3
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would enable refactoring the downstream jobs into one using a matrix strategy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;unit-tests:
  uses: ./.github/workflows/unit-test-shard.yml    
  secrets: inherit    
  needs: [build-selector]    
  if: ${{ matrix.unit-test-this-shard == &#39;true&#39; }}    
  strategy:
	  matrix:
		  include: ${{ fromJson(needs.build-selector.outputs.matrix) }}
  with:      
    shard-index: ${{ matrix.shard-index }}      
    gradle-task: ${{ matrix.gradle-task }}
    total-shards: ${{ matrix.total-shards }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🚨 &lt;strong&gt;NOTE:&lt;/strong&gt; It is important to call out that this doesn’t actually work because you can’t use an &lt;code&gt;if&lt;/code&gt; conditional in a matrix job.&lt;/p&gt;
&lt;p&gt;Luckily, because we control the JSON, we can simplify further and omit any entries we don&#39;t want to execute test jobs for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &amp;quot;shard-index&amp;quot;: 1,
    &amp;quot;gradle-task&amp;quot;: &amp;quot;test-shard-1&amp;quot;,
    &amp;quot;total-shards&amp;quot;: 3
  },
  {
    &amp;quot;shard-index&amp;quot;: 3,
    &amp;quot;gradle-task&amp;quot;: &amp;quot;test-shard-3&amp;quot;,
    &amp;quot;total-shards&amp;quot;: 3
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows us to reduce the JSON array AND remove the now unnecessary conditional:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;unit-tests:
  uses: ./.github/workflows/unit-test-shard.yml    
  secrets: inherit    
  needs: [build-selector]    
  strategy:
	  matrix:
		  include: ${{ fromJson(needs.build-selector.outputs.matrix) }}
  with:      
    shard-index: ${{ matrix.shard-index }}      
    gradle-task: ${{ matrix.gradle-task }}
    total-shards: ${{ matrix.total-shards }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;working-example&quot; tabindex=&quot;-1&quot;&gt;Working Example&lt;/h2&gt;
&lt;p&gt;A full workflow demonstrating this pattern can be seen here: &lt;a href=&quot;https://github.com/sidpalas/devops-directive-github-actions-course/blob/3e5401e1d8ea113bf1f9f4e2372fa8f753324fd0/.github/workflows/04-advanced-features--07-dynamic-matrix.yaml&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/devops-directive-github-actions-course/blob/3e5401e1d8ea113bf1f9f4e2372fa8f753324fd0/.github/workflows/04-advanced-features--07-dynamic-matrix.yaml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/08/advanced-github-actions-matrix/dynamic-matrix-example.png&quot; alt=&quot;dynamic-matrix-example.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;generate-matrix&lt;/code&gt; job outputs a JSON array of length N (provided as input to the job) with each item in the array having random values for &lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
	{
		foo: 35,
		bar: 29
	},
	{
		foo: 37,
		bar: 41
	},
	{
		foo: 77,
		bar: 66
	}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is then passed into the &lt;code&gt;execute&lt;/code&gt; job using &lt;code&gt;fromJSON&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;  execute:
    needs: generate-matrix
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include: ${{ fromJSON(needs.generate-matrix.outputs.include) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;prior-art&quot; tabindex=&quot;-1&quot;&gt;Prior Art&lt;/h2&gt;
&lt;h3 id=&quot;conditional-matrix-elements-stack-overflow&quot; tabindex=&quot;-1&quot;&gt;Conditional Matrix Elements (Stack Overflow)&lt;/h3&gt;
&lt;p&gt;In writing this post, I also came across this Stack Overflow post &lt;a href=&quot;https://stackoverflow.com/questions/65384420/how-do-i-make-a-github-action-matrix-element-conditional&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;“How do I make a GitHub Action matrix element conditional?&lt;/a&gt; and the GitHub action it inspired: &lt;a href=&quot;https://github.com/marketplace/actions/conditional-build-matrix&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;conditional-build-matrix&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;These use matrix + &lt;code&gt;fromJSON&lt;/code&gt;, but takes a fixed JSON file as its input and filters down to a subset of its contents to generate a the matrix configuration.&lt;/p&gt;
&lt;h3 id=&quot;from-json-documentation&quot; tabindex=&quot;-1&quot;&gt;fromJSON Documentation&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;fromJSON&lt;/code&gt; docs show an example like this, with an upstream job returning a json object which the downstream job uses for its matrix input: &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#example-returning-a-json-object&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#example-returning-a-json-object&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot; tabindex=&quot;-1&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Combining GitHub Actions’ matrix strategy with the &lt;code&gt;fromJSON&lt;/code&gt; function unlocks a powerful way to dynamically fan out jobs to any set of configurations without hardcoding every possibility.&lt;/p&gt;
&lt;p&gt;This approach is especially valuable in large codebases or monorepos, where selective test sharding can save substantial CI time and cost.&lt;/p&gt;
&lt;p&gt;The recent Reddit Engineering post reminded me how often this pattern is overlooked. While the fromJSON docs technically show an example, it’s buried and easy to miss unless you already know to look for it. Hopefully, this write-up makes the pattern more discoverable and gives you a practical starting point for your own workflows.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2025/08/advanced-github-actions-matrix/" />
        </entry>
        <entry>
            <title>
                speed-run-new-post
                
            </title>
            <updated>2025-07-09T18:03:34.000Z</updated>
            <id>https://devopsdirective.com/posts/2025/07/speed-run-new-post/</id>
            <content type="html">
                &lt;h1 id=&quot;this-is-a-test-to-see-how-fast-i-can-create-a-post&quot; tabindex=&quot;-1&quot;&gt;This is a test to see how fast I can create a post&lt;/h1&gt;
&lt;p&gt;See &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/07/site-revamp-3-0/&quot;&gt;site-revamp-3.0&lt;/a&gt; for more info!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/new-post-timer.png&quot; alt=&quot;new-post-timer.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Not bad!&lt;/em&gt;&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2025/07/speed-run-new-post/" />
        </entry>
        <entry>
            <title>
                site-revamp-3.0
                
            </title>
            <updated>2025-07-10T14:00:53.077Z</updated>
            <id>https://devopsdirective.com/posts/2025/07/site-revamp-3-0/</id>
            <content type="html">
                &lt;h1 id=&quot;yet-another-site-revamp-devopsdirective-com-v3-0&quot; tabindex=&quot;-1&quot;&gt;Yet Another Site Revamp (devopsdirective․com v3.0)&lt;/h1&gt;
&lt;p&gt;At the time of publishing this article (2025-07-09), I hadn&#39;t &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2023/11/velero-aws-account-migration/&quot;&gt;published an article to this site in 602 days&lt;/a&gt;! 😱&lt;/p&gt;
&lt;p&gt;I&#39;m not 100% sure why this is, but I think it as at least partially due to the friction involved with creating and publishing posts.&lt;/p&gt;
&lt;h2 id=&quot;previous-setup&quot; tabindex=&quot;-1&quot;&gt;Previous Setup&lt;/h2&gt;
&lt;p&gt;For a full history of the tooling for this site see:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2020/02/hugo-and-caddy-on-gcp/&quot;&gt;The Making of This Site (Hugo, Caddy, + GCP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2020/02/hugo-cloud-build/&quot;&gt;CI/CD for this site (Hugo + Cloud Build)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2020/10/gcs-cloudflare-hosting/&quot;&gt;Static Site Hosting Using Google Cloud Storage and Cloudflare (with SSL!)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2022/08/blog-workflow-improvements/&quot;&gt;Blog Workflow Improvements&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Previously, the site was built using &lt;a href=&quot;https://gohugo.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://gohugo.io/&lt;/a&gt;. I authored posts in VS Code, either locally or using GitPod, and committed the changes to GitHub, where a GitHub Action handled building/pushing to Google Cloud Storage to be served.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/old-site-setup.png&quot; alt=&quot;old-site-setup.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;This worked fine, but didn&#39;t ✨spark joy✨&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Despite my &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2022/08/blog-workflow-improvements/&quot;&gt;previous attempts to improve the workflow&lt;/a&gt;, the (small) friction of authoring in VS Code and interacting with Git directly were still annoying. I also had a couple of custom Hugo components for links and images which added additional micro-cuts to the workflow.&lt;/p&gt;
&lt;h2 id=&quot;should-i-use-ghost&quot; tabindex=&quot;-1&quot;&gt;Should I use Ghost?&lt;/h2&gt;
&lt;p&gt;As I considered what to do, I once again revisited &lt;a href=&quot;https://ghost.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://ghost.org/&lt;/a&gt; as a potential option. It is slick, but requires either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Paying them to host it (which gets quite pricey as the number of members grows)&lt;br /&gt;
&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/ghost-pricing.png&quot; alt=&quot;ghost-pricing.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hosting it yourself and managing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Ghost application&lt;/li&gt;
&lt;li&gt;MySQL database (+ backups)&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://www.mailgun.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Mailgun&lt;/a&gt; subscription&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://cloudinary.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloudinary&lt;/a&gt; subscription&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I played around with hosting it on &lt;a href=&quot;https://railway.com/?referralCode=P06La2&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Railway&lt;/a&gt; with &lt;a href=&quot;https://railway.com/new/template/ghost&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;this template&lt;/a&gt;, but ran into some wonkiness around:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Healthchecks:&lt;/strong&gt; I was unable to get &lt;a href=&quot;https://docs.railway.com/guides/healthchecks&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Railway&#39;s healthchecks&lt;/a&gt; for the Ghost application working properly on Railway (which in turn prevents zero downtime deploys) for two reasons:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;https redirect:&lt;/strong&gt; Railway healthchecks are made without TLS and ghost returns a &lt;code&gt;301&lt;/code&gt; redirect from &lt;code&gt;http -&amp;gt; https&lt;/code&gt;. I think this could be worked around by setting this http header: &lt;code&gt;X-Forwarded-Proto: https&lt;/code&gt; (but Railway does not allow setting custom headers for healthchecks).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;hostname:&lt;/strong&gt; Railway healthchecks originate from &lt;code&gt;healthcheck.railway.app&lt;/code&gt;. Again, if we could set custom http headers, we could use: &lt;code&gt;Host: devopsdirective.com&lt;/code&gt;, but we can’t.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Volume Snapshots:&lt;/strong&gt; Volume snapshots on Railway are only available to pro users. I could have implemented a separate mechanism to backup the database OR committed to this costing at least $20/month... but I&#39;m irrationally frugal (sometimes to my own detriment)! 🤑&lt;br /&gt;
&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/railway-canvas.png&quot; alt=&quot;railway-canvas.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;That canvas view is 🔥 though!&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;asking-for-inputs&quot; tabindex=&quot;-1&quot;&gt;Asking for Inputs&lt;/h2&gt;
&lt;p&gt;Two days ago I decided to see if anyone had recommendations for a blog setup they loved, and &lt;a href=&quot;https://www.linkedin.com/in/ovictorbuendia/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Victor Buendia&lt;/a&gt; recommended using &lt;a href=&quot;https://obsidian.md/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Obsidian&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/linkedin-help.png&quot; alt=&quot;linkedin-help.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;He might be onto something 👀&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I already use Obsidian for note taking, so the questions for me were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Can I make it do what I want it to?&lt;/li&gt;
&lt;li&gt;How seamless the publishing could be?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;obsidian-digital-garden&quot; tabindex=&quot;-1&quot;&gt;Obsidian Digital Garden&lt;/h2&gt;
&lt;p&gt;Turns out, a Norwegian legend named &lt;a href=&quot;https://github.com/oleeskild&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Ole Eskild Steensen&lt;/a&gt; has already done all of the hard work to make managing and publishing a website from Obsidian super nice with his project:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/obsidian-digital-garden-screenshot.png&quot; alt=&quot;obsidian-digital-garden-screenshot.png&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href=&quot;https://dg-docs.ole.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://dg-docs.ole.dev/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href=&quot;https://github.com/oleeskild/obsidian-digital-garden&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/oleeskild/obsidian-digital-garden&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-does-it-work&quot; tabindex=&quot;-1&quot;&gt;How Does it Work?&lt;/h3&gt;
&lt;p&gt;The setup instructions can be found here: &lt;a href=&quot;https://dg-docs.ole.dev/getting-started/01-getting-started/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://dg-docs.ole.dev/getting-started/01-getting-started/&lt;/a&gt;, but essentially you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the Digital Garden obsidian plugin&lt;/li&gt;
&lt;li&gt;Create a GitHub repo to store the site contents&lt;/li&gt;
&lt;li&gt;Authenticate the plugin via a GitHub personal access token&lt;/li&gt;
&lt;li&gt;Configure Vercel to build/deploy the site&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;dg-publish: true&lt;/code&gt; to the frontmatter of any Obsidian note you want to publish&lt;/li&gt;
&lt;li&gt;Use the Obsidian command pallet to publish one (or all) specified notes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/new-site-setup.png&quot; alt=&quot;new-site-setup.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The magic happens with:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Plugin:
&lt;ol&gt;
&lt;li&gt;Determining which notes + images need to be published&lt;/li&gt;
&lt;li&gt;Generating the appropriate file structure expected by the Eleventy configuration&lt;/li&gt;
&lt;li&gt;Automatically handling the git operations to commit and push those&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Vercel:
&lt;ol&gt;
&lt;li&gt;Building each commit with &lt;a href=&quot;https://www.11ty.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Eleventy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Deploying each commit&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;my-new-workflow&quot; tabindex=&quot;-1&quot;&gt;My New Workflow&lt;/h3&gt;
&lt;p&gt;I have a dedicated Obsidian vault for this website. It is synced across computers and mobile via iCloud. The daily note is configured to use the following template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;---
dg-publish: false
dg-pinned: false
dg-hide: false
tags: 
dg-published-at: {{date}}
title: foo bar baz
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and is created at: &lt;code&gt;/Posts/YYYY/MM/DD&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So, to create a post:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the Obsidian &amp;quot;Daily Notes: Open today&#39;s daily note&amp;quot; command&lt;/li&gt;
&lt;li&gt;Update the note name + title&lt;/li&gt;
&lt;li&gt;Write the actual post 😅&lt;/li&gt;
&lt;li&gt;Toggle &lt;code&gt;dg-publish: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use the Obsidian &amp;quot;Digital Garden: Publish Multiple Notes&amp;quot; command&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;...about 30 seconds later the post is live!&lt;/p&gt;
&lt;p&gt;For images, I paste them into obsidian directly. The initially are stored in &lt;code&gt;/attachments&lt;/code&gt; with an autogenerated name like &lt;code&gt;Pasted Image 20250709113743_986&lt;/code&gt;, but I usually move them to a directory alongside the post note and rename to something meaningful.&lt;/p&gt;
&lt;h2 id=&quot;porting-everything-over&quot; tabindex=&quot;-1&quot;&gt;Porting Everything Over&lt;/h2&gt;
&lt;p&gt;In order to make the switch, I needed the corpus of articles already published migrated over.&lt;/p&gt;
&lt;p&gt;Luckily, Hugo mostly uses vanilla markdown for authoring posts, so I was able to copy everything over and make a few minor tweaks to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Images:&lt;/strong&gt; I had custom Hugo components for handling images (and images that are also links).&lt;pre&gt;&lt;code&gt;{{&amp;lt; img-link &amp;quot;path-to-image.png&amp;quot; &amp;quot;https://some.link&amp;quot; &amp;quot;Some caption&amp;quot; &amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
I updated these to be:&lt;pre&gt;&lt;code&gt;[![[path-to-image.png]]](https://some.link)
*Some caption*
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Links:&lt;/strong&gt; In Hugo, default markdown links opened within the same tab so I had a custom link component to open links in new tabs.&lt;pre&gt;&lt;code&gt;{{&amp;lt; link &amp;quot;https://some.link&amp;quot; &amp;quot;Displayed text&amp;quot; &amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
I updated these to be:&lt;pre&gt;&lt;code&gt;%% External Links %%
[Displayed text](https://some.link)

%% Internal Links %%
[some-internal-page|Displayed text]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I could have scripted this, but the number of pages was such that I just converted these manually. Let me know if you notice an image or link that seems broken! 🙏&lt;/p&gt;
&lt;h3 id=&quot;javascript-sorting-woes&quot; tabindex=&quot;-1&quot;&gt;Javascript sorting woes&lt;/h3&gt;
&lt;p&gt;Everything was looking great, when I realized that the sorting of posts was wonky.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/broken-javascript-sorting.png&quot; alt=&quot;broken-javascript-sorting.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;10, 11, 02, 03,... WTH?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When constructing my directories (and permalinks), I had used left padded zeros to ensure that the months would sort correctly numerically AND alphabetically.&lt;/p&gt;
&lt;p&gt;So what is going on here? The digital garden site has a method which sorts these recursively which can be found here: &lt;a href=&quot;https://github.com/oleeskild/digitalgarden/blob/9e19880c4e13ea2ed30da296dca7e1ddf2de6118/src/helpers/filetreeUtils.js#L1-L57&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;filetreeUtils.js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This CORRECTLY sorts the months as you would expect. However, this sorted tree is stored as a normal Javascript object which gets rendered by a &lt;a href=&quot;https://mozilla.github.io/nunjucks/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Nunjucks template&lt;/a&gt; here: &lt;a href=&quot;https://github.com/oleeskild/digitalgarden/blob/9e19880c4e13ea2ed30da296dca7e1ddf2de6118/src/site/_includes/components/filetree.njk#L16-L18&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;filetree.njk&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This traverses the keys of the Javascript object, resulting in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;10&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;11&amp;quot;&lt;/code&gt;, and &lt;code&gt;&amp;quot;12&amp;quot;&lt;/code&gt; being interpreted as integer indices&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;01&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;02&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;03&amp;quot;&lt;/code&gt;, ...etc being interpreted as non-integer indices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Given that, the ES6 Rules specify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, the keys that are integer indices, in ascending numeric order.&lt;/li&gt;
&lt;li&gt;Then, all other string keys, in the order in which they were added to the object.&lt;/li&gt;
&lt;li&gt;Lastly, all symbol keys, in the order in which they were added to the object.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(See &lt;a href=&quot;https://2ality.com/2015/10/property-traversal-order-es6.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://2ality.com/2015/10/property-traversal-order-es6.html&lt;/a&gt; for more detail on what &amp;quot;Integer Indices&amp;quot; actually are)&lt;/p&gt;
&lt;p&gt;We end up with this incorrect ordering! 🤬&lt;/p&gt;
&lt;p&gt;My solution was to strip the leading zeros when constructing the key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;folders = note.filePathStem
	.split(&amp;quot;notes/&amp;quot;)[1]
	.split(&amp;quot;/&amp;quot;)
	// Here we strip leading zeros on the month folder to avoid JS treating 10, 11, 12 as integers,
	// but 01, 02, 03 as strings and sorting incorrectly when displaying with nunjucks. This allows us to
	// keep the leading zeros so that posts from our old hugo site all have the same URL structure
	.map((str) =&amp;gt; str.replace(/^0+(?=&#92;d)/, &#39;&#39;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives me the sorting I want, while maintaining the URL structure from my old site (with leading zeros) to avoid breaking old links.&lt;/p&gt;
&lt;h2 id=&quot;isn-t-vercel-overengineered-for-a-static-site&quot; tabindex=&quot;-1&quot;&gt;Isn&#39;t Vercel overengineered for a static site?&lt;/h2&gt;
&lt;p&gt;The site is built with Eleventy, which generates a static site that could be hosted in many ways (including the old GCS + Cloudflare setup I had before). I could totally update my old GitHub action to use Eleventy (instead of Hugo) to build the new site and deploy to GCS as I was before (and I may do that some day).&lt;/p&gt;
&lt;p&gt;For now, I used Vercel because the Obsidian Digital Garden project has a &amp;quot;Deploy to Vercel&amp;quot; button which creates the repo and wires up the necessary configurations. This enabled me to get up and running in just a few minutes! I will monitor if anything ends up costing me (e.g. Vercel&#39;s Image optimization or total volume of traffic served), but for now I&#39;m happy with the setup.&lt;/p&gt;
&lt;h2 id=&quot;speedrun-a-new-post&quot; tabindex=&quot;-1&quot;&gt;Speedrun a New Post&lt;/h2&gt;
&lt;p&gt;Okay, let&#39;s now test how long it takes me to create a new post.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/site-revamp-3.0/new-post-timer.png&quot; alt=&quot;new-post-timer.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;34 seconds!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/07/speed-run-new-post/&quot;&gt;This is a test to see how fast I can create a post&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Will this revamp actually inspire me to write more often? I&#39;m hopeful, but only time will tell... 🤞&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2025/07/site-revamp-3-0/" />
        </entry>
        <entry>
            <title>
                organizing-terraform-configurations
                
            </title>
            <updated>2025-08-04T15:13:57.881Z</updated>
            <id>https://devopsdirective.com/posts/2025/07/organizing-terraform-configurations/</id>
            <content type="html">
                &lt;h1 id=&quot;organizing-terraform-configurations-single-instance-vs-multi-instance-root-modules&quot; tabindex=&quot;-1&quot;&gt;Organizing Terraform Configurations (Single-Instance vs. Multi-Instance Root Modules)&lt;/h1&gt;
&lt;p&gt;There are a few primary approaches when it comes to organizing terraform configurations to be deployed across multiple environments (e.g. &lt;code&gt;dev&lt;/code&gt;/&lt;code&gt;staging&lt;/code&gt;/&lt;code&gt;prod&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I have seen a bunch of strong opinions about this on this floating around the interwebs recently, and wanted to lay out my thinking on the matter.&lt;/p&gt;
&lt;h2 id=&quot;terminology&quot; tabindex=&quot;-1&quot;&gt;Terminology&lt;/h2&gt;
&lt;p&gt;Before we can have an informed discussion about this topic, first we should agree on terminology and review existing resources:&lt;/p&gt;
&lt;p&gt;Matt Gowie (&lt;a href=&quot;https://masterpoint.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://masterpoint.io/&lt;/a&gt;) uses the terms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;multi-instance root module&lt;/code&gt;: A root module with multiple state files associated with it. This is done either with TF workspaces or with dynamic backend configurations, aka &amp;quot;Dynamic Backends&amp;quot;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;single-instance root module&lt;/code&gt;: A root module directory with &lt;em&gt;only one&lt;/em&gt; state file associated with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href=&quot;https://newsletter.masterpoint.io/p/multi-instance-vs-single-instance-root-modules&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Blog&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/posts/gowiem_terraform-opentofu-infrastructure-activity-7287508747831201793-EdkI&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;LinkedIn Post&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yevgeniy &amp;quot;Jim&amp;quot; Brikman (&lt;a href=&quot;https://www.gruntwork.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://www.gruntwork.io/&lt;/a&gt;) describes the different approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Workspaces&lt;/code&gt;: Terraform workspaces are one method of achieving &lt;code&gt;multi-instance&lt;/code&gt; root modules.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Branches&lt;/code&gt;: Git branches can be used to store different static backend configurations for different environments within the same root module. Similar to managing long lived released branches, this will lead to pain and suffering. Please don&#39;t do this 🙏.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;File Layout&lt;/code&gt; (optionally using Terragrunt or similar): Having separate root modules allows for configuring different backends by having separate files, organized via a logical directory structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href=&quot;https://www.terraformupandrunning.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Terraform Up and Running (Book)&lt;/a&gt;, &lt;a href=&quot;https://www.gruntwork.io/blog/how-to-manage-multiple-environments-with-terraform-using-workspaces&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Workspaces Blog&lt;/a&gt;, &lt;a href=&quot;https://www.gruntwork.io/blog/how-to-manage-multiple-environments-with-terraform-using-branches&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Branches Blog&lt;/a&gt;, &lt;a href=&quot;https://www.gruntwork.io/blog/how-to-manage-multiple-environments-with-terraform-using-terragrunt&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Terragrunt Blog&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I like the &lt;code&gt;single-instance&lt;/code&gt; vs &lt;code&gt;multi-instance&lt;/code&gt; terminology and will use it for the remainder of the article.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]&lt;br /&gt;
There are multiple ways to implement each of these approaches and those differences are important. For example, Terraform doesn&#39;t have dynamic backends but OpenTofu does. This is a critical difference as to whether a &lt;code&gt;multi-instance&lt;/code&gt; root module can meet your organizations security requirements.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;what-are-we-trying-to-achieve&quot; tabindex=&quot;-1&quot;&gt;What are we trying to achieve?&lt;/h2&gt;
&lt;p&gt;Rather than jump in and claim one approach is better than the other, we should first lay out what goals we want to achieve with our infrastructure configurations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Safety&lt;/strong&gt; - Guarantee the Infrastructure as Code changes execute &lt;em&gt;only&lt;/em&gt; in their intended environment (&lt;code&gt;staging ⟹ staging&lt;/code&gt;, &lt;code&gt;production ⟹ production&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;/strong&gt; - Prevent accidental configuration drift between environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upgradability&lt;/strong&gt; - Control the flow of provider and module upgrades across environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clarity&lt;/strong&gt; - Make it obvious to readers which environments exist and what resources they contain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt; - Isolate production state from non-production state for compliance rules and reduce the blast radius of any change.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;1-safety&quot; tabindex=&quot;-1&quot;&gt;1. Safety&lt;/h3&gt;
&lt;p&gt;It is critical that we apply Infrastructure as Code configurations to the desired environment and &lt;em&gt;ONLY&lt;/em&gt; the desired environment. An accidental application the wrong configuration to a production environment could be catastrophic.&lt;/p&gt;
&lt;p&gt;The proper way to achieve this type of safety is to avoid applying configurations to production manually and limit production applies to a &lt;em&gt;Terraform Automation and COllaboration Software&lt;/em&gt; (TACOS) tool such as Atlantis, Spacelift, or Env0. Once configured, these automations will ensure applies end up targeting the right environment.&lt;/p&gt;
&lt;h4 id=&quot;what-about-in-a-break-glass-scenario&quot; tabindex=&quot;-1&quot;&gt;What about in a break glass scenario?! 💥&lt;/h4&gt;
&lt;p&gt;In the heat of an incident, teams will often resort to overriding existing automations. I believe having &lt;code&gt;single-instance&lt;/code&gt; root modules (as opposed to terraform workspaces) makes it &lt;em&gt;slightly&lt;/em&gt;  more clear which environment you are targeting.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Bottom Line&lt;br /&gt;
The difference between approaches here is small and in my opinion does not tip the scale one way or another.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;2-consistency&quot; tabindex=&quot;-1&quot;&gt;2. Consistency&lt;/h3&gt;
&lt;p&gt;Another key consideration for IaC configurations is keeping environments consistent with one another.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;multi-instance&lt;/code&gt; root module by definition achieves this (it is a single module).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;multi-instance&lt;/code&gt; supporters often claim that &lt;code&gt;single-instance&lt;/code&gt; root modules will cause drift and duplication due to the additional files. In my opinion this is mostly a straw man argument.&lt;/p&gt;
&lt;p&gt;Why? Because regardless of which approach you chose, the logic for your configurations should live in &lt;strong&gt;SEPARATE CHILD MODULES&lt;/strong&gt;! In the &lt;code&gt;single-instance&lt;/code&gt; approach, the root modules are primarily:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A backend configuration&lt;/li&gt;
&lt;li&gt;Consuming a bunch of child modules (populating their input variables)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the logic lives in separate modules, the difference between whether you store the input variables in environment specific &lt;code&gt;.tfvars&lt;/code&gt; files and pass them into a &lt;code&gt;multi-instance&lt;/code&gt; root module or store them in an environment specific root module directly is minimal from a consistency standpoint.&lt;/p&gt;
&lt;p&gt;Let&#39;s look at set of files required by each approach for a hypothetical config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# The multi-instance root modules maps a bunch of tfvars values into child module inputs
└── multi-instance
    ├── live
    │   ├── compute
    │   │   ├── main.tf
    │   │   ├── production.tfvars
    │   │   └── staging.tfvars
    │   ├── global
    │   │   └── main.tf
    │   └── networking
    │       ├── main.tf
    │       ├── production.tfvars
    │       └── staging.tfvars
    └── modules # ALL OF THE LOGIC LIVES HERE!
        ├── compute
        │   ├── main.tf
        │   └── variables.tf
        └── networking
            ├── main.tf
            └── variables.tf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# The single-instance root modules set the child module inputs directly
└── single-instance
    ├── live
    │   ├── global
    │   │   └── main.tf
    │   ├── production
    │   │   ├── compute
    │   │   │   └── main.tf
    │   │   └── networking
    │   │       └── main.tf
    │   └── staging
    │       ├── compute
    │       │   └── main.tf
    │       └── networking
    │           └── main.tf
    └── modules # ALL OF THE LOGIC LIVES HERE!
        ├── compute
        │   ├── main.tf
        │   └── variables.tf
        └── networking
            ├── main.tf
            └── variables.tf

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; Tools like &lt;a href=&quot;https://terragrunt.gruntwork.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Terragrunt&lt;/a&gt; and &lt;a href=&quot;https://terramate.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Terramate&lt;/a&gt; help avoid the duplication of the backend blocks and reduce opportunities for configuration drift.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Bottom Line&lt;br /&gt;
A &lt;code&gt;multi-instance&lt;/code&gt; root module has an edge in this category, but it is much smaller than its proponents claim.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;3-upgradability&quot; tabindex=&quot;-1&quot;&gt;3. Upgradability&lt;/h3&gt;
&lt;p&gt;Within a root module, you can only specify a single version for each usage of a particular provider and module.&lt;/p&gt;
&lt;p&gt;All workspaces share the same &lt;code&gt;required_providers&lt;/code&gt; and child module source/version constraints.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;multi-instance&lt;/code&gt; root modules require upgrading these versions all at once. You can still apply them separately (i.e. apply to non-prod first), but until you have applied the upgrade across all environments, the configuration in version control will be out of date with reality.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;single-instance&lt;/code&gt; root modules can specify different version of providers and child modules. This allows for a more controlled upgrade path, where new versions can be validated in lower environments before a change is made to production.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Bottom Line&lt;br /&gt;
&lt;code&gt;single-instance&lt;/code&gt; root modules allow for testing upgrades of providers and child modules in a more controlled fashion than a &lt;code&gt;multi-instance&lt;/code&gt; root module.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;4-clarity&quot; tabindex=&quot;-1&quot;&gt;4. Clarity&lt;/h3&gt;
&lt;p&gt;Another consideration is the speed with which a developer can read and understand an IaC configuration. This includes things like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What environments exist?&lt;/li&gt;
&lt;li&gt;What resources are deployed in each environment?&lt;/li&gt;
&lt;li&gt;How to validate my changes for a particular environment?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a &lt;code&gt;multi-instance&lt;/code&gt; root module these questions are answered by examining the root module and mentally mapping values from corresponding &lt;code&gt;.tfvars&lt;/code&gt; files into it. For &lt;code&gt;single-instance&lt;/code&gt; root modules, you navigate to the corresponding directory and everything is right there.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Bottom Line&lt;br /&gt;
In my opinion, &lt;code&gt;single-instance&lt;/code&gt; root modules are easier to reason about and quickly understand exactly what is deployed to a given environment. &lt;strong&gt;Note:&lt;/strong&gt; This is subjective and may be due me having more experience working with them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;5-security&quot; tabindex=&quot;-1&quot;&gt;5. Security&lt;/h3&gt;
&lt;p&gt;For both practical and compliance reasons, it is useful to isolate the state of production environments from the state of non-production environments.&lt;/p&gt;
&lt;p&gt;Terraform workspaces share a single backend block, so state isolation for &lt;code&gt;multi-instance&lt;/code&gt; root modules wasn’t &lt;strong&gt;natively&lt;/strong&gt; supported until the release of &lt;a href=&quot;https://github.com/opentofu/opentofu/releases/tag/v1.8.0&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;OpenTofu 1.8.0&lt;/a&gt; (released 2024-07-29), which introduced the ability to use variables and locals within the backend block.&lt;/p&gt;
&lt;p&gt;Prior to that release, the only ways to achieve true state isolation were to either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use single instance root modules.&lt;/li&gt;
&lt;li&gt;Use a Terraform wrapper such as Terragrunt/Terramate to dynamically generate the backend at runtime.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Bottom Line&lt;br /&gt;
For Terraform, &lt;code&gt;single-instance&lt;/code&gt; root modules are the only native way to achieve true state isolation. For OpenTofu, a &lt;code&gt;multi-instance&lt;/code&gt; root module with a dynamic backend configuration can achieve this as well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;closing-thoughts&quot; tabindex=&quot;-1&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Both directory‑based &lt;strong&gt;single‑instance&lt;/strong&gt; roots and workspace/dynamic‑backend &lt;strong&gt;multi‑instance&lt;/strong&gt; roots are viable patterns in 2025. The right choice hinges on which trade‑offs map to your team’s priorities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security &amp;amp; isolation:&lt;/strong&gt; OpenTofu’s dynamic backends eliminate the last hard blocker for &lt;code&gt;multi‑instance&lt;/code&gt; layouts, allowing a unique bucket and IAM role per workspace. If you are on Terraform, &lt;code&gt;single‑instance&lt;/code&gt; still offers the most straightforward state segregation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upgrade cadence &amp;amp; visibility:&lt;/strong&gt; Separate directories make it trivial to stage provider or module upgrades in lower environments before promoting to production, and they double as living documentation of what runs where.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational consistency:&lt;/strong&gt; A &lt;code&gt;multi‑instance&lt;/code&gt; root module guarantees every environment uses the exact same codebase, reducing drift and simplifying CI pipelines, but moving logic into child modules mostly accomplishes the same thing for &lt;code&gt;single-instance&lt;/code&gt; configurations.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Bottom Line&lt;br /&gt;
For an infrastructure configuration that will be repeated many times, e.g. deploying a bunch of single tenant infrastructure stacks for different customers, I would lean towards the &lt;code&gt;multi-instance&lt;/code&gt; root module approach.&lt;/p&gt;
&lt;p&gt;For an infrastructure configuration that has a fixed set of environments, e.g. a company with a staging and production environment, I would lean towards &lt;code&gt;single-instance&lt;/code&gt; root modules organized by the file layout.&lt;/p&gt;
&lt;/blockquote&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2025/07/organizing-terraform-configurations/" />
        </entry>
        <entry>
            <title>
                github-actions-tool-cache
                
            </title>
            <updated>2025-08-08T01:57:21.432Z</updated>
            <id>https://devopsdirective.com/posts/2025/07/github-actions-tool-cache/</id>
            <content type="html">
                &lt;h1 id=&quot;installing-tools-within-git-hub-actions-workflows-deep-dive-git-hub-action-tool-cache&quot; tabindex=&quot;-1&quot;&gt;Installing Tools within GitHub Actions Workflows (Deep Dive: GitHub Action Tool Cache)&lt;/h1&gt;
&lt;p&gt;Two of the most common use cases for GitHub Actions are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Running an application&#39;s test suite&lt;/li&gt;
&lt;li&gt;Building an application for release&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both usually require a language runtime and/or build toolchain.&lt;/p&gt;
&lt;p&gt;This article explores how to do so optimally, which requires understanding and using the &lt;a href=&quot;https://github.com/actions/toolkit/blob/main/packages/tool-cache/README.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub Actions Tool Cache&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The code samples from this article can be found at: &lt;a href=&quot;https://github.com/sidpalas/tool-cache-deep-dive&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/tool-cache-deep-dive&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP] Upcoming Course&lt;br /&gt;
This article was written as part of my preparations for the upcoming &amp;quot;GitHub Actions: Beginner to Pro&amp;quot; course I am working on.&lt;/p&gt;
&lt;p&gt;See: &lt;a href=&quot;https://github.com/sidpalas/devops-directive-github-actions-course&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/devops-directive-github-actions-course&lt;/a&gt; for more info!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;aside-ephemeral-vs-stateful-runners&quot; tabindex=&quot;-1&quot;&gt;Aside: Ephemeral vs Stateful Runners&lt;/h2&gt;
&lt;p&gt;In most cases (including default GitHub-hosted runners), the environment where jobs are executed is ephemeral and lives only as long as the job it executes. This has many useful properties related to security, consistency/reproducibility, and scalability, but introduces inefficiencies related to installing dependencies.&lt;/p&gt;
&lt;p&gt;Specifically, any changes to the system (such as installing a tool) are not persisted across jobs. Unless you are intentional about how you install and cache tools, it is easy to end up downloading and installing things with every workflow run, wasting both time and compute resources.&lt;/p&gt;
&lt;p&gt;It is worth mentioning that self-hosted runners &lt;em&gt;can&lt;/em&gt; be stateful and reused across jobs. In that case, a script that performs &lt;code&gt;if not installed, then install&lt;/code&gt; is sufficient. The rest of the article assumes you are using an ephemeral runner.&lt;/p&gt;
&lt;h2 id=&quot;what-is-the-tool-cache&quot; tabindex=&quot;-1&quot;&gt;What is the Tool Cache?&lt;/h2&gt;
&lt;p&gt;The &lt;em&gt;tool cache&lt;/em&gt; is a well-known directory inside every runner where “setup-x” actions (e.g. &lt;a href=&quot;https://github.com/actions/setup-node&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/actions/setup-node&lt;/a&gt;) check for and install versioned tool payloads. There are two official locations:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OS&lt;/th&gt;
&lt;th&gt;Default Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux / macOS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$RUNNER_TOOL_CACHE&lt;/code&gt; → &lt;code&gt;/opt/hostedtoolcache/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%RUNNER_TOOL_CACHE%&lt;/code&gt; → &lt;code&gt;C:&#92;hostedtoolcache&#92;windows&#92;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Within that directory, tools are stored using a directory structure like (using Node.js as an example):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ tree /opt/hostedtoolcache/node -L 2
node/
`-- 20.19.3
    |-- x64 -&amp;gt; /bin/node
    `-- x64.complete # indicates that install is complete and ready for use
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The path to the tool encodes the version and architecture (x64, arm64), and a &lt;code&gt;.complete&lt;/code&gt; file indicates the tool is installed and ready for use.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;br /&gt;
Despite being named &amp;quot;cache&amp;quot;, the tool cache is not automatically shared across jobs (see section about ephemeral runners above).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;actions-tool-cache-npm-package&quot; tabindex=&quot;-1&quot;&gt;@actions/tool-cache npm package&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.npmjs.com/package/@actions/tool-cache&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;@actions/tool-cache npm package&lt;/a&gt; provides utilities for checking if a tool exists within the cache as well as downloading and extracting in platform specific ways.&lt;/p&gt;
&lt;p&gt;For example, to check if a specific version of Node.js exists within the cache you can use the &lt;code&gt;find&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const tc = require(&#39;@actions/tool-cache&#39;);

const nodeDirectory = tc.find(&#39;node&#39;, &#39;12.x&#39;, &#39;x64&#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This package is used by the &lt;a href=&quot;https://github.com/actions?q=setup-&amp;amp;type=all&amp;amp;language=&amp;amp;sort=&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;setup-x&lt;/code&gt; actions&lt;/a&gt; to ensure consistency when setting up necessary tools.&lt;/p&gt;
&lt;h2 id=&quot;how-do-code-actions-setup-x-code-actions-work&quot; tabindex=&quot;-1&quot;&gt;How do &lt;code&gt;actions/setup-x&lt;/code&gt; actions work?&lt;/h2&gt;
&lt;p&gt;The official &lt;code&gt;setup-x&lt;/code&gt; actions generally follow the same pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Look in &lt;code&gt;$RUNNER_TOOL_CACHE/&amp;lt;tool&amp;gt;/&amp;lt;version&amp;gt;/&amp;lt;arch&amp;gt;&lt;/code&gt; for a folder marked &lt;code&gt;.complete&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If present, configure the environment to use it&lt;/li&gt;
&lt;li&gt;If absent, it downloads the tool, unpacks it into that same path, drops the .complete sentinel, and then adds the folder to PATH — seeding the cache for any later steps or jobs that run on the same runner.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;what-are-my-options&quot; tabindex=&quot;-1&quot;&gt;What are my options?&lt;/h2&gt;
&lt;h3 id=&quot;option-0-use-pre-installed-versions-code-setup-x-code-actions&quot; tabindex=&quot;-1&quot;&gt;Option 0: Use pre-installed versions + &lt;code&gt;setup-x&lt;/code&gt; actions&lt;/h3&gt;
&lt;p&gt;Many popular tools come preinstalled on the runner image (see: &lt;a href=&quot;https://github.com/actions/runner-images&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/actions/runner-images&lt;/a&gt;). For example the ubuntu-24.04 image contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Language runtimes:
&lt;ul&gt;
&lt;li&gt;Node.js 20.19.3&lt;/li&gt;
&lt;li&gt;Python 3.12.3&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Popular CLIs:
&lt;ul&gt;
&lt;li&gt;AWS CLI 2.27.50&lt;/li&gt;
&lt;li&gt;GitHub CLI 2.75.0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lots more!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are willing and able to use the preinstalled version, this is ideal! You can generally use one of the official &lt;code&gt;actions/setup-x&lt;/code&gt; actions (e.g. &lt;a href=&quot;https://github.com/actions/setup-node&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/actions/setup-node&lt;/a&gt;) and it will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify that the desired version exists&lt;/li&gt;
&lt;li&gt;Skip the installation step&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is an example job using the preinstalled Node.js version via &lt;code&gt;actions/setup-node&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  preinstalled:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 20 # This will use any major version match (e.g. 20.x.x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/github-actions-tool-cache/preinstalled-cache-hit.png&quot; alt=&quot;preinstalled-cache-hit.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Using the pre-installed version, setup-node executes in just 1 second!&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;option-1-install-with-each-run&quot; tabindex=&quot;-1&quot;&gt;Option 1: Install with each run&lt;/h3&gt;
&lt;p&gt;If you need a different version, you can tell &lt;code&gt;setup-x&lt;/code&gt; to use the desired version, but this will cause the tool to be re-downloaded and re-installed with each run.&lt;/p&gt;
&lt;p&gt;Here is an example job pinned to a different version of Node.js not available in the runner image by default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  older-version:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 20.19.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/github-actions-tool-cache/tool-cache-miss.png&quot; alt=&quot;tool-cache-miss.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;In the event of a cache miss, the action downloads from the upstream release&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;option-2-cache-manually-via-code-actions-cache-code&quot; tabindex=&quot;-1&quot;&gt;Option 2: Cache manually (via &lt;code&gt;actions/cache&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;One way to reuse the tool across ephemeral runners is to cache (and restore from cache) using &lt;code&gt;actions/cache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This avoids needing to download from the upstream release, but downloading from the GitHub actions cache isn&#39;t much faster than downloading the release directly.&lt;/p&gt;
&lt;p&gt;Here is an example job caching and restoring a non-default version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;  older-version-gha-cache:
    runs-on: ubuntu-24.04
    env:
      NODE_DIR: /opt/hostedtoolcache/node/20.19.2/
    steps:
      - name: Restore Node from cache
        id: cache-task
        uses: actions/cache@v4
        with:
          path: ${{ env.NODE_DIR }}
          key: node-20.19.2-${{ runner.os }}-${{ runner.arch }}
      - uses: actions/setup-node@v4
        with:
          node-version: 20.19.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/github-actions-tool-cache/gha-cache.png&quot; alt=&quot;gha-cache.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Restoring from the GitHub cache takes approximately as long as downloading the release directly&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;option-3-use-another-caching-mechanism-e-g-namespace-cache-volumes-toolchain-caching&quot; tabindex=&quot;-1&quot;&gt;Option 3: Use another caching mechanism (e.g. Namespace Cache Volumes + toolchain caching)&lt;/h3&gt;
&lt;p&gt;If GitHub Action caching isn&#39;t much faster than the naive &amp;quot;reinstall every run&amp;quot; approach, is there a faster caching option we can use?&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://namespace.so/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Namespace Labs&lt;/a&gt; provides one such option with their &lt;a href=&quot;https://namespace.so/docs/architecture/storage/cache-volumes&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;cache volumes&lt;/a&gt;. These are built on NVMe attached to their hosted runners which you can configure for their hosted runners.&lt;/p&gt;
&lt;p&gt;One option when configuring these cache volumes is whether or not to cache toolchain downloads. When enabled, Namespace will automatically cache tools stored according to the tool cache conventions for use in future runs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/github-actions-tool-cache/cache-volumes.png&quot; alt=&quot;cache-volumes.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;On the GitHub Actions workflow side, the only necessary change is to use the corresponding runner name in the &lt;code&gt;runs-on&lt;/code&gt; field for the job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  older-version-namespace-cache-volume:
    runs-on: namespace-profile-toolchain-volume-cache
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 20.19.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/github-actions-tool-cache/cache-volume-cache-hit.png&quot; alt=&quot;cache-volume-cache-hit.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] This post is not sponsored&lt;br /&gt;
Namespace Labs is sponsoring my upcoming &lt;a href=&quot;https://courses.devopsdirective.com/github-actions-beginner-to-pro&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub Actions course&lt;/a&gt;, but this post is not sponsored.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;option-4-use-a-custom-container-image-or-ami&quot; tabindex=&quot;-1&quot;&gt;Option 4: Use a custom Container Image or AMI&lt;/h3&gt;
&lt;p&gt;If you want to avoid dealing with &lt;code&gt;setup-x&lt;/code&gt; altogether, you can run your jobs inside of a container or virtual machine preconfigured with the desired tool version(s).&lt;/p&gt;
&lt;h4 id=&quot;container&quot; tabindex=&quot;-1&quot;&gt;Container&lt;/h4&gt;
&lt;p&gt;GitHub Actions provides the option to run your jobs inside of a container with an image of your choosing.&lt;/p&gt;
&lt;p&gt;One downside to this approach is that running the container introduces its own initialization overhead.&lt;/p&gt;
&lt;p&gt;Here is an example job using this option:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;jobs:
  older-version-container:
    runs-on: ubuntu-24.04
    container:
      image: node:20.19.2-alpine
    steps:
      - run: |
          which node
          node -v

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2025/07/github-actions-tool-cache/container-image-bypass-setup-node.png&quot; alt=&quot;container-image-bypass-setup-node.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE] Self-hosted runners with ARC&lt;br /&gt;
If you are self-hosting runners via the &lt;a href=&quot;https://github.com/actions/actions-runner-controller&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;actions-runner-controller&lt;/a&gt; you could similarly use a container image preconfigured with the desired tools.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;virtual-machine&quot; tabindex=&quot;-1&quot;&gt;Virtual Machine&lt;/h4&gt;
&lt;p&gt;If you are self-hosting runners as virtual machines, such as with &lt;a href=&quot;https://runs-on.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;runs-on&lt;/a&gt;, you can build the virtual machine image (e.g. AMI if on AWS) with the desired tools using a tool like &lt;a href=&quot;https://developer.hashicorp.com/packer&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Packer&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;option-5-do-everything-within-the-dockerfile&quot; tabindex=&quot;-1&quot;&gt;Option 5: Do everything within the Dockerfile&lt;/h3&gt;
&lt;p&gt;Similar to using a custom image for the runner, we can bypass the need to use the tool-cache entirely by performing all of the necessary build/test actions within the container build process.&lt;/p&gt;
&lt;p&gt;This can be accomplished via a multi-stage build, with specific build targets which do things like execute the tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;# Dockerfile
FROM node:20.19.2-bookworm-slim AS base
WORKDIR /usr/src/app
COPY package*.json ./

###################################
FROM base AS which-and-version
RUN which node &amp;amp;&amp;amp; node -v

###################################
FROM base AS test
RUN npm ci 
COPY ./src/ .
ARG FAIL_OR_SUCCEED
RUN npm run test-${FAIL_OR_SUCCEED}

###################################
FROM base AS deploy
ENV NODE_ENV=production
RUN npm ci --production
USER node
COPY --chown=node:node ./src/ .
EXPOSE 3000
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;tool-cache-deep-dive&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;1.0.0&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;homepage&amp;quot;: &amp;quot;https://github.com/sidpalas/tool-cache-deep-dive#readme&amp;quot;,
  &amp;quot;bugs&amp;quot;: {
    &amp;quot;url&amp;quot;: &amp;quot;https://github.com/sidpalas/tool-cache-deep-dive/issues&amp;quot;
  },
  &amp;quot;repository&amp;quot;: {
    &amp;quot;type&amp;quot;: &amp;quot;git&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;git+https://github.com/sidpalas/tool-cache-deep-dive.git&amp;quot;
  },
  &amp;quot;license&amp;quot;: &amp;quot;ISC&amp;quot;,
  &amp;quot;author&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;commonjs&amp;quot;,
  &amp;quot;main&amp;quot;: &amp;quot;src/index.js&amp;quot;,
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;test-succeed&amp;quot;: &amp;quot;echo &#92;&amp;quot;This mimics a passing test suite... ✅&#92;&amp;quot; &amp;amp;&amp;amp; exit 0;&amp;quot;,
    &amp;quot;test-fail&amp;quot;: &amp;quot;echo &#92;&amp;quot;This mimics a failing test suite... ❌&#92;&amp;quot; &amp;amp;&amp;amp; exit 1;&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;noop&amp;quot;: &amp;quot;^1.0.1&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full example can be found at &lt;a href=&quot;https://github.com/sidpalas/tool-cache-deep-dive&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/tool-cache-deep-dive&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By targeting the &lt;code&gt;test&lt;/code&gt; stage we can then execute our tests within the container. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  tool-cache-deep-dive git:(sp/initial-workflow-development) docker build --build-arg FAIL_OR_SUCCEED=fail . --target=test                                   ...
 =&amp;gt; ERROR [test 3/3] RUN npm run test-fail
------                                                                           
 &amp;gt; [test 3/3] RUN npm run test-fail:                                             
0.168 
0.168 &amp;gt; tool-cache-deep-dive@1.0.0 test-fail
0.168 &amp;gt; echo &amp;quot;This mimics a failing test suite... ❌&amp;quot; &amp;amp;&amp;amp; exit 1;
0.168 
0.170 This mimics a failing test suite... ❌
------
Dockerfile:14
--------------------
  12 |     COPY ./src/ .
  13 |     ARG FAIL_OR_SUCCEED
  14 | &amp;gt;&amp;gt;&amp;gt; RUN npm run test-${FAIL_OR_SUCCEED}
  15 |     
  16 |     ###################################
--------------------
ERROR: failed to build: failed to solve: process &amp;quot;/bin/sh -c npm run test-${FAIL_OR_SUCCEED}&amp;quot; did not complete successfully: exit code: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;closing-thoughts&quot; tabindex=&quot;-1&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;I wrote this article mostly because I found the &lt;a href=&quot;https://github.com/actions/toolkit/blob/main/packages/tool-cache/README.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;existing documentation&lt;/a&gt; around the GitHub Actions tool cache to be lacking. Hopefully this gives you ideas for how to install tools required by your GitHub Actions workflows without incurring a significant time penalty.&lt;/p&gt;
&lt;p&gt;Whether sophisticated caching is “worth it” comes down to the trade-off between complexity and time saved.  Heavy toolchains (e.g. large Java distributions) can add significant time to every job. Every fresh download also introduces another external dependency: if the upstream mirror hiccups, your entire pipeline stalls.&lt;/p&gt;
&lt;p&gt;I would generally take the following approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If the runner contains an acceptable version preinstalled, use it!&lt;/li&gt;
&lt;li&gt;If you must pin a different version and you control self-hosted runners, a persistent cache volume (e.g. Namespace Labs cache volumes, ARC PVC, or similar) gives you the same performance with almost no extra YAML.&lt;/li&gt;
&lt;li&gt;When you’re already packaging and deploying containers, baking the runtime into the image keeps all dependencies self-contained and guarantees reproducibility across every environment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Start with the lightest solution, determine how long it takes, and add caching techniques when the saved time outweigh the added complexity.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2025/07/github-actions-tool-cache/" />
        </entry>
        <entry>
            <title>
                hugo-and-caddy-on-gcp
                
            </title>
            <updated>2025-07-10T14:02:33.204Z</updated>
            <id>https://devopsdirective.com/posts/2020/02/hugo-and-caddy-on-gcp/</id>
            <content type="html">
                &lt;h1 id=&quot;the-making-of-this-site-hugo-caddy-gcp&quot; tabindex=&quot;-1&quot;&gt;The Making of This Site (Hugo, Caddy, + GCP)&lt;/h1&gt;
&lt;p&gt;Alternate title: &lt;strong&gt;Just use &lt;a href=&quot;https://www.netlify.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Netlify&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Deploy a Hugo site to GCP for free in less time than it will take you to read this article (&lt;a href=&quot;https://github.com/sidpalas/hugo-gcp-deploy&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Github Repo&lt;/a&gt; with all referenced commands)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/02/hugo-and-caddy-on-gcp/hugo-caddy-gcp.png&quot; alt=&quot;hugo-caddy-gcp.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;EDITS:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In October 2020 I &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2020/10/gcs-cloudflare-hosting/&quot;&gt;updated the hosting of this site to use GCS + Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;In July 2025 I &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/07/site-revamp-3-0/&quot;&gt;updated the site to use Obsidian + Eleventy + Vercel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#choosing-a-site-generator&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Choosing a Site Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#initial-setup&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Initial Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#choosing-a-hosting-solution&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Choosing a Hosting Solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#local-configuration-http-only-for-now&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Local Configuration (HTTP-only for Now)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#deploying-to-gcp&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Deploying to GCP&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1-enable-billing-for-the-project&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1) Enable Billing for the Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2-enable-the-compute-engine-and-container-registry-apis&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2) Enable the Compute Engine and Container Registry APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3-reserve-a-static-ip-address&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3) Reserve a static IP address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4-add-firewall-rules&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4) Add firewall rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#5-create-the-virtual-machine-finally&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;5) Create the Virtual Machine (Finally!)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#6-configure-dockers&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;6) Configure Docker(s)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#7-deploy&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;7) Deploy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#8-were-live&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;8) We&#39;re Live!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#9-configuring-dns&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;9) Configuring DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#10-enabling-https&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;10) Enabling HTTPS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#closing-thoughts&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;choosing-a-site-generator&quot; tabindex=&quot;-1&quot;&gt;Choosing a Site Generator&lt;/h2&gt;
&lt;p&gt;With GeoCities no longer a viable option for hosting websites in 2020 (apparently Yahoo Japan shut down the final remnants in &lt;a href=&quot;https://www.cnet.com/news/geocities-dies-in-march-2019-and-with-it-a-piece-of-internet-history/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;March 2019&lt;/a&gt;) I needed a more modern solution to host this site.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/02/hugo-and-caddy-on-gcp/geocities.jpg&quot; alt=&quot;geocities.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After a brief look at the leading static site generators Jekyll, Hugo, Next.js, Gatsby, etc... I came to the conclusion that almost any of these would work just fine for my needs. I ended up choosing Hugo for two main reasons:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) Language:&lt;/strong&gt; It&#39;s written in Go, a language I have wanted to learn. Having this site with Hugo may provide a nudge of motivation to make some contribution to the Hugo project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Speed:&lt;/strong&gt; Site build times are near instant for this (tiny) site, but that would be true for any generator with so few pages. However, people have performed a number of &lt;a href=&quot;https://forestry.io/blog/hugo-vs-jekyll-benchmark/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;benchmarks&lt;/a&gt; showing Hugo&#39;s performance as a site grows.&lt;/p&gt;
&lt;h2 id=&quot;initial-setup&quot; tabindex=&quot;-1&quot;&gt;Initial Setup&lt;/h2&gt;
&lt;p&gt;The Hugo documentation is concise and they have an easy-to-follow quick start guide found here: &lt;a href=&quot;https://gohugo.io/getting-started/quick-start/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://gohugo.io/getting-started/quick-start/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&#39;m currently (as of February 2020) still using the suggested &lt;a href=&quot;https://themes.gohugo.io/gohugo-theme-ananke/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Ananke&lt;/a&gt; theme with a few minor styling tweaks, but eventually will probably spend some more time customizing the theme.&lt;/p&gt;
&lt;h2 id=&quot;choosing-a-hosting-solution&quot; tabindex=&quot;-1&quot;&gt;Choosing a Hosting Solution&lt;/h2&gt;
&lt;p&gt;With the site generator working, I then needed to decide how to host the site. In the past, I have used Github Pages to host static sites, but I noticed that they explicitly prohibit &lt;strong&gt;&amp;quot;Get-rich-quick schemes&amp;quot;&lt;/strong&gt; (which learning &amp;amp; writing about DevOps and Cloud infrastructure is) so that was out of the running.&lt;/p&gt;
&lt;p&gt;I also looked at hosting within an &lt;a href=&quot;https://aws.amazon.com/s3/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;AWS&lt;/a&gt; / &lt;a href=&quot;https://cloud.google.com/storage&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GCP&lt;/a&gt;](&lt;a href=&quot;https://cloud.google.com/storage&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://cloud.google.com/storage&lt;/a&gt;) / &lt;a href=&quot;https://azure.microsoft.com/en-us/services/storage/blobs/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Azure&lt;/a&gt; bucket. These are all super easy to set up and scale effortlessly, but if you want to use a domain with HTTPS enabled, you end up having to jump through some hoops configuring a Content Delivery Network.&lt;/p&gt;
&lt;p&gt;I then came across &lt;a href=&quot;https://caddyserver.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Caddy&lt;/a&gt;, a web server with automatic HTTPS configuring using &lt;a href=&quot;https://letsencrypt.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Let&#39;s Encrypt&lt;/a&gt; which seemed ideal for this use case!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/02/hugo-and-caddy-on-gcp/caddy.png&quot; alt=&quot;caddy.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;With a lock for a logo, it must be secure!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you are following along and setting up your own site, for most people, the best option at this point would be to stop here and go to &lt;a href=&quot;https://www.netlify.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://www.netlify.com/&lt;/a&gt;. They have a generous free tier plan and offer direct integration with github/gitlab/bitbucket to handle automatic build/deploys triggered by Git commits. I achieved a similar end result with GCP Compute Engine + Cloud Build (a topic for another post) that provides me a bit more control/extensibility, but Netlify covers most use cases with a fantastic user experience!&lt;/p&gt;
&lt;h2 id=&quot;local-configuration-http-only-for-now&quot; tabindex=&quot;-1&quot;&gt;Local Configuration (HTTP-only for Now)&lt;/h2&gt;
&lt;p&gt;Since my computer is running macOS, but ultimately the site would be deployed on a server running some variant of Linux, my default is to use containers to eliminate any configuration headaches with slight differences between the two environments.&lt;/p&gt;
&lt;p&gt;This appears to be the defacto standard Caddy docker image &lt;a href=&quot;https://hub.docker.com/r/abiosoft/caddy&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://hub.docker.com/r/abiosoft/caddy&lt;/a&gt; with over 10M pulls so I used that as the base.&lt;/p&gt;
&lt;p&gt;There are two options for accessing the files that Caddy needs to host within the container. (1) Copy them into the container image OR (2) mount a location the host filesystem into the container with &lt;code&gt;-v&lt;/code&gt; or &lt;code&gt;--mount&lt;/code&gt; &lt;a href=&quot;https://docs.docker.com/storage/bind-mounts/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;docker flags&lt;/a&gt; when running the container.&lt;/p&gt;
&lt;p&gt;Either option would be fine, but option 1 is nice for a small site because it ensures the entire site and its dependencies are included within the container image. As the site grows, I may switch to storing the site files outside of the container image.&lt;/p&gt;
&lt;p&gt;Bundling the site files into the container can be accomplished with a 2 line Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;FROM abiosoft/caddy:1.0.3
COPY ./public /srv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here &lt;code&gt;./public&lt;/code&gt; is the local directory where Hugo builds the site, and &lt;code&gt;/srv&lt;/code&gt; is the directory within the container Caddy expects to find the files it is serving. The following 4 commands will build the site, build the container, and run the container:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export IMAGE_NAME=my-hugo-caddy-docker-image
hugo -D    # -D flag tells Hugo to build drafts 
docker build ./ --tag $IMAGE_NAME
docker run -p 2015:2015 $IMAGE_NAME     
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; forwards the port from the host system into the container allowing us to connect to &lt;a href=&quot;http://localhost:2015/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;http://localhost:2015/&lt;/a&gt; and that request will be forwarded into the container on port 2015 where Caddy is listening.&lt;/p&gt;
&lt;h2 id=&quot;deploying-to-gcp&quot; tabindex=&quot;-1&quot;&gt;Deploying to GCP&lt;/h2&gt;
&lt;p&gt;With the container image working, I was then ready to deploy it somewhere. There are a variety of options to do this, but I chose to use a GCP Compute Engine &lt;code&gt;f1-micro&lt;/code&gt; virtual machine instance running Google&#39;s &lt;a href=&quot;https://cloud.google.com/container-optimized-os/docs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Container-Optimized OS&lt;/a&gt;. Container-Optimized OS provides nice &lt;a href=&quot;https://cloud.google.com/container-optimized-os/docs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;security features&lt;/a&gt; configured by default making it a good OS option for containerized applications. While the &lt;code&gt;f1-micro&lt;/code&gt; instance is small (0.2 vCPUs + 600MB Memory), running one is included in the GCP&#39;s &lt;a href=&quot;https://cloud.google.com/free/docs/gcp-free-tier#always-free-usage-limits&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;always free usage limits&lt;/a&gt; making this deployment cost me a &lt;em&gt;grand total of $0!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since this site is about DevOps, I clearly needed to automate the entire setup process, which I did here: &lt;a href=&quot;https://github.com/sidpalas/hugo-gcp-deploy&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/sidpalas/hugo-gcp-deploy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also decided to create a new GCP project for this site. Doing this makes it easy to clean things up should I decide to take the site down by simply deleting the entire project without having to worry about accidentally leaving some resources running.&lt;/p&gt;
&lt;p&gt;If you are more comfortable working with the GCP web interface, that is perfectly fine, but the following process should do the trick&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; for any of the following commands &lt;code&gt;$PROJECT_ID&lt;/code&gt; would need to be replaced with your GCP project id. I also like to explicitly pass the project ID into the commands to ensure they are executed in the correct location (Just in case I happened to have changed my default project configuration).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PROJECT_ID=my-awesome-project-1234
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1-enable-billing-for-the-project&quot; tabindex=&quot;-1&quot;&gt;1) Enable Billing for the Project&lt;/h3&gt;
&lt;p&gt;Even though the resources used here are included in the free tier, Google requires having a payment method on file. This is the one step I recommend doing via the console as the command line command is &lt;a href=&quot;https://cloud.google.com/sdk/gcloud/reference/alpha/billing&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;still in alpha&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-enable-the-compute-engine-and-container-registry-ap-is&quot; tabindex=&quot;-1&quot;&gt;2) Enable the Compute Engine and Container Registry APIs&lt;/h3&gt;
&lt;p&gt;This is necessary to provision the VM and to be able to push container images.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud services enable compute.googleapis.com --project=$PROJECT_ID
gcloud services enable containerregistry.googleapis.com --project=$PROJECT_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-reserve-a-static-ip-address&quot; tabindex=&quot;-1&quot;&gt;3) Reserve a static IP address&lt;/h3&gt;
&lt;p&gt;If the VM needs to restart (or if I wanted to move to a larger machine type), a static IP ensures it won&#39;t change unexpectedly and mess up DNS configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute addresses create my-site-external-ip &#92;
        --project=$PROJECT_ID &#92;
        --region=us-central1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-add-firewall-rules&quot; tabindex=&quot;-1&quot;&gt;4) Add firewall rules&lt;/h3&gt;
&lt;p&gt;By default, Compute Engine VMs do not allow HTTP or HTTPS traffic. Adding firewall rules allow those requests to make it to the web server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute firewall-rules create default-allow-http &#92;
        --project=$PROJECT_ID &#92;
        --target-tags=http-server &#92;
        --allow tcp:80
gcloud compute firewall-rules create default-allow-https &#92;
        --project=$PROJECT_ID &#92;
        --target-tags=https-server &#92;
        --allow tcp:443
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The target-tags allow the VM configuration to utilize these rules.&lt;/p&gt;
&lt;h3 id=&quot;5-create-the-virtual-machine-finally&quot; tabindex=&quot;-1&quot;&gt;5) Create the Virtual Machine (Finally!)&lt;/h3&gt;
&lt;p&gt;When creating the VM, the static IP and firewall rule tags are used to configure it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute instances create my-f1-micro-instance &#92;
        --project=$PROJECT_ID &#92;
        --zone=us-central1-a &#92;
        --machine-type=f1-micro &#92;
        --image=projects/cos-cloud/global/images/cos-69-10895-385-0 &#92;
        --address=my-site-external-ip &#92;
        --tags=http-server,https-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes a few minutes to provision.&lt;/p&gt;
&lt;h3 id=&quot;6-configure-docker-s&quot; tabindex=&quot;-1&quot;&gt;6) Configure Docker(s)&lt;/h3&gt;
&lt;p&gt;In order to configure my local Docker install to push images to Google Container Registry I had to run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud auth configure-docker    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To set up Docker and the GCR credentials within in container optimized OS running on the VM I had to run the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute ssh my-f1-micro-instance &#92;
        --project=$PROJECT_ID &#92;
        --zone=us-central1-a -- &#92;
        docker-credential-gcr configure-docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;7-deploy&quot; tabindex=&quot;-1&quot;&gt;7) Deploy&lt;/h3&gt;
&lt;p&gt;Before deploying the site, I had to add a Caddyfile to the container image to configure the server. The default configuration will serve the site on port 2015 which is inaccessible on our VM (because my firewall rules only allow traffic on ports 80 and 443). The following will tell Caddy to serve on port 80 and accept requests from any domain (&lt;a href=&quot;https://caddyserver.com/v1/docs/http-caddyfile&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;documentation&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# This test configuration will be replaced later when setting up HTTPS
:80 {
browse
log stdout
errors stdout
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also updated the Dockerfile to copy this Caddyfile into the container image:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;FROM abiosoft/caddy:1.0.3
COPY ./public /srv

# This environment variable gets used later during the container start up
# to accept the Let&#39;s Encrypt subscriber agreement (without requiring user input)
ENV ACME_AGREE=true 
COPY ./Caddyfile /etc/Caddyfile 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the configuration ready, I needed to get the container image into the google container registry by building, tagging, and then pushing it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export IMAGE_NAME=my-hugo-caddy-docker-image
export IMAGE_TAG=incrementing-tag-001 # change this with each deploy to ensure latest image is used
docker build ./ --tag $IMAGE_NAME
docker tag $IMAGE_NAME gcr.io/$PROJECT_ID/$IMAGE_NAME:$IMAGE_TAG
docker push gcr.io/$PROJECT_ID/$IMAGE_NAME:$IMAGE_TAG
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the site is already running, it needs to be cleaned up before the new container can be deployed or else it would fail when trying to bind to the same host ports. This can be accomplished using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute ssh my-f1-micro-instance &#92;
        --project=$PROJECT_ID &#92;
        --zone=us-central1-a -- &#92;
        &#39;docker container stop $(docker container ls -aq) &amp;amp;&amp;amp; docker container rm $(docker container ls -aq)&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having to do this step is one downside to bundling the entire site into the container image, the site has to be down for a second or two while the new version starts up. If the site was mounted into the container, the server could keep running and the files could be copied onto the host filesystem with no downtime.&lt;/p&gt;
&lt;p&gt;Finally, we can issue a &lt;code&gt;docker run&lt;/code&gt; to run the new container image:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud compute ssh my-f1-micro-instance &#92;
        --project=$PROJECT_ID &#92;
        --zone=us-central1-a -- &#92;
        &#39;docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -v $HOME/.caddy:/root/.caddy gcr.io/$PROJECT_ID/$IMAGE_NAME:$IMAGE_TAG&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; the &lt;code&gt;-v $HOME/.caddy:/root/.caddy&lt;/code&gt; mount isn&#39;t necessary here, but later once we request the TLS certificate, this will allow the certificate files to persist across container redeploys, avoiding extra requests to Let&#39;s Encrypt which could lead to being rate limited.&lt;/p&gt;
&lt;h3 id=&quot;8-we-re-live&quot; tabindex=&quot;-1&quot;&gt;8) We&#39;re Live!&lt;/h3&gt;
&lt;p&gt;At this point, I was able to visit the external IP address from step 3 and see the website.&lt;/p&gt;
&lt;p&gt;I even scripted the whole process so that it takes less than 5 minutes:&lt;/p&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/hxTnB_FZpw8?si=8ATl3hSc8NQ__Ayr&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
### 9) Configuring DNS
&lt;p&gt;The final element of the setup is to point a domain to the IP address which I accomplished with the following settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Name      Type   TTL     Data
@         A      1h      123.456.78.90
www       CNAME  1h      my-awesome-domain.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The CNAME record maps the www subdomain to the primary domain without www)&lt;/p&gt;
&lt;h3 id=&quot;10-enabling-https&quot; tabindex=&quot;-1&quot;&gt;10) Enabling HTTPS&lt;/h3&gt;
&lt;p&gt;The final element of the setup is to enable HTTPS within Caddy. This can be accomplished by modifying the &lt;code&gt;Caddyfile&lt;/code&gt; to include the domains and an email address for the TLS configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;my-awesome-domain.com www.my-awesome-domain.com {
        tls my-email-address@domain.com
        browse
        log stdout
        errors stdout
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After redeploying and waiting for the DNS settings to propagate I was able to access my site and bask in the glory of the HTTPS connection symbol!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/02/hugo-and-caddy-on-gcp/https-success.png&quot; alt=&quot;https-success.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot; tabindex=&quot;-1&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Overall I&#39;m happy with this configuration and am amazed that all of this can be accomplished for free using mostly open-source software! It was also useful to continue gaining experience with the tools and platforms.&lt;/p&gt;
&lt;p&gt;My hope is now that everything is set up and configured, the amount of maintenance effort required should be low. Time will tell...&lt;/p&gt;
&lt;p&gt;In a future post, I will cover how I used Google Cloud Build to automate the deployment process. I also plan to do some benchmarking to see just what kind of load this tiny server can handle!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/02/hugo-and-caddy-on-gcp/" />
        </entry>
        <entry>
            <title>
                Home
                
            </title>
            <updated>2026-03-09T15:41:27.595Z</updated>
            <id>https://devopsdirective.com/</id>
            <content type="html">
                &lt;h1 id=&quot;dev-ops-directive&quot; tabindex=&quot;-1&quot;&gt;DevOps Directive&lt;/h1&gt;
&lt;h2 id=&quot;a-place-for-dev-ops-and-cloud-infrastructure-content&quot; tabindex=&quot;-1&quot;&gt;⚙️ A place for DevOps and cloud infrastructure content!&lt;/h2&gt;
&lt;h3 id=&quot;a-handful-of-articles-to-start-with&quot; tabindex=&quot;-1&quot;&gt;📚 A handful of articles to start with:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2026/03/forking-the-planetscale-terraform-provider/&quot;&gt;I forked the PlanetScale Terraform Provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2026/01/rendered-manifests-pattern/&quot;&gt;The Rendered Manifests Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/08/github-actions-required-checks-for-conditional-jobs/&quot;&gt;GitHub Actions Required Checks for Conditional Jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/08/advanced-github-actions-matrix/&quot;&gt;Advanced Usage of GitHub Actions Matrix Strategy (Dynamic Fan Out)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/07/organizing-terraform-configurations/&quot;&gt;Organizing Terraform Configurations (Single-Instance vs. Multi-Instance Root Modules)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/07/github-actions-tool-cache/&quot;&gt;Installing Tools within GitHub Actions Workflows (Deep Dive: GitHub Action Tool Cache)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2025/07/site-revamp-3-0/&quot;&gt;Yet Another Site Revamp (devopsdirective․com v3.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2023/11/velero-aws-account-migration/&quot;&gt;Velero AWS Account Migration (Case Study)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2023/06/container-init-process/&quot;&gt;Container Init Process&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2022/10/kubernetes-maintenance-page/&quot;&gt;Kubernetes Maintenance Page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2022/04/cloud-control-spectrum/&quot;&gt;Cloud Control Spectrum: How Should I Deploy My Web Application?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://devopsdirective.com/posts/2022/04/stages-of-internal-development-systems/&quot;&gt;The Six Stages of Internal Development Platforms&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔎 The full archive of articles can be found using the left nav or search functionality!&lt;/p&gt;
&lt;h2 id=&quot;want-more-check-out&quot; tabindex=&quot;-1&quot;&gt;👀 Want more? Check out:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/@DevOpsDirective&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/sidpalas&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/in/sid-palas/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sidpalas&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://courses.devopsdirective.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Course Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://devopsdirective.com/" />
        </entry>
        <entry>
            <title>
                velero-aws-account-migration
                
            </title>
            <updated>2025-07-09T02:30:21.153Z</updated>
            <id>https://devopsdirective.com/posts/2023/11/velero-aws-account-migration/</id>
            <content type="html">
                &lt;h1 id=&quot;velero-aws-account-migration-case-study&quot; tabindex=&quot;-1&quot;&gt;Velero AWS Account Migration [Case Study]&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I recently helped an organization migrate a set of applications deployed in Kubernetes (EKS) across AWS accounts using &lt;a href=&quot;https://velero.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Velero&lt;/a&gt;. This case study describes that process.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/11/velero-aws-account-migration/account-structure.png&quot; alt=&quot;account-structure.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Please start with multiple AWS accounts so you don&#39;t have to hire someone like me to help with this! 🙏&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#summary&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Summary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#background&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Background&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#primary-goals&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Primary Goals&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#migration-approach&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Migration Approach&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#migrating-non-kubernetes-resources&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Migrating Non-Kubernetes Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#migrating-kubernetes-resources&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Migrating Kubernetes Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#migration-sequence&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Migration Sequence&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#lessons-learned-during-the-migrations&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Lessons Learned during the Migrations&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#velero-doesnt-back-up-cluster-scoped-resources-for-namespace-backups&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Velero doesn&#39;t back up cluster-scoped resources for namespace backups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#velero-backs-up-pods-by-default&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Velero backs up Pods by default&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#conclusion&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;The migration of Kubernetes clusters across different AWS accounts can be a challenging process, given the intricacies and dependencies that exist within each environment.&lt;/p&gt;
&lt;p&gt;I recently used &lt;a href=&quot;https://velero.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Velero&lt;/a&gt; to help an engineering organization migrate two EKS clusters (representing their staging and production environments) from a single AWS account to two separate accounts. I’ll delve into the migration process, share our experiences, and outline some lessons learned along the way.&lt;/p&gt;
&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;Each environment consisted of a variety of AWS resources (e.g. S3 buckets, RDS instances), some externally hosted databases (MongoDB Atlas, Neo4j Cloud), and a variety of microservices running in an EKS cluster.&lt;/p&gt;
&lt;p&gt;These microservices can be categorized into the following groups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Job Producers:&lt;/strong&gt; Detecting or receiving events from data vendors about new data to be processed or initiating periodic analytics jobs. These create messages containing the necessary metadata and send them to the Message Bus&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message Bus:&lt;/strong&gt; RabbitMQ cluster serving as a message bus for the system&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job Consumers:&lt;/strong&gt; Worker deployments that consume messages from the Message bus and perform the corresponding processing task (storing the associated data in S3 and/or databases)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Other:&lt;/strong&gt; Public facing web portal (nginx), various APIs, etc… These services were generally stateless and the primary consideration with respect to the migration is ensuring their configuration (loaded from ConfigMaps/Secrets were updated appropriately when started in the new clusters)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The applications were deployed into the cluster automatically using a pushed-based approach from GitLab CI/CD pipelines.&lt;/p&gt;
&lt;h3 id=&quot;primary-goals&quot; tabindex=&quot;-1&quot;&gt;Primary Goals&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Migrate clusters to new accounts&lt;/li&gt;
&lt;li&gt;Minimize customer-facing downtime&lt;/li&gt;
&lt;li&gt;Avoid data loss or duplication&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;migration-approach&quot; tabindex=&quot;-1&quot;&gt;Migration Approach&lt;/h2&gt;
&lt;h3 id=&quot;migrating-non-kubernetes-resources&quot; tabindex=&quot;-1&quot;&gt;Migrating Non-Kubernetes Resources&lt;/h3&gt;
&lt;p&gt;This case study is focused on using Velero for the Kubernetes workloads, but a brief description of the migration of other resources is useful context. Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud Development Kit (CDK) configurations were built to provision copies of all the AWS resources in the new accounts.&lt;/li&gt;
&lt;li&gt;S3 replication was used to copy data (historical and ongoing) to new buckets.&lt;/li&gt;
&lt;li&gt;AWS DMS was used to copy data (historical and ongoing) to new RDS instances.&lt;/li&gt;
&lt;li&gt;External databases (MongoDB Atlas + Neo4j Cloud) were left in place and VPC peering was used to enable connectivity from VPCs in the new accounts. &lt;strong&gt;Note:&lt;/strong&gt; This was simpler than migrating to new instances but did introduce the potential for legacy and new clusters to be attempting to write to them duplicate data to them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;migrating-kubernetes-resources&quot; tabindex=&quot;-1&quot;&gt;Migrating Kubernetes Resources&lt;/h3&gt;
&lt;p&gt;We considered three methods for migrating the Kubernetes-based workloads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Redeploy everything from GitLab&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Deployments were automated via GitLab CI/CD, so one option would be to update the GitLab environment to point to the new cluster and trigger the latest deployment pipeline for each service. This is a simple approach and doesn’t require any new tooling, but there are many repositories, and not 100% of the resources in the cluster was deployed in this way.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backup/restore with Velero&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Another option would be to take a Velero backup in the legacy cluster and then restore that backup in the new cluster. Cluster migration is one of the stated use cases for Velero, but it does represent another tool to learn/install/configure if you are not already using it for backups.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use a GitOps Approach&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using a tool like ArgoCD or FluxCD installed into the would provide a clean mechanism to pull the latest configuration for all of the applications from GitLab into the new cluster. This would require rolling out GitOps before performing the migration and would not work for any workloads not fully described in GitLab.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We decided to use Velero as it provided the simplest approach that would work even for resources that weren’t fully represented in GitLab or if we temporarily needed certain resources to differ from Git. For example ConfigMaps with references to S3 buckets or secrets containing RDS credentials. Also, we had recently set up Velero in the existing clusters as a backup solution, so the team was already familiar with its usage.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/11/velero-aws-account-migration/s3-config.png&quot; alt=&quot;s3-config.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;S3 configuration for replicating backups across accounts&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Note: We did consider migrating applications one at a time, but this would require adding significant complexity in temporary configurations to support cross-AWS account IAM and networking and we decided to move everything at once to avoid this and prevent a potentially long period where the system lived in a partially migrated state.&lt;/p&gt;
&lt;h3 id=&quot;migration-sequence&quot; tabindex=&quot;-1&quot;&gt;Migration Sequence&lt;/h3&gt;
&lt;p&gt;The following is the sequence we used for the migration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Velero into both clusters:&lt;/strong&gt; Each bucket used a separate S3 bucket located in the AWS account where the cluster lived.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set up S3 replication rule to copy backups between buckets:&lt;/strong&gt; This is one place our process differs from the described Velero migration documentation because the clusters live in different AWS accounts. Rather than set up the necessary IAM for the new cluster to access the legacy bucket, we opted to replicate the backup data from the legacy bucket into the new one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scale down “Job Producers” in legacy cluster (This is highly specific to our application and migration plan):&lt;/strong&gt; In order to avoid the potential for data loss and duplication we decided that the best approach would be to scale the job producers to zero and allow time for the job consumers to finish processing active jobs. Luckily, these data processing jobs are not directly visible to end users and therefore can be disabled temporarily without incurring customer-facing downtime.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trigger backup in legacy cluster:&lt;/strong&gt; Nothing special here, we created a backup from a schedule.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deploy modified ConfigMaps and Secrets to new cluster:&lt;/strong&gt; Many of the application consume their configuration from ConfigMaps and Secrets. When running in the new cluster they needed to use the new S3 buckets, RDS instances, etc. Velero by default does not overwrite existing Kubernetes objects, so pre-populating these resources would ensure they have the appropriate configuration when the corresponding applications are restored. We had open Merge Requests (or modifications to GitLab Secrets) containing all of these differences ready to Merge after the migration was complete to avoid reverting with the next deployment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Restore from backup in new cluster:&lt;/strong&gt; Again nothing special, we restored from the backup.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scale up “Job Producers” in new cluster:&lt;/strong&gt; Scaling the job producers back to their normal replica counts and cron schedules enables them to resume where they left off in the legacy cluster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Validate workload performance:&lt;/strong&gt; We defined a number of checks to determine that critical workloads were healthy in the new cluster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cut over DNS for external traffic:&lt;/strong&gt; Once we were satisfied that things were behaving as expected, we cut over external DNS for the public apps/APIs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update GitLab to deploy to new cluster:&lt;/strong&gt; We updated the GitLab environment such that future deploys would target the new cluster instead of the legacy one.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;lessons-learned-during-the-migrations&quot; tabindex=&quot;-1&quot;&gt;Lessons Learned during the Migrations&lt;/h2&gt;
&lt;p&gt;All things considered, the migration went well! That being said, there were some hiccups related to Velero configuration details that are worth highlighting.&lt;/p&gt;
&lt;h3 id=&quot;velero-doesn-t-back-up-cluster-scoped-resources-for-namespace-backups&quot; tabindex=&quot;-1&quot;&gt;Velero doesn&#39;t back up cluster-scoped resources for namespace backups&lt;/h3&gt;
&lt;p&gt;Our Velero backups were scoped to specific Kubernetes namespaces (for reasons that are not relevant here). However, namespace-scoped backups do not back up cluster resources by default. This seems obvious in hindsight… but we had not accounted for it.&lt;/p&gt;
&lt;p&gt;This caused certain CRDs our applications relied on to not be migrated. We identified this issue during the staging migration and updated one of the backup configurations to include the --include-cluster-resources=true option.&lt;/p&gt;
&lt;h3 id=&quot;velero-backs-up-pods-by-default&quot; tabindex=&quot;-1&quot;&gt;Velero backs up Pods by default&lt;/h3&gt;
&lt;p&gt;The default behavior for Velero is to back up all resource types, including Pods. For most applications this behavior is fine, but for certain stateful applications, the ordering of pod creation matters. In our case, our RabbitMQ cluster came up split-brained.&lt;/p&gt;
&lt;p&gt;Because of how we opted to scale down/up the job producers we decided it was not necessary to snapshot and migrate the persistent volumes associated with RabbitMQ. Velero backed up three pods and those pods were restored simultaneously in the new cluster. Each of them started, failed to find a leader amongst its peers, and declared itself the leader. Instead of having one cluster with three instances, we had three clusters with one instance each, behind Kubernetes Service which dutifully load-balanced requests across them.&lt;/p&gt;
&lt;p&gt;Unfortunately, we did not catch this during the staging migration because staging had been running with a single RabbitMQ replica (keep your environments identical people!) but by manually modifying the Service selector and labels on the pods we were able to systematically shift all traffic to one instance at a time and allow active jobs to be processed, eventually removing the separate instances, and finally scaling back to the desired single cluster configuration.&lt;/p&gt;
&lt;p&gt;This issue could have been avoided by either (a) configuring Velero not to back up Pods directly so that the Statefulset would bring up the replicas one at a time OR (b) snapshot and migrate the persistent volumes so that the previous clustering information would also be migrated (this would present additional challenges of copying EBS volumes across AWS accounts and modifying the backup files to reference the new volume names/IDs).&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; tabindex=&quot;-1&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Velero helped us streamline the migration of workloads between EKS clusters in separate AWS accounts. It is important to understand your specific backup configuration and how it might impact our system, also having a representative environment to test the environment can help identify issues before they reach your production environment.&lt;/p&gt;
&lt;p&gt;In the end, the key to moving Kubernetes clusters successfully comes down to planning, knowing your workloads, and understanding your tools. This case study provides a glimpse into our journey and might help others who are about to do the same. We hope you can learn from our experience and avoid some of the challenges we faced.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2023/11/velero-aws-account-migration/" />
        </entry>
        <entry>
            <title>
                container-init-process
                
            </title>
            <updated>2025-07-09T12:02:58.153Z</updated>
            <id>https://devopsdirective.com/posts/2023/06/container-init-process/</id>
            <content type="html">
                &lt;h1 id=&quot;container-init-process&quot; tabindex=&quot;-1&quot;&gt;Container Init Process&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; When deploying applications in containers, understanding the role and implications of the init process is critical. In this article, we&#39;ll delve into how our application runs as Process ID &lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#1&quot;&gt;#1&lt;/a&gt; in a container, and what this means for our application&#39;s signal handling and zombie process management.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/06/container-init-process/init-process-diagram.jpg&quot; alt=&quot;init-process-diagram.jpg&quot; /&gt;&lt;br /&gt;
&lt;em&gt;To init or not to init, that is the question...&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;h2 id=&quot;understanding-the-init-process&quot; tabindex=&quot;-1&quot;&gt;Understanding the Init Process&lt;/h2&gt;
&lt;p&gt;When we boot up a Linux computer or virtual machine, the first process to start is known as the &amp;quot;init process&amp;quot;. This process is assigned Process ID &lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#1&quot;&gt;#1&lt;/a&gt;, given that PIDs are assigned sequentially. The init process carries several responsibilities including initializing the kernel, mounting filesystems, managing services and daemons, and supervising other processes.&lt;/p&gt;
&lt;p&gt;This article is not meant to be an exhaustive discussion on init systems. We are only examining aspects of init systems that are relevant to running containers. If we want to learn more about Linux init systems, I recommend watching &lt;a href=&quot;https://www.youtube.com/watch?v=lDdVUlXLjx8&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&amp;quot;Whats the Point of A Linux Init System&amp;quot;&lt;/a&gt; by Brodie Robertson.&lt;/p&gt;
&lt;p&gt;{{&amp;lt; youtube lDdVUlXLjx8 &amp;gt;}}&lt;/p&gt;
&lt;h2 id=&quot;the-implications-of-pid-1-in-containers&quot; tabindex=&quot;-1&quot;&gt;The Implications of PID 1 in Containers&lt;/h2&gt;
&lt;p&gt;Now, what about Linux containers? Containers typically have their own isolated PID namespace. This implies that the process executed upon startup (the &lt;code&gt;ENTRYPOINT&lt;/code&gt; or &lt;code&gt;CMD&lt;/code&gt; within our Dockerfile) gets run as PID 1 inside the container.&lt;/p&gt;
&lt;p&gt;In a standard Linux system, PID 1, also known as &amp;quot;init,&amp;quot; is treated differently from other processes. As mentioned above it has a number of specific responsibilities, some of which may impact our container&#39;s operation. Two aspects of init processes are particularly relevant for containers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Signal Handling:&lt;/strong&gt; What happens when we send a &lt;code&gt;SIGINT&lt;/code&gt; or &lt;code&gt;SIGTERM&lt;/code&gt; signal to the container?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zombie Process Handling:&lt;/strong&gt; What happens when we spawn child processes, and the parent exits?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&#39;s examine these aspects in detail.&lt;/p&gt;
&lt;h2 id=&quot;signal-handling-in-containers&quot; tabindex=&quot;-1&quot;&gt;Signal Handling in Containers&lt;/h2&gt;
&lt;p&gt;Signal handling determines how our application responds to system signals, such as &lt;code&gt;SIGINT&lt;/code&gt; (interrupt from keyboard) or &lt;code&gt;SIGTERM&lt;/code&gt; (termination signal).&lt;/p&gt;
&lt;p&gt;For instance, consider a Node.js program that loops indefinitely and doesn&#39;t handle system signals:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// javascript program that doesn&#39;t handle signals
while (true) {
  console.log(&amp;quot;Looping forever...&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this is running inside a container, when we press &lt;code&gt;CTRL + C&lt;/code&gt;, which sends a &lt;code&gt;SIGINT&lt;/code&gt;, or when we execute &lt;code&gt;docker stop&lt;/code&gt;, which sends a &lt;code&gt;SIGTERM&lt;/code&gt;, the application continues running. This behavior is due to the lack of appropriate signal handling in the application code.&lt;/p&gt;
&lt;p&gt;So what can we do about this?&lt;/p&gt;
&lt;h3 id=&quot;option-1-handle-signals-in-the-application-code&quot; tabindex=&quot;-1&quot;&gt;Option 1: Handle Signals in the Application Code&lt;/h3&gt;
&lt;p&gt;If we control the application source code, it is best to add handlers in our application code that respond appropriately to system signals such as &lt;code&gt;SIGINT&lt;/code&gt; and &lt;code&gt;SIGTERM&lt;/code&gt;. Moreover, it&#39;s best practice to include code that gracefully terminates any ongoing operations, such as closing open client connections before calling &lt;code&gt;process.exit()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Node.js program that handles SIGINT and SIGTERM signals
process.on(&#39;SIGINT&#39;, function() {
  console.log(&#39;Received SIGINT. Gracefully shutting down...&#39;);
  // Insert necessary commands to shut down gracefully here
process.exit();
});

process.on(&#39;SIGTERM&#39;, function() {
  console.log(&#39;Received SIGTERM. Gracefully shutting down...&#39;);
  // Insert necessary commands to shut down gracefully here
  process.exit();
});

while (true) {
  console.log(&amp;quot;Looping forever...&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;option-2-run-a-separate-init-process&quot; tabindex=&quot;-1&quot;&gt;Option 2: Run a Separate Init Process&lt;/h3&gt;
&lt;p&gt;If we do not control the application source code, we can run a separate program as the init process which will in turn spawn the primary application. Tini is a &amp;quot;tiny buy valid init for containers&amp;quot; designed specifically for this purpose (&lt;a href=&quot;https://github.com/krallin/tini&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/krallin/tini&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Docker has the abiliy to do this at runtime by using the init flag (&lt;code&gt;docker run --init &amp;lt;CONTAINER_IMAGE&amp;gt;&lt;/code&gt;). This approach works, but is specific to Docker. If we want a solution that will always run Tini without relying on a specific container runtime implementation, we can build it into the container image!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;FROM node:18.16-bullseye

ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT [&amp;quot;/tini&amp;quot;, &amp;quot;--&amp;quot;]

COPY . .
CMD [&amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is important to note that this will NOT magically make our app terminate gracefully, but at least &lt;code&gt;SIGINT&lt;/code&gt; and &lt;code&gt;SIGTERM&lt;/code&gt; signals will stop the containerized application.&lt;/p&gt;
&lt;h2 id=&quot;zombie-process-handling-in-containers&quot; tabindex=&quot;-1&quot;&gt;Zombie Process Handling in Containers&lt;/h2&gt;
&lt;p&gt;A &amp;quot;Zombie&amp;quot; process is a process that has finished its execution but still exists in the process table. Although these processes no longer consume CPU or memory, they should ideally be cleaned up. The init process typically handles this clean-up.&lt;/p&gt;
&lt;p&gt;For example, consider a Node.js program that uses &lt;code&gt;child_process.spawn&lt;/code&gt; in detached mode to run a shell script that initiates a sleep operation in the background before exiting. This sequence can create zombie processes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// index.js
const { exec } = require(&#39;child_process&#39;);

function sleep(ms) {
  return new Promise((resolve) =&amp;gt; setTimeout(resolve, ms));
}

async function main() {
  while (true) {
    console.log(&#39;Spawning child process!&#39;);
    exec(&#39;./child.sh&#39;);
    await sleep(1000);
  }
}

main();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shell script &lt;code&gt;child.sh&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!usr/bin/env sh
# child.sh (called from Node.js program)
sleep 3 &amp;amp;
exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this scenario, each invocation of &lt;code&gt;child.sh&lt;/code&gt; is a child process, and each &lt;code&gt;sleep&lt;/code&gt; invocation within the child is a grandchild. When the &lt;code&gt;child.sh&lt;/code&gt; processes exit, the grandchildren run to completion and never get cleaned up, turning into zombie processes.&lt;/p&gt;
&lt;p&gt;To resolve this issue, we can update our code so that parent processes wait for their children to exit. In a shell script, we can achieve this by waiting for the PID of the grandchild. If we&#39;re spawning native Node.js processes, much of this will be handled automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!usr/bin/env sh
sleep 3 &amp;amp;

# By capturing the PID of the sleep command and waiting 
# for it to exit we no longer create zombies!
pid=$!
wait $pid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As with signal handling, if we don&#39;t control the source code, we can use a separate init process to handle zombie processes. The separate init process will reap zombie processes when they exit.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Running our application as PID 1 inside a container has implications for signal handling and zombie process reaping. The best practices are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Properly handle system signals in our application code.&lt;/li&gt;
&lt;li&gt;Manage child processes to prevent the creation of zombie processes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If we cannot modify the application code, consider using a separate init process.&lt;/p&gt;
&lt;p&gt;Hopefully, this article has helped we understand these considerations. To learn more about containers, check out my comprehensive course {{&amp;lt; link &amp;quot;&lt;a href=&quot;https://courses.devopsdirective.com/docker-beginner-to-pro/lessons/00-introduction/01-main&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://courses.devopsdirective.com/docker-beginner-to-pro/lessons/00-introduction/01-main&lt;/a&gt;&amp;quot; &amp;quot;Docker: Beginner to Pro&amp;quot; &amp;gt;}}:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://courses.devopsdirective.com/docker-beginner-to-pro/lessons/00-introduction/01-main&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/06/container-init-process/course-screenshot.png&quot; alt=&quot;course-screenshot.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;additional-resources&quot; tabindex=&quot;-1&quot;&gt;Additional Resources:&lt;/h2&gt;
&lt;p&gt;Here is a list of additional resources on the internet I found helpful when trying to understand this topic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://daveiscoding.com/why-do-you-need-an-init-process-inside-your-docker-container-pid-1&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Why do you need an init process inside your Docker container (PID 1) -- Dávid Szabó&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://petermalmgren.com/pid-1-child-processes-docker/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;PID 1 Orphan Child Processes in Docker -- Peter Malmgren&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://emmer.dev/blog/you-don-t-need-an-init-system-for-node.js-in-docker/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;You Don&#39;t Need an Init System for Node.js in Docker -- Christian Emmer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ianlewis.org/en/almighty-pause-container&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;The Almighty Pause Container -- Ian Lewis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ahmet.im/blog/minimal-init-process-for-containers/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Choosing an init process for multi-process containers -- Ahmet Alp Balkan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/Zgx0o8QjJk4?t=1065&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Docker and Node.js Best Practices from Bret Fisher at DockerCon 2019 [YouTube] -- Bret Fisher&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2023/06/container-init-process/" />
        </entry>
        <entry>
            <title>
                writing-a-nodejs-dockerfile
                
            </title>
            <updated>2025-07-09T02:27:47.040Z</updated>
            <id>https://devopsdirective.com/posts/2023/03/writing-a-nodejs-dockerfile/</id>
            <content type="html">
                &lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Tutorials often show simplistic, naive Dockerfiles. We can do better.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/03/writing-a-nodejs-dockerfile/crappy-node-dockerfile.jpeg&quot; alt=&quot;crappy-node-dockerfile.jpeg&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Please don&#39;t do this! 🙏&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#background&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#naive-implementation&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Naive Implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#pin-the-base-image-%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Pin the Base Image (🔒+🏎️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#set-a-working-directory-%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Set a Working Directory (👁️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#copy-packagejson-and-package-lockjson-before-source-code-%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Copy package.json and package-lock.json Before Source Code (🏎️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#use-a-non-root-user-&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Use a non-root USER (🔒)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#configure-for-the-production-environment---%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Configure for the Production Environment (🔒 + 🏎️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#add-useful-metadata-%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Add Useful Metadata (👁️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#use-a-cache-mount-to-speed-up-dependency-installation-%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Use a Cache Mount to Speed Up Dependency Installation (🏎️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#use-a-multi-stage-dockerfile-%EF%B8%8F&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Use a Multi-Stage Dockerfile (👁️)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#references&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;Node.js is a popular Javascript runtime environment that enables developers to write back-end applications. There are many tutorials online showing how to containerize Node.js applications, but many of them show a naive implementation that is functional, but lacking in so many ways!&lt;/p&gt;
&lt;h2 id=&quot;naive-implementation&quot; tabindex=&quot;-1&quot;&gt;Naive Implementation&lt;/h2&gt;
&lt;p&gt;This Dockerfile starts from the official &lt;a href=&quot;https://hub.docker.com/_/node&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;node container image from DockerHub&lt;/a&gt;, copies in the entire build context, installs dependencies with npm, and sets a command to be run upon startup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node
COPY . .
RUN npm install
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this will technically work, there are &lt;strong&gt;SO&lt;/strong&gt; many ways in which we can improve it. Let us incrementally improve this Dockerfile to make it more secure (🔒), build faster (🏎️), and more clear (👁️)!&lt;/p&gt;
&lt;h2 id=&quot;pin-the-base-image&quot; tabindex=&quot;-1&quot;&gt;Pin the Base Image (🔒+🏎️)&lt;/h2&gt;
&lt;p&gt;The first way we can improve the Dockerfile is by pinning the base image to a specific version. With no tag, Docker will use the &amp;quot;latest&amp;quot; tag which is the default tag applied to images. This would cause the base image to change with each new update to the upstream image, inevitably breaking your application.&lt;/p&gt;
&lt;p&gt;We can choose a specific base image that is small and secure to meet the needs of our application. See this article for information about choosing a good base image: &amp;quot;TODO&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Pin specific version
# Use slim for reduced image size
FROM node:19.6-bullseye-slim
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
COPY . .
RUN npm install
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pinning to the minor version should prevent known breaking changes while still allowing patch versions containing bugfixes to be utilized. If you want to truly lock the base image you can refer to a specific image hash such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim@sha256:e684615bdfb71cb676b3d0dfcc538c416f7254697d8f9639bd87255062fd1681
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;set-a-working-directory&quot; tabindex=&quot;-1&quot;&gt;Set a Working Directory (👁️)&lt;/h2&gt;
&lt;p&gt;By default, the working directory would be the root path (&lt;code&gt;/&lt;/code&gt;) but you should set it to something else based on the conventions of your specific language + framework.&lt;/p&gt;
&lt;p&gt;This will provide a dedicated place in the filesystem with your app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Specify working directory other than /
WORKDIR /usr/src/app
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
COPY . .
RUN npm install
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;copy-package-json-and-package-lock-json-before-source-code&quot; tabindex=&quot;-1&quot;&gt;Copy package.json and package-lock.json Before Source Code (🏎️)&lt;/h2&gt;
&lt;p&gt;Each instruction within the Dockerfile creates a new layer within the image. Docker caches these layers to speed up subsequent builds. Previously, every change to the source code would invalidate the layer cache for &lt;code&gt;COPY . .&lt;/code&gt; causing the build to reinstall all of the dependencies (which can be SLOW!).&lt;/p&gt;
&lt;p&gt;By copying only the dependency configuration files before running &lt;code&gt;npm install&lt;/code&gt; we can protect the layer cache and avoid reinstalling the dependencies with each source code change.&lt;/p&gt;
&lt;p&gt;We can also use a &lt;a href=&quot;https://docs.docker.com/engine/reference/builder/#dockerignore-file&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;.dockerignore&lt;/code&gt; file &lt;/a&gt; to specify files that should not be included in the container image (such as the &lt;code&gt;node_modules&lt;/code&gt; directory).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim
WORKDIR /usr/src/app
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Copy only files required to install
# dependencies (better layer caching)
COPY package*.json ./
RUN npm install
# Copy remaining source code AFTER installing dependencies. 
# Again, copy only the necessary files
COPY ./src/ .
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;use-a-non-root-user&quot; tabindex=&quot;-1&quot;&gt;Use a non-root USER (🔒)&lt;/h2&gt;
&lt;p&gt;If configured properly, containers provide some protection (via user namespaces) between a root user inside a container and the host system user, but setting to a non-root user provides another layer to our defense in depth security approach!&lt;/p&gt;
&lt;p&gt;The node base image already has a user named &lt;code&gt;node&lt;/code&gt; we can use for this purpose.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Use non-root user
# Use --chown on COPY commands to set file permissions
USER node
COPY --chown=node:node ./src/ .
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;configure-for-the-production-environment&quot; tabindex=&quot;-1&quot;&gt;Configure for the Production Environment (🔒 + 🏎️)&lt;/h2&gt;
&lt;p&gt;Many Node.js packages look for the &lt;code&gt;NODE_ENV&lt;/code&gt; environment variable and behave differently if it is set to production (reduced logging, etc...). We can set this within the Dockerfile to ensure it will be set at runtime by default.&lt;/p&gt;
&lt;p&gt;Also, rather than using &lt;code&gt;npm install&lt;/code&gt; it is preferable to use &lt;code&gt;npm ci&lt;/code&gt; or &amp;quot;clean install&amp;quot; which requires the use of a &lt;code&gt;package-lock.json&lt;/code&gt; file and ensures the installed dependencies match the fully specified versions from that file. By using &lt;code&gt;--only=production&lt;/code&gt; we can avoid installing unnecessary development dependencies reducing the attack surface area and further reducing the image size.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Set NODE_ENV
ENV NODE_ENV production
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
WORKDIR /usr/src/app
COPY package*.json ./
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Install only production dependencies
RUN npm ci --only=production
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
USER node
COPY --chown=node:node ./src/ .
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;add-useful-metadata&quot; tabindex=&quot;-1&quot;&gt;Add Useful Metadata (👁️)&lt;/h2&gt;
&lt;p&gt;There are a few Dockerfile instructions that don&#39;t change the container runtime behavior, do provide useful metadata for users of the resulting container image.&lt;/p&gt;
&lt;p&gt;We can add &lt;code&gt;LABEL&lt;/code&gt; instructions with various annotations about the container image. For example we might want to include the Dockerfile author, version, licenses, etc... A set of suggested annotation keys from the Open Container Initiative can be found here: &lt;a href=&quot;https://github.com/opencontainers/image-spec/blob/main/annotations.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/opencontainers/image-spec/blob/main/annotations.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;EXPOSE&lt;/code&gt; command tells end users the port number that the containerized application expects to listen on. The port will still need to be published at runtime, but it is useful to include this instruction to make it clear to end users which port should be opened.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Use LABELS to provide additional info
LABEL org.opencontainers.image.authors=&amp;quot;sid@devopsdirective.com&amp;quot;
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
USER node
COPY --chown=node:node ./src/ .
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Indicate expected port
EXPOSE 3000
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;use-a-cache-mount-to-speed-up-dependency-installation&quot; tabindex=&quot;-1&quot;&gt;Use a Cache Mount to Speed Up Dependency Installation (🏎️)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/build/buildkit/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Buildkit&lt;/a&gt; provides many useful features, including the ability to specify a cache mount for specific &lt;code&gt;RUN&lt;/code&gt; instructions within a Dockerifle. By specifying a cache in this way, changing a dependency won&#39;t require redownloading all dependencies from the internet because previously installed dependencies will be stored locally.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/em&gt; If building the image in a remote continuous Integration system (e.g. GitHub Actions), you would need to configure that system to store and retrieve this cache across pipeline runs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:19.6-bullseye-slim
LABEL org.opencontainers.image.authors=&amp;quot;sid@devopsdirective.com&amp;quot;
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY package*.json ./
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Use cache mount to speed up install of existing dependencies
RUN --mount=type=cache,target=/usr/src/app/.npm &#92;
  npm set cache /usr/src/app/.npm &amp;amp;&amp;amp; &#92;
  npm ci --only=production
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
USER node
COPY --chown=node:node ./src/ .
EXPOSE 3000
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;use-a-multi-stage-dockerfile&quot; tabindex=&quot;-1&quot;&gt;Use a Multi-Stage Dockerfile (👁️)&lt;/h2&gt;
&lt;p&gt;By splitting out separate development and production image stages we can have an ergonomic dev environment with dev dependencies, hot reloading, etc... but retain the production improvements for deployment.&lt;/p&gt;
&lt;p&gt;Shared steps can be built into a &lt;code&gt;base&lt;/code&gt; stage and then customizations can be built on top of that base.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Name the first stage to reference later
FROM node:19.6-bullseye-slim AS base
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
LABEL org.opencontainers.image.authors=&amp;quot;sid@devopsdirective.com&amp;quot;
WORKDIR /usr/src/app
COPY package*.json ./
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Use the base stage to create dev image
FROM base AS dev
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
RUN --mount=type=cache,target=/usr/src/app/.npm &#92;
  npm set cache /usr/src/app/.npm &amp;amp;&amp;amp; &#92;
  npm install
COPY . .
CMD [&amp;quot;npm&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;dev&amp;quot;]
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
# Use the base stage to create separate production image
FROM base AS production
&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#-------------------------------------------&quot;&gt;#-------------------------------------------&lt;/a&gt;
ENV NODE_ENV production
RUN --mount=type=cache,target=/usr/src/app/.npm &#92;
  npm set cache /usr/src/app/.npm &amp;amp;&amp;amp; &#92;
  npm ci --only=production
USER node
COPY --chown=node:node ./src/ .
EXPOSE 3000
CMD [ &amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;This post from &lt;a href=&quot;https://twitter.com/liran_tal&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Liran Tal&lt;/a&gt; inspired many of the security-related improvements: &lt;a href=&quot;https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;My tweet thread on this topic went viral. See here for the dialog it generated: &lt;a href=&quot;https://twitter.com/sidpalas/status/1634194026500096000&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://twitter.com/sidpalas/status/1634194026500096000&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2023/03/writing-a-nodejs-dockerfile/" />
        </entry>
        <entry>
            <title>
                docker-cmd-vs-entrypoint
                
            </title>
            <updated>2025-07-09T02:25:56.031Z</updated>
            <id>https://devopsdirective.com/posts/2023/03/docker-cmd-vs-entrypoint/</id>
            <content type="html">
                &lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The simplest possible example to demonstrate the difference between Docker&#39;s CMD and ENTRYPOINT instructions:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/03/docker-cmd-vs-entrypoint/cmd-vs-entrypoint.png&quot; alt=&quot;cmd-vs-entrypoint.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;h2 id=&quot;dockerfile&quot; tabindex=&quot;-1&quot;&gt;Dockerfile&lt;/h2&gt;
&lt;p&gt;The Dockerfile CMD and ENTRYPOINT instructions can be confusing. Let&#39;s create a mimimal docker image to explore how they behave and interact with eachother:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Bash&quot;&gt;docker build -t entrypoint-cmd-example -&amp;lt;&amp;lt;EOF 
  FROM busybox 
  ENTRYPOINT [ &amp;quot;echo&amp;quot;, &amp;quot;[ENTRYPOINT]&amp;quot; ]
  CMD [ &amp;quot;[CMD]&amp;quot; ]
EOF 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;no-arguments&quot; tabindex=&quot;-1&quot;&gt;No Arguments&lt;/h2&gt;
&lt;p&gt;If you run the image as is (no options/arguments), the ENTRYPOINT is used as the executable and the CMD is appended as arguments.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run entrypoint-cmd-example 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output will combine the ENTRYPOINT and CMD from the Dockerfile:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[ENTRYPOINT] [CMD]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;with-arguments-override-cmd&quot; tabindex=&quot;-1&quot;&gt;With Arguments (override CMD)&lt;/h2&gt;
&lt;p&gt;If you specify arguments at runtime (e.g. docker run image-name FOO), those arguments override the CMD from the Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run entrypoint-cmd-example &amp;quot;[OVERRIDDEN CMD]&amp;quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output will combine the ENTRYPOINT from the Dockerfile with the arguments provided at runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[ENTRYPOINT] [OVERRIDDEN CMD] 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;override-entrypoint&quot; tabindex=&quot;-1&quot;&gt;Override Entrypoint&lt;/h2&gt;
&lt;p&gt;If you specify a new entrypoint at runtime (e.g. docker run --entrypoint=BAR image-name) that will override the ENTRYPOINT and IGNORE the CMD from the Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --entrypoint echo entrypoint-cmd-example 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output is an empty string since &lt;code&gt;echo&lt;/code&gt; is run with no arguments.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;with-arguments-override-cmd-and-override-entrypoint&quot; tabindex=&quot;-1&quot;&gt;With Arguments (override CMD) AND Override Entrypoint&lt;/h2&gt;
&lt;p&gt;If you specify a new entrypoint AND arguments at runtime (e.g. &lt;code&gt;docker run --entrypoint=BAZ image-name BING&lt;/code&gt;) those will be used instead of the values from the Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;❯ docker run --entrypoint=echo entrypoint-cmd-example &amp;quot;[OVERRIDDEN CMD]&amp;quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output will use the arguments supplied at runtime with the executable specified in the overriden entrypoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[OVERRIDDEN CMD] 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;when-to-use-each&quot; tabindex=&quot;-1&quot;&gt;When to Use Each&lt;/h2&gt;
&lt;p&gt;You should specify an ENTRYPOINT if you want the container to use the same executable (but potentially with different arguments) every time.&lt;/p&gt;
&lt;p&gt;The CMD should contain the default arguments you want the container to run with.&lt;/p&gt;
&lt;p&gt;The official docs have a nice summary of the various interactions as well: &lt;a href=&quot;https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It is made slightly more complex by the fact that there is different behavior between:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ENTRYPOINT foo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AND&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ENTRYPOINT [&amp;quot;foo&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(With the latter being preferred)&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2023/03/docker-cmd-vs-entrypoint/" />
        </entry>
        <entry>
            <title>
                cloudflare-workers-link-forwarder
                
            </title>
            <updated>2025-07-09T02:25:18.559Z</updated>
            <id>https://devopsdirective.com/posts/2023/03/cloudflare-workers-link-forwarder/</id>
            <content type="html">
                &lt;h1 id=&quot;cloudflare-workers-custom-link-forwarder&quot; tabindex=&quot;-1&quot;&gt;Cloudflare Workers Custom Link Forwarder&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Setting up a custom link forwarder (e.g. &lt;code&gt;links.devopsdirective.com/foo&lt;/code&gt;) takes about 3 minutes using Cloudflare workers!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/03/cloudflare-workers-link-forwarder/cloudflare-workers-screenshot.png&quot; alt=&quot;cloudflare-workers-screenshot.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;h1 id=&quot;motivation&quot; tabindex=&quot;-1&quot;&gt;Motivation&lt;/h1&gt;
&lt;p&gt;I have lots of content across many different platforms (YouTube, Twitter, LinkedIn, etc...) and wanted to create a was to easily set up custom links pointing to them. This way I can provide people links such as &lt;a href=&quot;https://links.devopsdirective.com/terraform-gumroad&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://links.devopsdirective.com/terraform-gumroad&lt;/a&gt; rather than &lt;a href=&quot;https://sidpalas.gumroad.com/l/hieekq&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://sidpalas.gumroad.com/l/hieekq&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This seemed like a perfect opportunity to try out &lt;a href=&quot;https://workers.cloudflare.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;cloudflare workers&lt;/a&gt;, a serverless compute offering from Cloudflare.&lt;/p&gt;
&lt;h2 id=&quot;create-a-service&quot; tabindex=&quot;-1&quot;&gt;Create a Service&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/03/cloudflare-workers-link-forwarder/workers-homepage-screenshot.png&quot; alt=&quot;workers-homepage-screenshot.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Each unit of compute within Cloudflare workers is called a &amp;quot;service&amp;quot;. To create a service you can either use the CLI called &amp;quot;&lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Wrangler&lt;/a&gt;&amp;quot; or navigate to the home page of your Cloudflare account at &lt;code&gt;https://dash.cloudflare.com/&amp;lt;ACCOUNT_ID&amp;gt;/workers/overview&lt;/code&gt;. For this article I&#39;ll be showing the web UI.&lt;/p&gt;
&lt;p&gt;You give your service a name and choose some simple starter code before clicking &lt;code&gt;Create Service&lt;/code&gt;. This will deploy the service to &lt;code&gt;https://&amp;lt;SERVICE_NAME&amp;gt;.&amp;lt;YOUR_CUSTOM_WORKERS_SUBDOMAIN&amp;gt;.workers.dev&lt;/code&gt; and within a few seconds it will be active.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; While Cloudflare workers do support other languages (either via WASM or cr languages that compile to JavaScript) JavaScript and TypeScript have the best native developer experience.&lt;/p&gt;
&lt;h2 id=&quot;updating-the-code&quot; tabindex=&quot;-1&quot;&gt;Updating the Code&lt;/h2&gt;
&lt;p&gt;After creating the service, it will then show up in your top-level workers dashboard where you can click it and choose &lt;code&gt;Quick Edit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This will bring up a simple code editor + HTTP request testing interface so you can make changes to your code and test them right in the browser. For more complex use cases, editing in your IDE of choice and using the Wrangler CLI to deploy makes sense, but here I am only going to have a few lines of code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/03/cloudflare-workers-link-forwarder/workers-editor-screenshot.png&quot; alt=&quot;workers-editor-screenshot.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-code&quot; tabindex=&quot;-1&quot;&gt;The Code&lt;/h2&gt;
&lt;p&gt;Your service will need to export a default object containing properties corresponding to the events the worker should handle (e.g. &lt;code&gt;fetch&lt;/code&gt; events or &lt;code&gt;scheduled&lt;/code&gt; events). A simple hello world service can be written as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
  async fetch(request, env) {
    return new Response(&amp;quot;Hello world&amp;quot;)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the link forwarding use case, we extract the pathname from the fetch event &lt;code&gt;request.URL&lt;/code&gt; and then check if it matches one of our links. If so, redirect to the desired location, but if no links match, redirect to &lt;a href=&quot;https://devopsdirective.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://devopsdirective.com&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
  async fetch(request, env) {
    try {
      const { pathname } = new URL(request.url);
      const statusCode=301;

      if (pathname.startsWith(&amp;quot;/terraform-gumroad&amp;quot;)) {
        const destinationURL=&amp;quot;https://658236975647.gumroad.com/l/hieekq&amp;quot;;
        return Response.redirect(destinationURL, statusCode);
      }

      // Insert additional links here!

      const defaultDestinationURL=&amp;quot;https://devopsdirective.com&amp;quot;;
      return Response.redirect(defaultDestinationURL, statusCode);
    } catch(e) {
      return new Response(err.stack, { status: 500 })
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;custom-subdomain&quot; tabindex=&quot;-1&quot;&gt;Custom Subdomain&lt;/h2&gt;
&lt;p&gt;At this point, the worker is doing what we want, but to make the links a bit nicer we can wire up custom trigger for a domain of our choosing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2023/03/cloudflare-workers-link-forwarder/custom-triggers-screenshot.png&quot; alt=&quot;custom-triggers-screenshot.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can choose a domain or subdomain of any domain managed by your CloudFlare account once the DNS settings propagate, your forwarding links will be active!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2023/03/cloudflare-workers-link-forwarder/" />
        </entry>
        <entry>
            <title>
                kubernetes-maintenance-page
                
            </title>
            <updated>2025-07-09T14:14:45.510Z</updated>
            <id>https://devopsdirective.com/posts/2022/10/kubernetes-maintenance-page/</id>
            <content type="html">
                &lt;h1 id=&quot;kubernetes-maintenance-page&quot; tabindex=&quot;-1&quot;&gt;Kubernetes Maintenance Page&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR: Need to set up a quick maintenance page for a service hosted in Kubernetes? You can create a custom page and host it with nginx without even needing a custom container image!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A gist containing the full code can be found here: &lt;em&gt;{{&amp;lt; link &amp;quot;&lt;a href=&quot;https://gist.github.com/sidpalas/e388f1a63bacc4c365d6cebf366f492d&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://gist.github.com/sidpalas/e388f1a63bacc4c365d6cebf366f492d&lt;/a&gt;&amp;quot; &amp;quot;LINK&amp;quot; &amp;gt;}}&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/10/kubernetes-maintenance-page/maintenance-page-screenshot.png&quot; alt=&quot;maintenance-page-screenshot.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;h3 id=&quot;table-of-contents&quot; tabindex=&quot;-1&quot;&gt;Table of Contents:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#why&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Why?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#how&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;How?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#nginx-configuration&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Nginx Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#kubernetes-resources&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kubernetes resources&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#configmap&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;ConfigMap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#deployment&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#service--ingress&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Service &amp;amp; Ingress&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why&quot; tabindex=&quot;-1&quot;&gt;Why?&lt;/h2&gt;
&lt;p&gt;While it is usually best to attempt to avoid downtime for your production services, sometimes downtime is unavoidable (or at least not worth the effort to avoid). I recently had a Kubernetes-based project that called for a maintenance period, and this is the solution I came up with. The two key attributes I wanted to achieve were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Easy + fast to toggle on/off&lt;/strong&gt; (I wanted to avoid needing a DNS update because caching can cause them to be slow and unpredictable)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No additional container image required&lt;/strong&gt; (Every time you build a custom container image, it is one more thing to maintain, host in a registry, etc...)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how&quot; tabindex=&quot;-1&quot;&gt;How?&lt;/h2&gt;
&lt;p&gt;To achieve these goals I used the default &lt;a href=&quot;https://hub.docker.com/_/nginx&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;nginx image&lt;/a&gt; and took advantage of the fact that &lt;code&gt;ConfigMaps&lt;/code&gt; can be &lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#populate-a-volume-with-data-stored-in-a-configmap&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;mounted as volumes&lt;/a&gt; to inject my HTML, CSS, and nginx configuration into the pod at runtime!&lt;/p&gt;
&lt;p&gt;This gives me an easy way to serve the maintenance page and then a minor change to my Ingress definition will route traffic to/away from it.&lt;/p&gt;
&lt;h3 id=&quot;nginx-configuration&quot; tabindex=&quot;-1&quot;&gt;Nginx Configuration&lt;/h3&gt;
&lt;p&gt;I StackOverflow copy/pasted my way to the following &lt;code&gt;default.conf&lt;/code&gt; nginx configuration which will take all requests (except for &lt;code&gt;png|jpg|jpeg|css&lt;/code&gt; filetypes) and route them to the maintenance page located at &lt;code&gt;/usr/share/nginx/html/maintenance/maintenance.html&lt;/code&gt;, returning the proper &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;503 Service Unavailable&lt;/a&gt; HTML code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# default.conf
server { 
  listen 80 default_server;
  server_name  _ ;

  location / {
    if (-f /usr/share/nginx/html/maintenance/maintenance.html) {
      return 503;
    }
  }
  
  # for all routes, return maintenance page
  error_page 503 @maintenance;
  location @maintenance {
    root    /usr/share/nginx/html/maintenance/;
    rewrite ^(.*)$ /maintenance.html break;
  }
  
  # allow images and css to be retrieved
  location ~* &#92;.(png|jpg|jpeg|css) {
    root /usr/share/nginx/html/maintenance/;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;kubernetes-resources&quot; tabindex=&quot;-1&quot;&gt;Kubernetes resources&lt;/h2&gt;
&lt;h3 id=&quot;config-map&quot; tabindex=&quot;-1&quot;&gt;ConfigMap&lt;/h3&gt;
&lt;p&gt;I created a &lt;code&gt;ConfigMap&lt;/code&gt; containing the site files (HTML + CSS) and nginx &lt;code&gt;default.conf&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-YAML&quot;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: maintenance-page
data:
  maintenance.html: |-
    &amp;lt;!--HTML GOES HERE--&amp;gt;
  maintenance.css: |-
    /* CSS GOES HERE */
  default.conf: |-
    # NGINX CONFIGURATION GOES HERE
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;deployment&quot; tabindex=&quot;-1&quot;&gt;Deployment&lt;/h3&gt;
&lt;p&gt;I then created a &lt;code&gt;Deployment&lt;/code&gt; which mounts this &lt;code&gt;ConfigMap&lt;/code&gt; to achieve the desired effect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-YAML&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: maintenance-page
  labels:
    app: maintenance-page
spec:
  replicas: 1
  selector:
    matchLabels:
      app: maintenance-page
  template:
    metadata:
      labels:
        app: maintenance-page
    spec:
      containers:
      - name: nginx
        image: nginx:1.23
        ports:
        - containerPort: 80
        volumeMounts:
        # Because no subPath is specified, all keys in configmap will
        # be mounted as files at the specified mountPath
        - name: config-volume
          mountPath: /usr/share/nginx/html/maintenance/
        - name: config-volume
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: default.conf
      volumes:
        - name: config-volume
          configMap:
            name: maintenance-page
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;service-and-amp-ingress&quot; tabindex=&quot;-1&quot;&gt;Service &amp;amp; Ingress&lt;/h3&gt;
&lt;p&gt;All that remained were the &lt;code&gt;Service&lt;/code&gt; and &lt;code&gt;Ingress&lt;/code&gt; objects to route traffic to this &lt;code&gt;Deployment&lt;/code&gt;&#39;s pod (&lt;em&gt;Note:&lt;/em&gt; the cluster is using the &lt;a href=&quot;https://docs.nginx.com/nginx-ingress-controller/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;nginx ingress controller&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-YAML&quot;&gt;apiVersion: v1
kind: Service
metadata:
  name: maintenance-page
spec:
  selector:
    app: maintenance-page
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.org/rewrites: serviceName=maintenance-page rewrite=/;
  name: maintenance-page
spec:
  rules:
  - host: maintenance.devopsdirective.com
    http:
      paths:
      # Can also set this up only for specific paths
      - path: /
        pathType: Prefix
        backend:
          service:
            name: maintenance-page
            port:
              number: 8080
      # Can still have other paths defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To turn on maintenance mode, I apply these resources to the cluster and to turn it off, I reapply the previous &lt;code&gt;Ingress&lt;/code&gt; definition. The &lt;code&gt;ConfigMap&lt;/code&gt;/&lt;code&gt;Deployment&lt;/code&gt;/&lt;code&gt;Service&lt;/code&gt; could be deleted, or the &lt;code&gt;Deployment&lt;/code&gt; scaled to zero replicas if it will be used again in the future!&lt;/p&gt;
&lt;p&gt;If you already have an &lt;code&gt;nginx&lt;/code&gt; based deployment in the cluster, you could instead incorporate this configuration into it, but I like the fact that this is a standalone setup completely isolated from the standard config.&lt;/p&gt;
&lt;p&gt;If you need to have a maintenance window for some services running in your cluster hopefully this helps make things easier for you!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2022/10/kubernetes-maintenance-page/" />
        </entry>
        <entry>
            <title>
                why-k8s-exists
                
            </title>
            <updated>2025-07-09T02:23:14.315Z</updated>
            <id>https://devopsdirective.com/posts/2022/08/why-k8s-exists/</id>
            <content type="html">
                &lt;p&gt;&lt;strong&gt;TL;DR: Kubernetes is complex because it is solving complex problems. It builds on multiple decades of progress to provide a solid foundation for deploying web applications!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/08/why-k8s-exists/k8s-history.png&quot; alt=&quot;k8s-history.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;h4 id=&quot;table-of-contents&quot; tabindex=&quot;-1&quot;&gt;Table of Contents:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#pre-cloud-2000s&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Pre-Cloud (2000s)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#virtual-machine--configuration-management-2010s&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Virtual Machine + Configuration Management (2010s)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#workload-orchestrators-late-2010s---today&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Workload Orchestrators (Late 2010s -&amp;gt; Today)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#video-summary&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Video Summary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;pre-cloud-2000s&quot; tabindex=&quot;-1&quot;&gt;Pre-Cloud (2000s)&lt;/h3&gt;
&lt;p&gt;In the early days of the web, modern clouds didn&#39;t exist (AWS started in 2006), but deploying web applications still required access to servers!&lt;/p&gt;
&lt;p&gt;You either racked them yourself or rented them from a colocation facility, but either way, they were slow to procure and painful to manage!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/08/why-k8s-exists/server-room.jpg&quot; alt=&quot;server-room.jpg&quot; /&gt;&lt;br /&gt;
&lt;em&gt;This doesn&#39;t look very fun...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The tooling to manage large fleets of servers and applications simply didn&#39;t exist, so you would often hire a team of system administrators, who would end up rolling their own tooling in the form of Bash or Batch/PowerShell scripts! Also, because the applications were running directly on the host system(s), dependency management was a nightmare!&lt;/p&gt;
&lt;p&gt;These factors pushed teams to use monolithic architectures because it was simply too hard to manage anything else. Doing zero downtime deployments with this type of setup was incredibly difficult, so many companies leveraged scheduled maintenance windows to handle updates and roll out new versions.&lt;/p&gt;
&lt;h3 id=&quot;virtual-machine-configuration-management-2010s&quot; tabindex=&quot;-1&quot;&gt;Virtual Machine + Configuration Management (2010s)&lt;/h3&gt;
&lt;p&gt;Once cloud providers started maturing, the concept of creating and destroying Virtual Machines in minutes is a paradigm shift. Rather than maintain long-lived servers you could tear down the old ones and replace them!&lt;/p&gt;
&lt;p&gt;Also, configuration management tools like Puppet and Chef matured, helping make it possible to manage fleets of servers more easily!&lt;/p&gt;
&lt;p&gt;Virtualization technologies provide a level of isolation between applications that alleviate some of the dependency hell from before!&lt;/p&gt;
&lt;p&gt;As web applications started tackling more complex problem domains, sometimes team size caused teams to start moving away from monolithic architectures. The improved tooling of the cloud made it possible to carve things up into smaller pieces.&lt;/p&gt;
&lt;p&gt;However, because the size and complexity of these systems were now larger, we needed improved tooling to help automate deployments, monitor application health across all of those machines, aggregate logs, etc...&lt;/p&gt;
&lt;h3 id=&quot;workload-orchestrators-late-2010s-and-gt-today&quot; tabindex=&quot;-1&quot;&gt;Workload Orchestrators (Late 2010s -&amp;gt; Today)&lt;/h3&gt;
&lt;p&gt;Moving towards the late 2010s and into the present day, solutions to those challenges have emerged in the form of what we now call &amp;quot;Workload Orchestrators&amp;quot;.&lt;/p&gt;
&lt;p&gt;There are a number of these types of systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker Swarm&lt;/li&gt;
&lt;li&gt;Apache Mesos&lt;/li&gt;
&lt;li&gt;Hashicorp Nomad&lt;/li&gt;
&lt;li&gt;Kubernetes (today&#39;s industry darling)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These provide APIs to abstract away the individual machines and instead treat a cluster of instances as a single resource pool.&lt;/p&gt;
&lt;p&gt;You provide information about the types of resources our workloads require and then let the system decide how to schedule and run them!&lt;/p&gt;
&lt;p&gt;Kubernetes provides things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automated health checks&lt;/li&gt;
&lt;li&gt;Deployment strategies to enable zero downtime deployment (+ rollbacks)&lt;/li&gt;
&lt;li&gt;Autoscaling based on metrics&lt;/li&gt;
&lt;li&gt;Standard interfaces for networking, storage, and runtimes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before these systems were available, large companies were building them internally in private.&lt;/p&gt;
&lt;p&gt;Luckily for us, a few of those companies decided to release their systems. Google took many of the ideas from their internal system &amp;quot;Borg&amp;quot; and started the Kubernetes project!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/08/why-k8s-exists/borg-diagram.jpeg&quot; alt=&quot;borg-diagram.jpeg&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Diagram from Google&#39;s public borg paper (&lt;a href=&quot;https://research.google/pubs/pub43438/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://research.google/pubs/pub43438/&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, rather than provisioning a bunch of instances/autoscaling groups for each app, you can provision a cluster and deploy many heterogeneous workloads into it!&lt;/p&gt;
&lt;p&gt;You leverage the well-tested utilities of the system to handle what used to be done with your home-grown scripts.&lt;/p&gt;
&lt;p&gt;So, yes Kubernetes IS quite complex, but that is because it is attempting to solve a complex problem!&lt;/p&gt;
&lt;p&gt;Hopefully, this brief look over the past two decades has helped to give you an appreciation for why Kubernetes exists and operates the way it does.&lt;/p&gt;
&lt;h3 id=&quot;video-summary&quot; tabindex=&quot;-1&quot;&gt;Video Summary&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/228XgLK1I8A?si=ahu_YgmJ-uWUFPpU&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2022/08/why-k8s-exists/" />
        </entry>
        <entry>
            <title>
                blog-workflow-improvements
                
            </title>
            <updated>2025-07-09T02:21:02.955Z</updated>
            <id>https://devopsdirective.com/posts/2022/08/blog-workflow-improvements/</id>
            <content type="html">
                &lt;h1 id=&quot;blog-workflow-improvements&quot; tabindex=&quot;-1&quot;&gt;Blog Workflow Improvements&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I set up GitPod and improved the tooling for creating new posts. I am hoping that this reduces the activation energy for writing new posts significantly!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/08/blog-workflow-improvements/blog-workflow-new-life.jpg&quot; alt=&quot;blog-workflow-new-life.jpg&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;h4 id=&quot;table-of-contents&quot; tabindex=&quot;-1&quot;&gt;Table of Contents:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#why-dont-i-post-here-frequently&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Why Don&#39;t I Post here Frequently?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#what-to-do&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;What to do?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#content-management-systems&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Content Management Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#containerized--remote-dev-environment&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Containerized / Remote Dev Environment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#additional-tweaks&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Additional Tweaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#final-thoughts&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-don-t-i-post-here-frequently&quot; tabindex=&quot;-1&quot;&gt;Why Don&#39;t I Post here Frequently?&lt;/h2&gt;
&lt;p&gt;If you look at the history of articles on this site, it is clear that I haven&#39;t done a great job staying consistent with posting. This is in contrast to my YouTube channel and Twitter.&lt;/p&gt;
&lt;p&gt;When I thought about why that was, a big reason was that writing posts felt like a bit of a chore. There were several manual steps in the process and the just wasn&#39;t very fun. Here is what my old process looked like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open up VS Code&lt;/li&gt;
&lt;li&gt;Navigate to the &lt;code&gt;devops-directive-website&lt;/code&gt; project&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;hugo new &amp;lt;POST_NAME&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Manually change the posts from a single markdown file to a directory&lt;/li&gt;
&lt;li&gt;Start up the Hugo development server&lt;/li&gt;
&lt;li&gt;Write the post&lt;/li&gt;
&lt;li&gt;Commit associated files to GitHub&lt;/li&gt;
&lt;li&gt;GitHub Actions takes over from here&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This workflow wasn&#39;t terrible... but there were some pain points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It only works on my laptop with the environment preconfigured. What if I want to write a post from my iPad or a different computer?&lt;/li&gt;
&lt;li&gt;Writing a blog post &lt;em&gt;feels&lt;/em&gt; different than writing code and requires a different mindset. I would prefer if the workflow could be isolated such that I can go into &amp;quot;writing mode&amp;quot;.&lt;/li&gt;
&lt;li&gt;The manual manipulation of new post files.&lt;/li&gt;
&lt;li&gt;I didn&#39;t have spelling/grammar checking set up which lead to lower quality writing with more errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-to-do&quot; tabindex=&quot;-1&quot;&gt;What to do?&lt;/h2&gt;
&lt;h3 id=&quot;content-management-systems&quot; tabindex=&quot;-1&quot;&gt;Content Management Systems&lt;/h3&gt;
&lt;p&gt;As I tried to determine how to best address the first two pain points, I considered all sorts of things. The obvious solution would be to use a web-based content management system (CMS). This could be a fully integrated blogging platform like &lt;a href=&quot;https://wordpress.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;WordPress&lt;/a&gt; or &lt;a href=&quot;https://ghost.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Ghost&lt;/a&gt; or a &amp;quot;headless&amp;quot; CMS that stores the content which is then displayed via a separate frontend.&lt;/p&gt;
&lt;p&gt;These CMSs have a web interface for authoring content which would eliminate the need to have my environment properly set up and would provide a standalone editing experience. I even went so far as to set up both a WordPress and Ghost blog and export/import my content (via RSS feed) into them:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/08/blog-workflow-improvements/ghost.png&quot; alt=&quot;ghost.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Looks pretty good!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This experimentation was reasonably successful and I was able to get my blog ported over and looking decent with some minor theme tweaking.&lt;/p&gt;
&lt;p&gt;That being said, I like the fact that this blog uses static site generation (SSG) which is just a fancy way of saying that rather than storing content in a database and loading it when a user requests the page, the pages are all pre-generated ahead of time. This makes [hosting simple]({{&amp;lt; ref &amp;quot;/posts/2020/10/gcs-cloudflare-hosting/index.md&amp;quot; &amp;gt;}}) and makes handling large traffic spikes like 😅 rather than 🥵.&lt;/p&gt;
&lt;h3 id=&quot;containerized-remote-dev-environment&quot; tabindex=&quot;-1&quot;&gt;Containerized / Remote Dev Environment&lt;/h3&gt;
&lt;p&gt;I gave some more thought to how I could address the pain points without having to abandon my current setup entirely.  How could I achieve that same browser-based, isolated writing environment while still using Hugo as the blogging engine? The answer was a remote development environment.&lt;/p&gt;
&lt;p&gt;In the past few years, products like &lt;a href=&quot;https://gitpod.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Gitpod&lt;/a&gt; and &lt;a href=&quot;https://github.com/features/codespaces&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub CodeSpaces&lt;/a&gt; have come along, offering development environments that can be spun up quickly with all of the necessary dependencies/configurations and torn down when not in use. The Gitpod free tier allows for 50hrs/month.&lt;/p&gt;
&lt;p&gt;The only external dependency my dev environment really needs is Hugo, so I wrote a script to download it from GitHub and place it into a directory on the PATH:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
set -e -o pipefail

HUGO_VERSION=0.76.5
HUGO_TAR_FILE=hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz

wget https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_TAR_FILE}
tar -xf ${HUGO_TAR_FILE}
sudo mv hugo /usr/local/bin/
rm ${HUGO_TAR_FILE}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then added this script to be run in the &lt;code&gt;.gitpod.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;tasks: 
  - before: ./gitpodinit.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One gotcha I ran into is that I initially ran the script from an &lt;code&gt;init&lt;/code&gt; task rather than the &lt;code&gt;before&lt;/code&gt; task. There is a limitation with &lt;code&gt;init&lt;/code&gt; tasks that any changes made within them only persist within the &lt;code&gt;/workspace&lt;/code&gt; directory (see &lt;a href=&quot;https://www.gitpod.io/docs/prebuilds#workspace-directory-only&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;here&lt;/a&gt;) so moving to &lt;code&gt;/urs/local/bin&lt;/code&gt; didn&#39;t persist.&lt;/p&gt;
&lt;p&gt;I also added some extensions to the &lt;code&gt;.gitpod.yml&lt;/code&gt; file to ensure they are pre-installed in the workspace and that was all the config needed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;vscode:
  extensions:
    - znck.grammarly
    - yzhang.markdown-all-in-one
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In fact, I&#39;m editing this post from a Gitpod environment right now!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/08/blog-workflow-improvements/gitpod-screenshot.png&quot; alt=&quot;gitpod-screenshot.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Whoa, meta...&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;additional-tweaks&quot; tabindex=&quot;-1&quot;&gt;Additional Tweaks&lt;/h2&gt;
&lt;p&gt;As you may have noticed in my &lt;code&gt;.gitpod.yml&lt;/code&gt; code above, I added an extension that uses Grammarly to check for spelling and grammar issues, so that solves another one of my problems from before.&lt;/p&gt;
&lt;p&gt;Finally, I actually read the Hugo documentation to set up a &lt;a href=&quot;https://gohugo.io/content-management/archetypes/#directory-based-archetypes&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;directory-based archetype&lt;/a&gt; and a new/improved &lt;code&gt;Make&lt;/code&gt; target for creating new posts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Make&quot;&gt;.PHONY: create-dir-post 
create-dir-post: check-post-name
	git checkout -b $(POST_NAME)
	hugo new -k=dir-post $(POST_PATH) 
	code content/$(POST_PATH)/index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, it checks out a new git branch, creates a post using the directory-based archetype I set up, and opens the &lt;code&gt;index.md&lt;/code&gt; file so I can start editing right away. No more fiddling with moving/renaming files, etc...&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts&quot; tabindex=&quot;-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;I am hopeful that this new workflow will make it easier for me to write articles and therefore I will do so more frequently. Only time will tell! 🤞&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2022/08/blog-workflow-improvements/" />
        </entry>
        <entry>
            <title>
                stages-of-internal-development-systems
                
            </title>
            <updated>2025-07-09T02:19:33.546Z</updated>
            <id>https://devopsdirective.com/posts/2022/04/stages-of-internal-development-systems/</id>
            <content type="html">
                &lt;h1 id=&quot;the-six-stages-of-internal-development-platforms&quot; tabindex=&quot;-1&quot;&gt;The Six Stages of Internal Development Platforms&lt;/h1&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;you-either-die-supporting-raw-kubernetes-manifests-or-live-long-enough-to-see-yourself-build-an-internal-developer-platform-anonymous&quot; tabindex=&quot;-1&quot;&gt;You either die supporting raw Kubernetes manifests, or live long enough to see yourself build an internal developer platform. ~Anonymous&lt;/h3&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/04/stages-of-internal-development-platforms/internal-development-platform.jpeg&quot; alt=&quot;internal-development-platform.jpeg&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt;
&lt;hr /&gt;
&lt;h4 id=&quot;table-of-contents&quot; tabindex=&quot;-1&quot;&gt;Table of Contents:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#what-the-heck-is-an-internal-development-platform&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;What the Heck is an Internal Development Platform?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#why-am-i-hearing-about-this-now&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Why am I hearing about this now?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#designing-the-right-idp&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Designing the Right IDP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#evolution-of-an-idp&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Evolution of an IDP&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-0-external-platform-as-a-service&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 0: External Platform as a Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-1-vanilla-kubernetes&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 1: Vanilla Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-2-templating-tools&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 2: Templating Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-3-initial-internal-developer-platform&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 3: Initial Internal Developer Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-4-extending-existing-platform-building-blocks&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 4: Extending Existing Platform Building Blocks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-5-optional-inventing-new-platform-building-blocks&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 5 (Optional): Inventing New Platform Building Blocks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#stage-6-full-internal-platform-as-a-service&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Stage 6: Full Internal Platform as a Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#go-forth-and-conquer&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Go Forth and Conquer!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-the-heck-is-an-internal-development-platform&quot; tabindex=&quot;-1&quot;&gt;What the Heck is an Internal Development Platform?&lt;/h2&gt;
&lt;p&gt;An Internal Development Platform (IDP) is the system that your organization uses to facilitate developing, deploying, and operating applications. The IDP defines how application developers create, modify, and test their applications, ideally in a self-service manner.&lt;/p&gt;
&lt;p&gt;Instead of just handing application teams an AWS root account and saying &lt;strong&gt;&amp;quot;have fun&amp;quot;&lt;/strong&gt;, IDPs seek to provide abstractions that enable those teams to focus on building products, without needing to become DevOps/cloud experts!&lt;/p&gt;
&lt;p&gt;As outlined by {{&amp;lt; link &amp;quot;&lt;a href=&quot;https://internaldeveloperplatform.org/core-components/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://internaldeveloperplatform.org/core-components/&lt;/a&gt;&amp;quot; &amp;quot;&lt;a href=&quot;http://internaldeveloperplatform.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;internaldeveloperplatform.org&lt;/a&gt;&amp;quot; &amp;gt;}}, the 5 core components of an Internal development platform are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Application Configuration Management:&lt;/strong&gt; Manage application configuration in a scalable and reliable way&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure Orchestration:&lt;/strong&gt; Integrate with your existing and future infrastructure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment Management:&lt;/strong&gt; Enable developers to create new environments whenever needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment Management:&lt;/strong&gt; Implement a Continuous Delivery or even Continuous Deployment (CD) approach&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role-Based Access Control:&lt;/strong&gt; Manage who can do what in a scalable way&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everywhere I turn these days I see another article/talk about companies building internal development platforms on top of Kubernetes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2022-03:&lt;/strong&gt; &lt;a href=&quot;https://slack.engineering/applying-product-thinking-to-slacks-internal-compute-platform/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Applying Product Thinking to Slack’s Internal Compute Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2022-02:&lt;/strong&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=j5i00z3QXyU&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;DevOps MUST Build Internal Developer Platform (IDP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2022-01:&lt;/strong&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=euDC19oknw4&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Your Internal Developer Platform s**ks!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2020-05:&lt;/strong&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=z3nFTi5-b3A&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;More Power, Less Pain: Building an Internal Platform with CNCF Tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This concept is not actually new! &lt;a href=&quot;https://twitter.com/kelseyhightower&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kelsey Hightower&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/jbeda&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Joe Beda&lt;/a&gt; (and others) have been telling us that Kubernetes is a platform for building platforms since 2017!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/04/stages-of-internal-development-platforms/kelsey-tweet.png&quot; alt=&quot;kelsey-tweet.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;It&#39;s pretty dope... but it&#39;s still not the endgame&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/QQsq2Ny5a4A?t=1151&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/04/stages-of-internal-development-platforms/beda-video.png&quot; alt=&quot;beda-video.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;em&gt;&amp;quot;How can we turn Kubernetes into a platform for building platforms?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-am-i-hearing-about-this-now&quot; tabindex=&quot;-1&quot;&gt;Why am I hearing about this now?&lt;/h2&gt;
&lt;p&gt;At this point you might ask, what is fueling this renaissance of companies building (and publicly talking about) Internal Developer Platforms? In my opinion, it is driven by the rapid maturation of the cloud-native tools that represent the building blocks for these types of platforms. This makes it feasible for smaller teams to build and support platforms that actually work and add business value.&lt;/p&gt;
&lt;p&gt;That being said, the {{&amp;lt; link &amp;quot;&lt;a href=&quot;https://landscape.cncf.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://landscape.cncf.io/&lt;/a&gt;&amp;quot; &amp;quot;Cambrian explosion&amp;quot; &amp;gt;}} of cloud-native tools is both a blessing and a curse. While it is more likely than ever that a tool exists that somewhat addresses your particular need, choosing between them and ensuring interoperability is becoming increasingly difficult! (more on this in a future post... {{&amp;lt; link &amp;quot;&lt;a href=&quot;https://roamresearch.com/#/app/devopsdirective/page/xi2CaPv5i&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://roamresearch.com/#/app/devopsdirective/page/xi2CaPv5i&lt;/a&gt;&amp;quot; &amp;quot;but you can find some raw thoughts here&amp;quot; &amp;gt;}})&lt;/p&gt;
&lt;h2 id=&quot;designing-the-right-idp&quot; tabindex=&quot;-1&quot;&gt;Designing the Right IDP&lt;/h2&gt;
&lt;p&gt;The size and complexity of the appropriate platform to suit your organization depends on three main factors:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The number and nature of the applications it needs to support&lt;/li&gt;
&lt;li&gt;The size and skill set of the application development team(s)&lt;/li&gt;
&lt;li&gt;The size and skill set of the platform development team&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Building an internal development process is generally an iterative process and ideally, you will have some early adopter application developers willing to use it as soon as possible and provide feedback to ensure it is meeting their needs effectively.&lt;/p&gt;
&lt;p&gt;Below is a rough spectrum (scale intentionally vague) of the different stages an internal developer platform often exhibits within an organization. This is based on my experience working with teams as they build out these types of application platforms and from discussions with others doing the same.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2022/04/stages-of-internal-development-platforms/idp-spectrum.png&quot; alt=&quot;idp-spectrum.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;evolution-of-an-idp&quot; tabindex=&quot;-1&quot;&gt;Evolution of an IDP&lt;/h2&gt;
&lt;h3 id=&quot;stage-0-external-platform-as-a-service&quot; tabindex=&quot;-1&quot;&gt;Stage 0: External Platform as a Service&lt;/h3&gt;
&lt;p&gt;For very small teams, you likely should avoid building your own platform entirely, and use a 3rd party Platform as a Service (PaaS) while focusing on product iteration and achieving product market fit. There are many options to choose from here including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.heroku.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Heroku&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cloudfoundry.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloud Foundry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://render.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Render&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/appengine&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;App Engine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eventually, though, it is likely that either:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your workloads no longer fit the constraints imposed by the PaaS 📦&lt;/li&gt;
&lt;li&gt;The PaaS pricing begins to hurt your wallet a bit too much 💸&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;at which point you may consider migrating off the PaaS, and in the past few years Kubernetes has become the &lt;a href=&quot;https://www.cockroachlabs.com/guides/kubernetes-trends/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;de facto leader&lt;/a&gt; for doing so!&lt;/p&gt;
&lt;h3 id=&quot;stage-1-vanilla-kubernetes&quot; tabindex=&quot;-1&quot;&gt;Stage 1: Vanilla Kubernetes&lt;/h3&gt;
&lt;p&gt;When you first move to Kubernetes and are still figuring out how to migrate things, you probably won&#39;t have built out additional abstractions. You will use the raw Kubernetes resource types directly and interact with the cluster via &lt;code&gt;kubectl&lt;/code&gt;. This stage generally won&#39;t last long before moving on to the next stage.&lt;/p&gt;
&lt;h3 id=&quot;stage-2-templating-tools&quot; tabindex=&quot;-1&quot;&gt;Stage 2: Templating Tools&lt;/h3&gt;
&lt;p&gt;Very early on you will come across scenarios where you want to have variants of applications running in different environments. There are a variety of ways to accomplish this, ranging from custom bash scripts using &lt;code&gt;envsubst&lt;/code&gt; to more robust tools such as &lt;code&gt;helm&lt;/code&gt;. Your application developers will still need some familiarity with Kubernetes to be successful at this stage.&lt;/p&gt;
&lt;h3 id=&quot;stage-3-initial-internal-developer-platform&quot; tabindex=&quot;-1&quot;&gt;Stage 3: Initial Internal Developer Platform&lt;/h3&gt;
&lt;p&gt;Eventually, as the team and applications grow, creating bespoke definitions for each workload becomes unwieldy. To wrangle this complexity you will likely begin stitching together some collection of tools from the &lt;a href=&quot;https://landscape.cncf.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;CNCF ecosystem&lt;/a&gt; to help.&lt;/p&gt;
&lt;p&gt;Much of the work at this stage is selecting the tools that best suit your team&#39;s needs and configuring them to play nicely together. This is also the stage where you start to bake more control/opinions into the platform itself and you shape the application developer experience (for better or for worse).&lt;/p&gt;
&lt;p&gt;There are several companies and projects such as &lt;a href=&quot;https://backstage.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;backstage&lt;/a&gt;, &lt;a href=&quot;https://humanitec.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;humanitec&lt;/a&gt;, and &lt;a href=&quot;https://shipa.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;shipa&lt;/a&gt; that are attempting to help make rolling an IDP easier. It&#39;s worth evaluating these options to see if they will suit your need and/or for inspiration in designing your own.&lt;/p&gt;
&lt;h3 id=&quot;stage-4-extending-existing-platform-building-blocks&quot; tabindex=&quot;-1&quot;&gt;Stage 4: Extending Existing Platform Building Blocks&lt;/h3&gt;
&lt;p&gt;As you iterate on your platform, you will almost certainly start to push the boundaries of what existing tools can support or encounter use cases that aren&#39;t quite supported. In this case, you may start to extend those tools (either via a wrapper or a fork) to meet your own needs.&lt;/p&gt;
&lt;h3 id=&quot;stage-5-optional-inventing-new-platform-building-blocks&quot; tabindex=&quot;-1&quot;&gt;Stage 5 (Optional): Inventing New Platform Building Blocks&lt;/h3&gt;
&lt;p&gt;At a certain scale/complexity, there might not be an existing tool/project and your custom scripts and glue code are no longer enough. At that point, you might choose to build a new tool to fill the gap.&lt;/p&gt;
&lt;h3 id=&quot;stage-6-full-internal-platform-as-a-service&quot; tabindex=&quot;-1&quot;&gt;Stage 6: Full Internal Platform as a Service&lt;/h3&gt;
&lt;p&gt;At this point, you have come full circle! While the platform will never be &lt;strong&gt;complete&lt;/strong&gt;, a mature internal developer platform starts to look and feel a lot like the 3rd party PaaS you started with, except it has all the specific features your organization needs.&lt;/p&gt;
&lt;h2 id=&quot;go-forth-and-conquer&quot; tabindex=&quot;-1&quot;&gt;Go Forth and Conquer!&lt;/h2&gt;
&lt;p&gt;Building an Internal Developer Platform is a significant undertaking, but if done properly can be a force multiplier for application developers and reduce the operational toil of building and operating applications on Kubernetes.&lt;/p&gt;
&lt;p&gt;The sooner you start to think about what your ideal IDP might eventually look like, the easier it will be to make decisions that will help you set a course to reach that desired state.&lt;/p&gt;
&lt;p&gt;Good luck! 🍀&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2022/04/stages-of-internal-development-systems/" />
        </entry>
        <entry>
            <title>
                cloud-control-spectrum
                
            </title>
            <updated>2025-07-09T02:15:01.068Z</updated>
            <id>https://devopsdirective.com/posts/2022/04/cloud-control-spectrum/</id>
            <content type="html">
                &lt;h1 id=&quot;cloud-control-spectrum-how-should-i-deploy-my-web-application&quot; tabindex=&quot;-1&quot;&gt;Cloud Control Spectrum: How Should I Deploy My Web Application?&lt;/h1&gt;
&lt;p&gt;There are seemingly infinite ways to deploy web applications to the cloud:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#-on-premise&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🏢 On-Premise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#-colocation&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🤝 Colocation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#-bare-metal-cloud&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🤘 Bare Metal Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#-dedicated-hostsinstances&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;📜 Dedicated Hosts/instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#-virtual-machines&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🤖 Virtual Machines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#%EF%B8%8F-managed-services&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;⚙️ Managed Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#-platform-as-a-service&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;✨ Platform as a Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which option is right for you? Read on to find out!&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;h2 id=&quot;on-premise&quot; tabindex=&quot;-1&quot;&gt;🏢 On-Premise&lt;/h2&gt;
&lt;p&gt;If you want full control over everything, you can buy servers (computers), hook them up to power, set up networking, install any dependencies, and then deploy your software onto those systems.&lt;/p&gt;
&lt;p&gt;Operating an on-premise deployment with high security and reliability comes with significant overhead and is generally only appropriate for large enterprises.&lt;/p&gt;
&lt;p&gt;You are responsible for everything from physical security to climate control.&lt;/p&gt;
&lt;h2 id=&quot;colocation&quot; tabindex=&quot;-1&quot;&gt;🤝 Colocation&lt;/h2&gt;
&lt;p&gt;If you want full control, but don&#39;t want to own and operate your own data center, colocation offloads that responsibility to a service provider. You still buy the servers but set them up in a building owned/operated by another company.&lt;/p&gt;
&lt;p&gt;They will charge you rent for the space as well as a fee to cover utilities and other costs to operate the building.&lt;/p&gt;
&lt;p&gt;This option can make sense if you have stable compute needs and are able to fully utilize the capacity you buy but don&#39;t want to deal with all of the operations.&lt;/p&gt;
&lt;h2 id=&quot;bare-metal-cloud&quot; tabindex=&quot;-1&quot;&gt;🤘 Bare Metal Cloud&lt;/h2&gt;
&lt;p&gt;If you don&#39;t want to take on the capital expense of the servers, there are cloud providers that allow you to rent servers directly.&lt;/p&gt;
&lt;p&gt;&amp;quot;Bare metal&amp;quot; refers to the fact that you interact with the server directly (instead of through a virtualization layer).&lt;br /&gt;
With bare metal servers, you are responsible for installing the OS, any dependencies, and your software.&lt;/p&gt;
&lt;p&gt;If you mess something up, you may need to wipe the system and reinstall the OS from scratch (which can take a while). Bare metal servers have some performance benefits over virtual machines but are less dynamic.&lt;/p&gt;
&lt;p&gt;This can be a good option if you have performance-critical workloads but your compute needs are variable and you don&#39;t want to buy your own servers!&lt;/p&gt;
&lt;h2 id=&quot;dedicated-hosts-instances&quot; tabindex=&quot;-1&quot;&gt;📜 Dedicated Hosts/instances&lt;/h2&gt;
&lt;p&gt;Adding a virtualization layer, but renting a specific server(s) from a cloud provider makes it easier to create/destroy/configure servers while retaining hardware-level isolation from other customers (e.g. nobody else is running on the same servers).&lt;/p&gt;
&lt;p&gt;You choose or create an &amp;quot;image&amp;quot; with an operating system (and potentially some dependencies) installed and the cloud provider creates a virtual server for you.&lt;br /&gt;
You then install additional dependencies and your software OR you might pre-build them into the image.&lt;/p&gt;
&lt;p&gt;Dedicated hosts tend to cost more than standard virtual machines (next in this thread) but make sense in two cases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Some software licenses charge you based on specific hardware usage so you need them&lt;/li&gt;
&lt;li&gt;Some industries require data isolation guarantees&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;virtual-machines&quot; tabindex=&quot;-1&quot;&gt;🤖 Virtual Machines&lt;/h2&gt;
&lt;p&gt;Similar to dedicated hosts, you choose or create an &amp;quot;image&amp;quot; with an OS (and potentially some dependencies) installed and the cloud provider creates a virtual server for you.&lt;/p&gt;
&lt;p&gt;The main difference here is that standard VMs are cheaper and have no guarantees about which physical server your deployment goes on.&lt;/p&gt;
&lt;p&gt;This is a great option to keep full control over the configuration but retain the flexibility to create and destroy servers quickly and easily!&lt;/p&gt;
&lt;h2 id=&quot;managed-services&quot; tabindex=&quot;-1&quot;&gt;⚙️ Managed Services&lt;/h2&gt;
&lt;p&gt;Many cloud providers offer specific products that further take over some of the management responsibility for portions of your application.&lt;/p&gt;
&lt;p&gt;For example, AWS offers Relational Database Service (RDS) which provides a managed SQL database.&lt;/p&gt;
&lt;p&gt;This allows you to avoid setting up and configuring your own database (with high availability, backups, etc).&lt;/p&gt;
&lt;p&gt;You can leverage expertise from the provider, but you don&#39;t control the system so if something goes awry it can be an issue. It also costs more than operating yourself&lt;/p&gt;
&lt;h2 id=&quot;platform-as-a-service&quot; tabindex=&quot;-1&quot;&gt;✨ Platform as a Service&lt;/h2&gt;
&lt;p&gt;If you want to offload all of your deployment/operations, you can use a PaaS offering in which you generally hook up a code repo and let them handle the rest.&lt;br /&gt;
PaaS offerings can be a great choice for small teams who are focused on product iteration.&lt;/p&gt;
&lt;p&gt;When using a PaaS, you are forced to adhere to the guidelines/requirements provided by the provider.&lt;/p&gt;
&lt;p&gt;You give up control and pay a bit extra for the simplicity and streamlined developer experience!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Hopefully, this helps you understand the options and make a choice about what the right option is for you! 🎉&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2022/04/cloud-control-spectrum/" />
        </entry>
        <entry>
            <title>
                tiny-container-image
                
            </title>
            <updated>2025-07-09T02:11:27.801Z</updated>
            <id>https://devopsdirective.com/posts/2021/04/tiny-container-image/</id>
            <content type="html">
                &lt;h1 id=&quot;tiny-container-challenge-building-a-6k-b-containerized-http-server&quot; tabindex=&quot;-1&quot;&gt;Tiny Container Challenge: Building a 6kB Containerized HTTP Server!&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; I set out to build the smallest container image that I could that was still able to do something useful. By taking advantage of multistage builds, the &lt;code&gt;scratch&lt;/code&gt; base image, and a tiny assembly-based HTTP server, I was able to get it down to 6.32kB!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2021/04/tiny-container-image/container-for-ants.png&quot; alt=&quot;container-for-ants.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt;
&lt;hr /&gt;
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#bloated-containers&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Bloated Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#challenge&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#naive-solution&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Naive Solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#smaller-base-image&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Smaller Base Image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#compiled-languages&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Compiled Languages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#multi-stage-builds&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Multi-stage Builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#static-compilation--scratch-image&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Static Compilation + Scratch image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#asm-for-the-win&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;ASM for the Win!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=VG8rZIE8ET8&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2021/04/tiny-container-image/tiny-container-thumbnail.png&quot; alt=&quot;tiny-container-thumbnail.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;em&gt;If you prefer video format, check out the corresponding YouTube video here!&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;bloated-containers&quot; tabindex=&quot;-1&quot;&gt;Bloated Containers&lt;/h2&gt;
&lt;p&gt;Containers are often touted as a silver bullet to solve every challenge associated with operating software. While I love containers, I often encounter container images in the wild that have a variety of issues. One common issue is size, with images sometimes clocking in at multiple GB!&lt;/p&gt;
&lt;p&gt;Because of this, I decided to challenge myself and others to build the smallest possible image.&lt;/p&gt;
&lt;h2 id=&quot;challenge&quot; tabindex=&quot;-1&quot;&gt;Challenge&lt;/h2&gt;
&lt;p&gt;The rules are pretty simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The container should serve the contents of a file over http on the port of your choosing&lt;/li&gt;
&lt;li&gt;No volume mounts are allowed (Also known as &amp;quot;The Marek Rule&amp;quot; 😜)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;naive-solution&quot; tabindex=&quot;-1&quot;&gt;Naive Solution&lt;/h2&gt;
&lt;p&gt;To get a baseline image size, we can use node.js to create a simple server &lt;code&gt;index.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const fs = require(&amp;quot;fs&amp;quot;);
const http = require(&#39;http&#39;);

const server = http.createServer((req, res) =&amp;gt; {
  res.writeHead(200, { &#39;content-type&#39;: &#39;text/html&#39; })
  fs.createReadStream(&#39;index.html&#39;).pipe(res)
})

server.listen(port, hostname, () =&amp;gt; {
  console.log(`Server: http://0.0.0.0:8080/`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and built it into an image starting the official node base image:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;FROM node:14
COPY . .
CMD [&amp;quot;node&amp;quot;, &amp;quot;index.js&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This clocked in at &lt;code&gt;943MB&lt;/code&gt;! 😳&lt;/p&gt;
&lt;h2 id=&quot;smaller-base-image&quot; tabindex=&quot;-1&quot;&gt;Smaller Base Image&lt;/h2&gt;
&lt;p&gt;One of the simplest and most obvious tactics for reducing image size is to use a smaller base image. The official node image has a &lt;code&gt;slim&lt;/code&gt; variant (still debian based, but with fewer dependencies preinstalled) and an &lt;code&gt;alpine&lt;/code&gt; variant based on &lt;a href=&quot;https://alpinelinux.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Alpine Linux&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;node:14-slim&lt;/code&gt; and &lt;code&gt;node:14-alpine&lt;/code&gt; as the base image brought the image size down to &lt;code&gt;167MB&lt;/code&gt; and &lt;code&gt;116MB&lt;/code&gt; respectively.&lt;/p&gt;
&lt;p&gt;Because docker images are additive, with each layer building on the next there isn&#39;t much else we can do to make the node.js solution smaller.&lt;/p&gt;
&lt;h2 id=&quot;compiled-languages&quot; tabindex=&quot;-1&quot;&gt;Compiled Languages&lt;/h2&gt;
&lt;p&gt;To take things to the next level we can move to a compiled language with many fewer runtime dependencies. There are a variety of options, but for building web services, &lt;a href=&quot;https://golang.org/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;golang&lt;/a&gt; is a popular choice.&lt;/p&gt;
&lt;p&gt;I created a basic fileserver &lt;code&gt;server.go&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&amp;quot;fmt&amp;quot;
	&amp;quot;log&amp;quot;
	&amp;quot;net/http&amp;quot;
)

func main() {
	fileServer := http.FileServer(http.Dir(&amp;quot;./&amp;quot;))
	http.Handle(&amp;quot;/&amp;quot;, fileServer)
	fmt.Printf(&amp;quot;Starting server at port 8080&#92;n&amp;quot;)
	if err := http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil); err != nil {
			log.Fatal(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and built it into a container image using the official golang base image:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;FROM golang:1.14
COPY . .
RUN go build -o server .
CMD [&amp;quot;./server&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which clocked in at... &lt;code&gt;818MB&lt;/code&gt;. 😡&lt;/p&gt;
&lt;p&gt;The issue here is that the golang base image has a lot of dependencies installed that are useful when building go software but aren&#39;t needed to run the software.&lt;/p&gt;
&lt;h2 id=&quot;multi-stage-builds&quot; tabindex=&quot;-1&quot;&gt;Multi-stage Builds&lt;/h2&gt;
&lt;p&gt;Docker has a feature called [multi-stage &lt;a href=&quot;https://docs.docker.com/develop/develop-images/multistage-build/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;builds&lt;/a&gt; which makes it easy to build the code in an environment with all of the necessary dependencies, and then copy the resulting executable into a different image.&lt;/p&gt;
&lt;p&gt;This is useful for a variety of reasons, but one of the most obvious is image size! By refactoring the Dockerfile as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;### build stage ###
FROM golang:1.14-alpine AS builder
COPY . .
RUN go build -o server .

### run stage ###
FROM alpine:3.12
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD [&amp;quot;./server&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting image is just &lt;code&gt;13.2MB&lt;/code&gt;! 🙂&lt;/p&gt;
&lt;h2 id=&quot;static-compilation-scratch-image&quot; tabindex=&quot;-1&quot;&gt;Static Compilation + Scratch image&lt;/h2&gt;
&lt;p&gt;13 MB isn&#39;t too bad, but there are still a couple of tricks we can play to make it even smaller.&lt;/p&gt;
&lt;p&gt;There is a base image called &lt;a href=&quot;https://hub.docker.com/_/scratch&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;scratch&lt;/a&gt; which is explicitly empty and has zero size. Because the &lt;code&gt;scratch&lt;/code&gt; has nothing inside, any image built from it must bring all of its necessary dependencies.&lt;/p&gt;
&lt;p&gt;To make this possible with our go-based server, we need to add a few flags to the compilation step to ensure the necessary libraries are statically linked into the executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;### build stage ###
FROM golang:1.14 as builder
COPY . .
RUN go build &#92;
  -ldflags &amp;quot;-linkmode external -extldflags -static&amp;quot; &#92;
  -a server.go

### run stage ###
FROM scratch
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD [&amp;quot;./server&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specifically, we set the link mode to &lt;code&gt;external&lt;/code&gt; and pass the &lt;code&gt;-static&lt;/code&gt; flag to the external linker.&lt;/p&gt;
&lt;p&gt;These two changes bring the image size to &lt;code&gt;8.65MB&lt;/code&gt; 😀&lt;/p&gt;
&lt;h2 id=&quot;asm-for-the-win&quot; tabindex=&quot;-1&quot;&gt;ASM for the Win!&lt;/h2&gt;
&lt;p&gt;An image less than 10MB, written in a language like Go is plenty small for almost any circumstance... but we can go smaller! Github user &lt;a href=&quot;https://github.com/nemasu&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;nemasu&lt;/a&gt; fully functional HTTP server written in assembly on GitHub named &lt;a href=&quot;https://github.com/nemasu/asmttpd&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;assmttpd&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All that was required to containerize it was to install a few build dependencies into the ubuntu base image before running the provided &lt;code&gt;make release&lt;/code&gt; recipe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;### build stage ###
FROM ubuntu:18.04 as builder
RUN apt update
RUN apt install -y make yasm as31 nasm binutils 
COPY . .
RUN make release

### run stage ###
FROM scratch
COPY --from=builder /asmttpd /asmttpd
COPY /web_root/index.html /web_root/index.html
CMD [&amp;quot;/asmttpd&amp;quot;, &amp;quot;/web_root&amp;quot;, &amp;quot;8080&amp;quot;] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting &lt;code&gt;asmttpd&lt;/code&gt; executable is then copied into the scratch image and invoked with the &lt;code&gt;CMD&lt;/code&gt;. This resulted in an image size of just 6.34kB! 🥳&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2021/04/tiny-container-image/image-sizes.png&quot; alt=&quot;image-sizes.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;The progression of container image sizes!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Hopefully, you enjoyed this journey from our initial 943MB Node.js image all the way to this tiny 6.34kB Assembly image and learned some techniques you can apply to make your container images smaller in the future.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2021/04/tiny-container-image/" />
        </entry>
        <entry>
            <title>
                credential-management
                
            </title>
            <updated>2025-07-09T02:09:18.160Z</updated>
            <id>https://devopsdirective.com/posts/2020/11/credential-management/</id>
            <content type="html">
                &lt;h1 id=&quot;how-to-manage-application-secrets-7-levels-of-credential-management&quot; tabindex=&quot;-1&quot;&gt;How to Manage Application Secrets (7 Levels of Credential Management)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; If you develop web applications, inevitably you will have secrets (database credentials, 3rd party API keys, etc...) that you need to manage. I have seen a variety of approaches used here and wanted to walk through them, from least secure to most.&lt;/p&gt;
&lt;p&gt;There are always trade-offs when writing software, and in this case, the tradeoff is between convenience and security. The ideal solution will establish convenient developer workflows while also protecting user data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/11/credential-management/credential-management-meme.png&quot; alt=&quot;credential-management-meme.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level--2-no-authentication&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level -2: No Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level--1-all-passwords--password&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level -1: All Passwords = &amp;quot;password&amp;quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level-0-hardcode-everywhere&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level 0: Hardcode Everywhere&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level-1-move-secrets-into-a-config-file&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level +1: Move Secrets into a Config File&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level-2-encrypt-the-config-file&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level +2: Encrypt the Config File&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level-3-use-a-secret-manager&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level +3: Use a Secret Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#level-4-dynamic-ephemeral-credentials&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Level +4: Dynamic Ephemeral Credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#final-thoughts&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DISCLAIMER:&lt;/strong&gt; &lt;em&gt;Hopefully this is obvious from the tone of my writing, but PLEASE do not use levels -2, -1, or 0 in the real world.&lt;/em&gt; 🙏&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=7NTFZoDpzbQ&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/11/credential-management/credential-managment-thumbnail.png&quot; alt=&quot;credential-managment-thumbnail.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;em&gt;If you prefer video format, check out the corresponding YouTube video here!&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;level-2-no-authentication&quot; tabindex=&quot;-1&quot;&gt;Level -2: No Authentication&lt;/h2&gt;
&lt;p&gt;For lazy people who like to live dangerously, you can simply turn off authentication for all services and cross your fingers 🤞.&lt;/p&gt;
&lt;p&gt;This is the ultimate in convenience as you never have to deal with any secrets. It will be somewhat less convenient when inevitably your data is leaked on the dark web and you have to clean up the ensuing mess.&lt;/p&gt;
&lt;h2 id=&quot;level-1-all-passwords-and-quot-password-and-quot&quot; tabindex=&quot;-1&quot;&gt;Level -1: All Passwords = &amp;quot;password&amp;quot;&lt;/h2&gt;
&lt;p&gt;If it is too hard to remember more than one password... how about setting them all to the same value? As long as you choose something easy to remember, you can then just share it with your teammates verbally and they will be good to go!&lt;/p&gt;
&lt;h2 id=&quot;level-0-hardcode-everywhere&quot; tabindex=&quot;-1&quot;&gt;Level 0: Hardcode Everywhere&lt;/h2&gt;
&lt;p&gt;When I was learning to program, I received the advice to &amp;quot;define things close to where they are used&amp;quot;. Well... you can&#39;t get any closer than hard coding it EXACTLY where it gets used!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const mongoose = require(&#39;mongoose&#39;);

const connectionString = 
  &#39;mongodb://myUser:superSecretPassword@localhost:27017/databaseName&#39;;

mongoose.connect(connectionString, { useNewUrlParser: true }).catch((e) =&amp;gt; {
  console.error(&#39;Connection error&#39;, e.message);
});
const db = mongoose.connection;

module.exports = db;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, jokes aside -- there are &lt;strong&gt;many&lt;/strong&gt; issues here. First, there is no way to avoid checking in the secret to your version control system. Second, it will be a nightmare to maintain because credentials will be sprinkled across many files. Third, there is no way to support multiple environments (separate dev/staging/production).&lt;/p&gt;
&lt;h2 id=&quot;level-1-move-secrets-into-a-config-file&quot; tabindex=&quot;-1&quot;&gt;Level +1: Move Secrets into a Config File&lt;/h2&gt;
&lt;p&gt;Rather than hard code at the point of use, it is better to extract secrets into a separate configuration file and then load them in as environment variables. By doing this, you can treat the credential file as sensitive (including adding to &lt;code&gt;.gitignore&lt;/code&gt; to avoid accidentally checking it into the codebase).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# secrets.env
DB_PASS=superSecretPassword
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const dotenv = require(&#39;dotenv&#39;)
const mongoose = require(&#39;mongoose&#39;);

dotenv.config({ path: &#39;./secrets.env&#39; })

const connectionString = 
  `mongodb://myUser:${process.env.DB_PASS}@localhost:27017/myDatabaseName`;

mongoose.connect(connectionString, { useNewUrlParser: true }).catch((e) =&amp;gt; {
  console.error(&#39;Connection error&#39;, e.message);
});

const db = mongoose.connection;

module.exports = db;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a side project where you are the only person working on it, this method might be sufficient, but it does have the downside of storing the credentials in plain text on your system. Also if you need to share credentials with teammates it is difficult to do so safely.&lt;/p&gt;
&lt;h2 id=&quot;level-2-encrypt-the-config-file&quot; tabindex=&quot;-1&quot;&gt;Level +2: Encrypt the Config File&lt;/h2&gt;
&lt;p&gt;One way to share the sensitive configuration file is to encrypt it. This can be done using a two-way encryption algorithm with a tool such as &lt;code&gt;openssl&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export ENCRYPTION_KEY=Where-am-I-supposed-to-store-this?!

# encrypt
openssl aes-256-cbc -a -salt -in secrets.env -out secrets.env.enc -pass pass:$ENCRYPTION_KEY

# decrypt
openssl aes-256-cbc -d -a -salt -in secrets.env.enc -out secrets.env -pass pass:$ENCRYPTION_KEY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While certainly better than before, this does kick the can down the road a bit because now you have to figure out how to manage/share the encryption key.&lt;/p&gt;
&lt;p&gt;One possible approach is to store it in a shared password manager such as 1Password or use a key management system such as Google Cloud KMS.&lt;/p&gt;
&lt;p&gt;This approach doesn&#39;t provide any ability to monitor when individual developers are accessing the secrets. In certain industries where this type of audit log is required, this could be an issue.&lt;/p&gt;
&lt;h2 id=&quot;level-3-use-a-secret-manager&quot; tabindex=&quot;-1&quot;&gt;Level +3: Use a Secret Manager&lt;/h2&gt;
&lt;p&gt;At this point we have taken the concept of a local configuration file about as far as we can. The next level is to move secrets into a dedicated secret manager. All of the major cloud providers offer a service like this, for example, AWS has the &lt;a href=&quot;https://aws.amazon.com/secrets-manager/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Secrets Manager&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The credentials can then be passed into your application as an environment variable at runtime. I often use the following Makefile snippet to retrieve credentials from GCP as needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PROJECT_ID:=&amp;lt;MY_GCP_PROJECT&amp;gt;
SECRET_NAME:=DB_PASS

define get-secret
$(shell gcloud secrets versions access latest --secret=$(1) --project=$(PROJECT_ID))
endef

run-app:
	@DB_PASS=$(call get-secret,$(DB_PASS))&amp;quot; npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving secrets into a system like this offer many benefits. First, these companies have entire teams of security experts building their products making them highly likely to be more secure than whatever system you roll yourself (analogous to &amp;quot;Don&#39;t roll your own crypto&amp;quot;).&lt;/p&gt;
&lt;p&gt;If you are already using the cloud provider to host your application, the tight integration with Identity and Access Management (IAM) functionality, and audit logging can be big wins from a security perspective.&lt;/p&gt;
&lt;p&gt;The biggest shortfall with this approach is that rotating can still be a hassle, and often leads to a static set of long-lived credentials.&lt;/p&gt;
&lt;h2 id=&quot;level-4-dynamic-ephemeral-credentials&quot; tabindex=&quot;-1&quot;&gt;Level +4: Dynamic Ephemeral Credentials&lt;/h2&gt;
&lt;p&gt;To achieve fully enlightened credential management, we can move to a model of auto-generating credentials for each use case and only allow them to be active for a short period of time. This way if there is a leak, it greatly minimizes the potential blast radius.&lt;/p&gt;
&lt;p&gt;The best implementation of this concept I have seen is &lt;a href=&quot;https://www.vaultproject.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;HashiCorp&#39;s Vault&lt;/a&gt;. You can configure Vault so that whenever an application (or individual) needs access to a resource such as a database. It will create a new username/password and handle deleting those credentials after a pre-specified period of time.&lt;/p&gt;
&lt;p&gt;There is a great talk on YouTube by Bench Accounting showcasing this approach in action (&lt;a href=&quot;https://www.youtube.com/watch?v=Y0SdwZDy20Q&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://www.youtube.com/watch?v=Y0SdwZDy20Q&lt;/a&gt;), along with the corresponding Github Repo (&lt;a href=&quot;https://github.com/BenchLabs/talk-vault-ephemeral-credentials&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://github.com/BenchLabs/talk-vault-ephemeral-credentials&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;To generate new credentials you can make a call to vault such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ vault read database/creds/service-write
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which returns the new username/password along with some metadata:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Key                Value
---                -----
lease_id           database/creds/service-write/lVpzrysA5akqSvjZVtCgx1i9
lease_duration     240h
lease_renewable    true
password           A1a-9yW06ZdVk54I5KnX
username           v-token-service--1luYzAYl7SdMxvdpibYv-1555972312
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving to this approach does require that you truly trust Vault (or whatever tool you use) because you have to grant it &lt;code&gt;root&lt;/code&gt;-level permissions across many of your resources. You should protect those credentials as described in level +3.&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts&quot; tabindex=&quot;-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Hopefully, this overview has helped you understand many of the available options for managing credentials for your web application. The right solution for your project will depend on its scale and the sensitivity of the resources being protected.&lt;/p&gt;
&lt;p&gt;Before you start building your next application, think about where along this scale makes sense for you and your team so that you can keep your application secure.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/11/credential-management/" />
        </entry>
        <entry>
            <title>
                gcs-cloudflare-hosting
                
            </title>
            <updated>2025-07-09T18:25:35.187Z</updated>
            <id>https://devopsdirective.com/posts/2020/10/gcs-cloudflare-hosting/</id>
            <content type="html">
                &lt;h1 id=&quot;static-site-hosting-using-google-cloud-storage-and-cloudflare-with-ssl&quot; tabindex=&quot;-1&quot;&gt;Static Site Hosting Using Google Cloud Storage and Cloudflare (with SSL!)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I used to host my site on a [virtual machine running Caddy webserver]({{&amp;lt; ref &amp;quot;/posts/2020/02/hugo-and-caddy-on-gcp/index.md&amp;quot; &amp;gt;}}), but recently migrated it to Google Cloud Storage with Cloudflare in front of it as a proxy/cache/SSL termination solution. It&#39;s pretty awesome! 😎&lt;/p&gt;
&lt;p&gt;I also recorded a video about this setup on &lt;strong&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=sUr4GBzEqNs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;YouTube&lt;/a&gt;&lt;/strong&gt;. ← Check out the video and subscribe if you are into this sort of thing 🙏&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/10/gcs-cloudflare-hosting/gcs-site.png&quot; alt=&quot;gcs-site.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Does this count as serverless?&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#overview&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Overview&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#cloud-storage&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloud Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#cloudflare&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#setup&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Setup&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1-purchase-a-domain&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1) Purchase a Domain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2-verify-ownership&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2) Verify Ownership&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3-create-a-gcs-bucket&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3) Create a GCS bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4-configure-cloudflare&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4) Configure Cloudflare&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#dns-nameservers&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;DNS Nameservers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#cname-record&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;CNAME Record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#ssl-setting&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;SSL Setting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#page-rules&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Page Rules&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#5-deploy-site&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;5) Deploy Site&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#conclusions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;overview&quot; tabindex=&quot;-1&quot;&gt;Overview&lt;/h1&gt;
&lt;p&gt;If you want to host a static website in 2020, the number of options can be a bit overwhelming. GitHub pages, Netlify, Vercel, oh my! This post describes my current favorite solution for hosting a static website. Here are the attributes of the setup described here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple:&lt;/strong&gt; No servers to maintain&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalable:&lt;/strong&gt; Can handle practically any load you throw at it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Affordable:&lt;/strong&gt; Hosting this site costs me a few pennies per month&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSL:&lt;/strong&gt; The site is served over HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cloud-storage&quot; tabindex=&quot;-1&quot;&gt;Cloud Storage&lt;/h2&gt;
&lt;p&gt;Cloud object storage (S3, GCS, Azure Storage) is a pretty amazing technology. For a few pennies/GB/month you can store files and the cloud provider will serve them with high reliability to pretty much any level of traffic you can throw at it. Also, for nearly 10 years, AWS S3 has provided features to make &lt;a href=&quot;https://aws.amazon.com/blogs/aws/host-your-static-website-on-amazon-s3/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;hosting a website directly from a storage bucket simple&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That being said, those solutions don&#39;t support HTTPS by default. Google Cloud Platform does offer a guide for setting up a &lt;a href=&quot;https://cloud.google.com/storage/docs/hosting-static-website&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;site with GCS with an HTTPS load balancer in front of it&lt;/a&gt;, but the load balancer &lt;a href=&quot;https://cloud.google.com/vpc/network-pricing#lb&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;costs $18/month&lt;/a&gt; at which point this solution becomes significantly less attractive relative to its competition.&lt;/p&gt;
&lt;h2 id=&quot;cloudflare&quot; tabindex=&quot;-1&quot;&gt;Cloudflare&lt;/h2&gt;
&lt;p&gt;Cloudflare is a web-infrastructure company that provides a variety of network-related services, including DNS and content delivery caching. They also have a variety of SSL/TLS encryption options ranging from &amp;quot;Off&amp;quot; to &amp;quot;Full (strict)&amp;quot;. The &amp;quot;Flexible&amp;quot; setting is the one we want here.&lt;/p&gt;
&lt;p&gt;With this setting, SSL termination is done on the Cloudflare server, so the users&#39; traffic is encrypted from their browser to Cloudflare and then requests between Cloudflare and the origin server (in our case the GCS server) will be un-encrypted.&lt;/p&gt;
&lt;p&gt;If you were hosting a site with sensitive information, this would not be the right option, but for a public website (such as this one), this option gives us &lt;a href=&quot;https://doesmysiteneedhttps.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;most of the benefits of HTTPS&lt;/a&gt; without having to set up an SSL certificate on our server!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/10/gcs-cloudflare-hosting/flexible-ssl.png&quot; alt=&quot;flexible-ssl.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Cloudflare flexible SSL&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Cloudflare has the added benefit of automatically caching your content, speeding up delivery and reducing the data egress from GCS.&lt;/p&gt;
&lt;h1 id=&quot;setup&quot; tabindex=&quot;-1&quot;&gt;Setup&lt;/h1&gt;
&lt;h2 id=&quot;1-purchase-a-domain&quot; tabindex=&quot;-1&quot;&gt;1) Purchase a Domain&lt;/h2&gt;
&lt;p&gt;The first step is to purchase a domain. Any domain provider will do, but purchasing with &lt;a href=&quot;https://domains.google/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Google Domains&lt;/a&gt; makes the next step easier or unnecessary.&lt;/p&gt;
&lt;h2 id=&quot;2-verify-ownership&quot; tabindex=&quot;-1&quot;&gt;2) Verify Ownership&lt;/h2&gt;
&lt;p&gt;Before creating a GCS bucket associated with a particular domain, Google requires you to verify ownership of the domain.&lt;/p&gt;
&lt;p&gt;There are a few methods to verify your ownership of the domain. If purchased through Google Domains, this can be done through the [Webmaster Central &lt;a href=&quot;https://www.google.com/webmasters/verification/home&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;page&lt;/a&gt;, otherwise, you can use a &lt;a href=&quot;https://cloud.google.com/identity/docs/verify-domain-txt&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;DNS text record&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;3-create-a-gcs-bucket&quot; tabindex=&quot;-1&quot;&gt;3) Create a GCS bucket&lt;/h2&gt;
&lt;p&gt;After verifying ownership, creating and configuring the bucket can be done using the &lt;a href=&quot;https://cloud.google.com/storage/docs/gsutil&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;gsutil tool&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;	gsutil mb -p $PROJECT_ID -b on gs://$DOMAIN
	gsutil web set -m index.html -e 404.html gs://$DOMAIN
	gsutil iam ch allUsers:legacyObjectReader gs://$DOMAIN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;mb&lt;/code&gt; command creates the bucket, the &lt;code&gt;web set&lt;/code&gt; command configures the main and error pages, and the &lt;code&gt;iam ch&lt;/code&gt; command sets the permissions to enable public file access.&lt;/p&gt;
&lt;h2 id=&quot;4-configure-cloudflare&quot; tabindex=&quot;-1&quot;&gt;4) Configure Cloudflare&lt;/h2&gt;
&lt;h3 id=&quot;dns-nameservers&quot; tabindex=&quot;-1&quot;&gt;DNS Nameservers&lt;/h3&gt;
&lt;p&gt;When you add the domain to Cloudflare, it will prompt you to configure your domain provider to use a pair of Cloudflare nameservers. In my case this was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elma.ns.cloudflare.com
james.ns.cloudflare.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These will be use in place of the default nameservers provided by the domain provider.&lt;/p&gt;
&lt;h3 id=&quot;cname-record&quot; tabindex=&quot;-1&quot;&gt;CNAME Record&lt;/h3&gt;
&lt;p&gt;Because the website content will be served from a Google-managed server at &lt;code&gt;c.storage.googleapis.com&lt;/code&gt; a CNAME record set that maps the custom domain to that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Type   |  Name                 |  Content
------------------------------------------------------
CNAME  |  devopsdirective.com  |  c.storage.googleapis.com  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ssl-setting&quot; tabindex=&quot;-1&quot;&gt;SSL Setting&lt;/h3&gt;
&lt;p&gt;As described above, the site will use the &amp;quot;Flexible&amp;quot; SSL/TLS encryption mode, which can be set under the SSL/TLS tab on Cloudflare.&lt;/p&gt;
&lt;h3 id=&quot;page-rules&quot; tabindex=&quot;-1&quot;&gt;Page Rules&lt;/h3&gt;
&lt;p&gt;This step is optional, but without it, the &lt;code&gt;www&lt;/code&gt; subdomain (e.g. &lt;code&gt;www.devopsdirective.com&lt;/code&gt; won&#39;t work properly).&lt;/p&gt;
&lt;p&gt;I prefer to use the root domain, but if a user adds a &lt;code&gt;www.&lt;/code&gt; prefix I don&#39;t want them to encounter an error. This can be handled by creating the following forwarding rule.&lt;/p&gt;
&lt;p&gt;{{&amp;lt; img &amp;quot;images/page-rule.png&amp;quot; &amp;quot;www page redirect&amp;quot; &amp;gt;}}&lt;/p&gt;
&lt;p&gt;The asterisk (&lt;code&gt;*&lt;/code&gt;) will capture anything after the slash and gets substituted into the redirect URL at the &lt;code&gt;$1&lt;/code&gt;. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://www.devopsdirective.com/about/ -&amp;gt; https://devopsdirective.com/about/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This page rule will only work properly if there is a DNS record for the www subdomain. This is accomplished by creating a dummy A record pointing to 192.0.2.1 (all addresses in the &lt;code&gt;192.0.2.0/24&lt;/code&gt; are assigned to &lt;code&gt;TEST-NET-1&lt;/code&gt; for documentation and example code and don&#39;t actually correspond to a real server)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Type   |  Name                 |  Content
------------------------------------------------------
A      |  www                  |  192.0.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;5-deploy-site&quot; tabindex=&quot;-1&quot;&gt;5) Deploy Site&lt;/h2&gt;
&lt;p&gt;With everything configured, the only thing remaining is to upload the website files into the bucket. The easiest way to do this is using the &lt;code&gt;gsutil rsync&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gsutil -m rsync -d -r $LOCAL_SITE_DIR gs://$DOMAIN
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-m&lt;/code&gt; flag enables multi-threading which can help speed up the execution dramatically for large sets of files&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;-d&lt;/code&gt; flag will delete files from the bucket not found in the &lt;code&gt;$LOCAL_SITE_DIR&lt;/code&gt;. NOTE: If there is a chance the local dir could be empty, this would cause all the bucket contents to be deleted (so use at your own risk!).&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;conclusions&quot; tabindex=&quot;-1&quot;&gt;Conclusions&lt;/h1&gt;
&lt;p&gt;I have been using this approach for a few months now and so far it has been rock solid. I love the simplicity of the setup, especially redeploying via a single &lt;code&gt;rsync&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;Let me know how you end up hosting your next static site and why!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/10/gcs-cloudflare-hosting/" />
        </entry>
        <entry>
            <title>
                stupid-github-actions
                
            </title>
            <updated>2025-07-09T02:04:54.398Z</updated>
            <id>https://devopsdirective.com/posts/2020/07/stupid-github-actions/</id>
            <content type="html">
                &lt;h1 id=&quot;doing-stupid-stuff-with-git-hub-actions&quot; tabindex=&quot;-1&quot;&gt;Doing Stupid Stuff with GitHub Actions&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; DevOps doesn&#39;t have to be all work and no play. I built 5 stupid (but fun!) GitHub actions... because &lt;em&gt;why not&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;The full code for these actions can all be found in this &lt;strong&gt;&lt;a href=&quot;https://github.com/sidpalas/stupid-actions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub repo&lt;/a&gt;&lt;/strong&gt;. I encourage you to fork and/or add issues/PRs with impractical actions of your own!&lt;/p&gt;
&lt;p&gt;I also recorded a video about this project on &lt;strong&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=w7-ugGAYVCo&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;YouTube&lt;/a&gt;&lt;/strong&gt;. ← Check out the video and subscribe if you are into this sort of thing 🙏&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/07/stupid-github-actions/stupid-actions.png&quot; alt=&quot;stupid-actions.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#what-is-github-actions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;What is GitHub Actions?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#the-actions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;The Actions&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1----holiday-reminder&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1 -- Holiday Reminder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2----recursive-action&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2 -- Recursive Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3----exponential-action&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3 -- Exponential Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4----smart-lights&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4 -- Smart Lights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#5----tic-tac-toe&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;5 -- Tic-Tac-Toe&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#closing-thoughts&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-is-git-hub-actions&quot; tabindex=&quot;-1&quot;&gt;What is GitHub Actions?&lt;/h2&gt;
&lt;p&gt;GitHub Actions is a CI/CD platform built into GitHub. It can be used to automate things such as building, testing, and deploying code and can be triggered by any GitHub event.&lt;/p&gt;
&lt;p&gt;There is also a &lt;a href=&quot;https://github.com/marketplace?type=actions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;marketplace&lt;/a&gt; where developers can publish their actions for others to use.&lt;/p&gt;
&lt;p&gt;While I have used many CI/CD systems including Jenkins, Google Cloud Build, and CircleCI, prior to this project I hadn&#39;t explored GitHub Actions, so I thought I would try it out and have some fun along the way.&lt;/p&gt;
&lt;p&gt;Enough preamble, let&#39;s get to the stupid stuff!&lt;/p&gt;
&lt;h2 id=&quot;the-actions&quot; tabindex=&quot;-1&quot;&gt;The Actions&lt;/h2&gt;
&lt;h3 id=&quot;1-holiday-reminder&quot; tabindex=&quot;-1&quot;&gt;1 -- Holiday Reminder&lt;/h3&gt;
&lt;p&gt;Starting simple with this first action, I take advantage of the fact that actions can be triggered on a cron schedule to create the following 10 line action:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: holiday-reminder-happy-new-year
on:
  schedule:
    - cron: &#39;0 0 1 1 *&#39;
jobs:
  happy-new-year:
    runs-on: ubuntu-latest
    steps:
    - name: throw error
      run: exit 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The action will run at midnight on New Year&#39;s day and fail every time due to the non-zero exit code. This will cause GitHub to send me an email wishing me a Happy New Year 🎉🎉🎉.&lt;/p&gt;
&lt;h3 id=&quot;2-recursive-action&quot; tabindex=&quot;-1&quot;&gt;2 -- Recursive Action&lt;/h3&gt;
&lt;p&gt;The next idea was proposed by a friend and former colleague (&lt;a href=&quot;https://scotchka.github.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://scotchka.github.io/&lt;/a&gt;). He suggested that I make an action that triggers itself, thus creating an infinite chain of actions.&lt;/p&gt;
&lt;p&gt;Given that the GitHub documentation explicitly states &lt;a href=&quot;https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&amp;quot;To minimize your GitHub Actions usage costs, ensure that you don&#39;t create recursive or unintended workflow runs,&amp;quot;&lt;/a&gt; this seemed like a sufficiently stupid idea. To achieve this, I created an action triggered by commits that makes and commits a code change of its own.&lt;/p&gt;
&lt;p&gt;There are two interesting parts to this action:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GitHub helps prevent users from accidentally doing this by not triggering actions based on events associated with the default &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;. In order to get around this, I created a &lt;a href=&quot;https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;personal access token&lt;/a&gt;. See the &lt;a href=&quot;https://github.com/sidpalas/stupid-actions/blob/master/.github/workflows/recursive.yml&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;action yaml file&lt;/a&gt; for how this gets used.&lt;/li&gt;
&lt;li&gt;In order to prevent the infinite action chain, I persist a count of the action executions within a file in the repo and increment it with each execution. This allows me to terminate the action chain when I reach a specified limit.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;COUNTER_FILE=./recursive/counter.txt
MAX_COUNT=5

count=$(cat &amp;quot;$COUNTER_FILE&amp;quot;) 
if (( $count &amp;gt; $MAX_COUNT ));
then 
    echo &amp;quot;Count too high... exiting&amp;quot;;
else
    echo &amp;quot;Count okay... continuing&amp;quot;;
    echo $(( $count + 1 )) &amp;gt; $COUNTER_FILE
    git config --global user.email &amp;quot;sid@devopsdirective.com&amp;quot;
    git config --global user.name &amp;quot;sid&amp;quot;
    git add $COUNTER_FILE
    git commit -m &amp;quot;Incremented counter file&amp;quot;
    git push 
fi; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-exponential-action&quot; tabindex=&quot;-1&quot;&gt;3 -- Exponential Action&lt;/h3&gt;
&lt;p&gt;The previous action has the potential to run indefinitely, but only one instance executes at a time, so it could be stopped manually if necessary. What if instead, the action triggered itself multiple times? This way, if it got out of hand there would be no stopping the exponential growth 😰. That sounds dumb... let&#39;s do it!&lt;/p&gt;
&lt;p&gt;The file counter trick from before no longer works because multiple actions would be executing in parallel, all trying to modify and commit simultaneously causing conflicts. Instead, I stored the state in Git tags!&lt;/p&gt;
&lt;p&gt;A random UUID is used as the base for the tag and the current iteration is appended to prevent collisions. The current implementation only supports single digits, but if I make it past 9 iterations of exponential growth that means I messed up 💀.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;function increment_tag_push {
    uuid=$(uuidgen)
    suffix=$(( $count + 1 ))
    tag=$1.$uuid.$suffix
    git tag -a $tag -m &amp;quot;New UUID tag&amp;quot;
    git push origin $tag
}

count=&amp;quot;$(echo -n $GITHUB_REF| tail -c 1)&amp;quot;

echo $GITHUB_REF
echo $count

sleep 10 # In case something goes wrong (this saved me during development 😳)
MAX_COUNT=2

if (( $count &amp;gt; $MAX_COUNT ));
then 
    echo &amp;quot;Count too high... exiting&amp;quot;;
else
    echo &amp;quot;Count okay... continuing&amp;quot;;
    git config --global user.email &amp;quot;sid@devopsdirective.com&amp;quot;
    git config --global user.name &amp;quot;sid&amp;quot;
    for ((i = 1; i &amp;lt;= $1; i++ ));
    do
    increment_tag_push $i
    done
fi; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are looking for a quick way to burn through your 2000 free tier minutes... this is definitely the way to go!&lt;/p&gt;
&lt;h3 id=&quot;4-smart-lights&quot; tabindex=&quot;-1&quot;&gt;4 -- Smart Lights&lt;/h3&gt;
&lt;p&gt;Thus far, these actions have been implemented in small bash scripts. To explore the capabilities of GitHub actions I decided the next action should utilize &lt;a href=&quot;https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Docker&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I had a couple of WiFi smart plugs from VeSync that I received as a gift a few years ago and found a &lt;a href=&quot;https://pypi.org/project/pyvesync/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;python client for their API&lt;/a&gt;. This made it simple to create an action that turns the lights on for a short period after each commit (what better way to incentivize code velocity?!💡)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/07/stupid-github-actions/lights-off.gif&quot; alt=&quot;lights-off.gif&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Lights on... Lights off!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The code for this one isn&#39;t particularly interesting, but because the action is using more than just bash, it requires an &lt;code&gt;action.yml&lt;/code&gt; file in which we can see how inputs get passed into the action:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: &#39;Turn on Lights&#39;
description: &#39;Turn on smart home lights for a few seconds&#39;
inputs:
  VESYNC_PASS:  
    description: password for VESYNC_PASS
    required: true
runs:
  using: &#39;docker&#39;
  image: &#39;Dockerfile&#39;
  env:
    VESYNC_PASS: ${{ inputs.VESYNC_PASS }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-tic-tac-toe&quot; tabindex=&quot;-1&quot;&gt;5 -- Tic-Tac-Toe&lt;/h3&gt;
&lt;p&gt;With the team starting to get a bit burnt out having to commit code constantly just to keep the lights on, I decided to implement a game of Tic-tac-toe to let them burn off some steam. The trick is that the computer player for this game executes within an action!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/07/stupid-github-actions/tic-tac-toe.png&quot; alt=&quot;tic-tac-toe.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Command line interface + board state file&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I wanted to try out the &lt;a href=&quot;https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Javascript runtime&lt;/a&gt;, so I built the game with Node, persisting the board state in a text file. Since the point of this game was to learn about actions the computer&#39;s strategy is random, but this has the added benefit of letting the human win, boosting morale 🤔.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot; tabindex=&quot;-1&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Overall, GitHub Actions turned out to be fairly easy to work with and the option to Dockerize the action steps ensures that it should be able to support pretty much any CI/CD need. Also, GitHub has made sharing and reuse of actions a core part of the experience through the Marketplace.&lt;/p&gt;
&lt;p&gt;While I won&#39;t be rushing off to port existing CI/CD workloads onto GitHub actions, I will certainly consider it for future greenfield projects!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/07/stupid-github-actions/" />
        </entry>
        <entry>
            <title>
                password-protected-website
                
            </title>
            <updated>2025-07-08T20:32:49.696Z</updated>
            <id>https://devopsdirective.com/posts/2020/04/password-protected-website/</id>
            <content type="html">
                &lt;h1 id=&quot;password-protected-website&quot; tabindex=&quot;-1&quot;&gt;password-protected-website&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; If you have a static website that needs to be password protected, using Google&#39;s Identity Aware Proxy along with App Engine is one of the simplest ways to accomplish this.&lt;/p&gt;
&lt;p&gt;If you want to follow along with a site of your own, I have provided a working example in this &lt;strong&gt;&lt;a href=&quot;https://github.com/sidpalas/password-protected-docs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub repo&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; I have also seen &lt;a href=&quot;https://douglasduhaime.com/posts/s3-lambda-auth.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;S3 + Lambda used to accomplish this&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;~[[read-the-docs.png]]&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#the-need&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;The Need&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#other-static-hosting-solutions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Other static hosting solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#handling-passwords&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Handling passwords&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#solution-iap--app-engine&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Solution (IAP + App engine)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#example-site&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Example Site&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#configuring-app-engine&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Configuring App Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#setting-up-iap&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Setting up IAP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#configuring-cicd-with-circleci&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Configuring CI/CD with CircleCI&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#workflow&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1-checkout&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1) checkout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2-install-sphinx&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2) Install Sphinx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3-make-docs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3) Make Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4-deploy-docs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4) Deploy Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#next-steps&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Next steps&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#custom-domain&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Custom Domain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#custom-oauth-screen&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Custom OAuth screen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-need&quot; tabindex=&quot;-1&quot;&gt;The Need&lt;/h2&gt;
&lt;p&gt;A company I have been working with (&lt;a href=&quot;https://gauntlet.network/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Gauntlet Networks&lt;/a&gt;) needed to set up a website containing documentation for the simulation SDK they are building but wanted to restrict access to only their clients and employees.&lt;/p&gt;
&lt;h3 id=&quot;other-static-hosting-solutions&quot; tabindex=&quot;-1&quot;&gt;Other static hosting solutions&lt;/h3&gt;
&lt;p&gt;There are a plethora of high-quality, affordable options (&lt;a href=&quot;https://pages.github.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub Pages&lt;/a&gt;, &lt;a href=&quot;https://www.netlify.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Netlify&lt;/a&gt;, a [Free Tier Cloud VM]({{&amp;lt; ref &amp;quot;/posts/2020/02/hugo-and-caddy-on-gcp/index.md&amp;quot; &amp;gt;}}), or even an &lt;a href=&quot;https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;S3 bucket&lt;/a&gt;), but adding a layer of authentication makes the task significantly more challenging.&lt;/p&gt;
&lt;p&gt;Netlify does offer &lt;a href=&quot;https://docs.netlify.com/visitor-access/password-protection/#site-wide-protection&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;password protection&lt;/a&gt; using basic authentication for their paid plans, but using this would still require distributing the password to the authorized individuals and rotating it whenever it was necessary to revoke anyone&#39;s access.&lt;/p&gt;
&lt;h3 id=&quot;handling-passwords&quot; tabindex=&quot;-1&quot;&gt;Handling passwords&lt;/h3&gt;
&lt;p&gt;Historically, implementing authentication would require adding significant complexity to our website&#39;s architecture. We would need a database of some kind to store the (hashed) passwords and server-side processing to check if a login attempt is valid. We would also probably need to add some form of email confirmation/password reset functionality because people are &lt;a href=&quot;https://xkcd.com/936/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;terrible at remembering their passwords&lt;/a&gt;. All of the sudden the one-hour task of getting the documentation site set up just ballooned into something that could take weeks to accomplish!&lt;/p&gt;
&lt;p&gt;Luckily, we don&#39;t need to take on all of this complexity ourselves. A standard called &lt;a href=&quot;https://en.wikipedia.org/wiki/OAuth&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;OAuth&lt;/a&gt; allows us to leverage the authentication system of another entity (for example, Google) to provide&amp;quot;secure delegated access&amp;quot; to our content.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;solution-iap-app-engine&quot; tabindex=&quot;-1&quot;&gt;Solution (IAP + App engine)&lt;/h2&gt;
&lt;p&gt;OAuth by itself would allow us to set up a sign-in flow using an external authentication provider, but for this use case, we want the process to be completely hands-off.&lt;/p&gt;
&lt;p&gt;This is where Google&#39;s &lt;a href=&quot;https://cloud.google.com/iap/docs/concepts-overview&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Identity Aware Proxy (IAP)&lt;/a&gt; comes in. Cloud IAP uses Google Sign-In and GCP&#39;s Identity and Access Management (IAM) to handle authentication and authorization to control access to GCP resources. Granting access to the site can then be managed using individual Google accounts and/or Google groups.&lt;/p&gt;
&lt;p&gt;Given that Gauntlet was already using Google Cloud Platform, leveraging IAP in conjunction with Google App Engine to host the static site provided a simple, clean solution to satisfy the need.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/password-protected-website/iap-app.png&quot; alt=&quot;iap-app.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;example-site&quot; tabindex=&quot;-1&quot;&gt;Example Site&lt;/h2&gt;
&lt;p&gt;Because a documentation website was the use case that inspired this post, I am using the &lt;a href=&quot;https://www.sphinx-doc.org/en/master/usage/quickstart.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Sphinx Python documentation generator quick start&lt;/a&gt; as the basis for this demo. The static site files will be hosted using App Engine, with CircleCI for CI/CD.&lt;/p&gt;
&lt;h3 id=&quot;configuring-app-engine&quot; tabindex=&quot;-1&quot;&gt;Configuring App Engine&lt;/h3&gt;
&lt;p&gt;Google has documentation outlining how to &lt;a href=&quot;https://cloud.google.com/appengine/docs/standard/python/getting-started/hosting-a-static-website&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;use app engine to create a static site&lt;/a&gt; by adding an &lt;code&gt;app.yaml&lt;/code&gt; file to the codebase that tells App Engine how to interpret URL request paths. Sphinx stores its output in the &lt;code&gt;/docs/build/html&lt;/code&gt; directory by default, so I used the following app.yaml:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /
  static_files: docs/build/html/index.html
  upload: docs/build/html/index.html

- url: /(.*)
  static_files: docs/build/html/&#92;1
  upload: docs/build/html/(.*)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this in place, running the &lt;code&gt;gcloud&lt;/code&gt; app deploy` command within the root of the project deploys the application.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-iap&quot; tabindex=&quot;-1&quot;&gt;Setting up IAP&lt;/h3&gt;
&lt;p&gt;With the website deployed, the next step was to follow Google&#39;s guide to enabling &lt;a href=&quot;https://cloud.google.com/iap/docs/app-engine-quickstart&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;IAP for App Engine&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/password-protected-website/iap-page.png&quot; alt=&quot;iap-page.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Just click the slider!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This involves configuring a few things for the Oauth Consent page (app name, support email, etc...), but the process is relatively painless.&lt;/p&gt;
&lt;p&gt;The final step is to grant the &lt;code&gt;IAP-secured Web App User&lt;/code&gt; access to all authorized individuals. In the screenshot below, I granted access to &lt;code&gt;allAuthenticatedUsers&lt;/code&gt; which allows anyone with a Google account to access it. (&lt;a href=&quot;https://devops-directive-project.uc.r.appspot.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Test it out here!&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/password-protected-website/iap-access.png&quot; alt=&quot;iap-access.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Access can be granted to individuals or groups&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-ci-cd-with-circle-ci&quot; tabindex=&quot;-1&quot;&gt;Configuring CI/CD with CircleCI&lt;/h3&gt;
&lt;p&gt;At this point, the website is live and access controlled, but to minimize future work, it is useful to set up a system to handle continuous integration and deployment.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://circleci.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;CircleCi&lt;/a&gt; offers simple integration with GitHub and a &lt;a href=&quot;https://circleci.com/pricing/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;free tier&lt;/a&gt; that will easily handle a small project like this. The first step is to create a CircleCI account and then grant access to the GitHub repo containing the source files using their &lt;a href=&quot;https://github.com/marketplace/circleci&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub marketplace app&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;workflow&quot; tabindex=&quot;-1&quot;&gt;Workflow&lt;/h4&gt;
&lt;p&gt;To define our workflow, we create a file at &lt;code&gt;.circleci/config.yml&lt;/code&gt; containing two parts, a workflow definition and a job definition.&lt;/p&gt;
&lt;p&gt;We define the workflow using version 2 of the CircleCI API. This workflow tells CircleCi to run the &lt;code&gt;build_and_deploy&lt;/code&gt; job on any push to the &lt;code&gt;master&lt;/code&gt; branch.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build_and_deploy:
        filters:
          branches:
            only:
              - master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we define the &lt;code&gt;build_and_deploy&lt;/code&gt; job. Each CircleCI job runs within a Docker image, and because the goal is to deploy to Google App Engine, the &lt;a href=&quot;https://hub.docker.com/r/google/cloud-sdk/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;google/cloud-sdk&lt;/code&gt;&lt;/a&gt; provides a good starting point.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: 2.1
jobs:
  build_and_deploy:     
    docker:
        - image: google/cloud-sdk:slim
    steps:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The job is broken down into four steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;checkout,&lt;/li&gt;
&lt;li&gt;Sphinx installation,&lt;/li&gt;
&lt;li&gt;Generating the website&lt;/li&gt;
&lt;li&gt;Deploying to App Engine&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h4 id=&quot;1-checkout&quot; tabindex=&quot;-1&quot;&gt;1) checkout&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - checkout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As its name suggests, this step retrieves the source code within the repo and stores it in the working directory.&lt;/p&gt;
&lt;h4 id=&quot;2-install-sphinx&quot; tabindex=&quot;-1&quot;&gt;2) Install Sphinx&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - run:
          name: Install Sphinx
          command: |
            apt install -y python-pip &amp;amp;&amp;amp; &#92;
            python3 -m pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;google/cloud-sdk:slim&lt;/code&gt; container image we are using is based on Debian, so we can install Sphinx and its dependencies by first installing &lt;code&gt;pip&lt;/code&gt; with &lt;code&gt;apt&lt;/code&gt; and then using &lt;code&gt;pip&lt;/code&gt; to install the &lt;code&gt;requirements.txt&lt;/code&gt; file.&lt;/p&gt;
&lt;h4 id=&quot;3-make-docs&quot; tabindex=&quot;-1&quot;&gt;3) Make Docs&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - run:
          name: Make Docs
          command: |
            cd docs &amp;amp;&amp;amp; make html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Sphinx quickstart creates a Makefile within the &lt;code&gt;/docs&lt;/code&gt; directory which can be used to generate the documentation site. This step changes directory into &lt;code&gt;/docs&lt;/code&gt; and executes this make target passing &lt;code&gt;html&lt;/code&gt; as the destination for Sphinx to store its output.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; &lt;code&gt;html&lt;/code&gt; here needs to match the directory used in the App Engine &lt;code&gt;app.yaml&lt;/code&gt; config (&lt;code&gt;make html&lt;/code&gt; will build the site into &lt;code&gt;docs/build/html&lt;/code&gt; where App Engine is configured to find them)&lt;/p&gt;
&lt;h4 id=&quot;4-deploy-docs&quot; tabindex=&quot;-1&quot;&gt;4) Deploy Docs&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;      - run: 
          name: Deploy Docs
          command: |
            echo ${GCLOUD_SERVICE_KEY} &amp;gt; /tmp/sa_key.json
            gcloud auth activate-service-account --key-file=/tmp/sa_key.json
            rm /tmp/sa_key.json
            gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
            gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
            gcloud --quiet app deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final step in the job is to deploy the site. To do this, we first need to create a service account (&lt;a href=&quot;https://console.cloud.google.com/iam-admin/serviceaccounts&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://console.cloud.google.com/iam-admin/serviceaccounts&lt;/a&gt;) and grant it the necessary permissions. Although it seems as though &lt;code&gt;App Engine Deployer&lt;/code&gt; would be sufficient, it turns out there are actually a few additional permissions involved with deploying the site. The screenshot below shows the 5 roles I needed to add to achieve a successful deployment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/password-protected-website/sa-roles.png&quot; alt=&quot;sa-roles.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At this point I created a key for the service account (&lt;code&gt;.json&lt;/code&gt; format) and stored its content as an environment variable in CircleCI. I also stored the Zone and Project ID as environment variables to ensure the deployment was targeting the correct app engine environment.&lt;/p&gt;
&lt;p&gt;There was one final &amp;quot;gotcha&amp;quot; having to do with the service account key. My first attempt tried to pass it into the &lt;code&gt;gcloud auth&lt;/code&gt; command by piping it in as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo ${GCLOUD_SERVICE_KEY} | gcloud auth activate-service-account --key-file=-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This caused &lt;code&gt;gcloud&lt;/code&gt; to assume the key was a &lt;code&gt;.p12&lt;/code&gt; type and fail.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ERROR: (gcloud.auth.activate-service-account) Missing required argument [ACCOUNT]: An account is required when using .p12 keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As shown in the full example at the beginning of the step, I was able to solve this by redirecting the echo output to a temporary file containing the JSON key and passing the file path to &lt;code&gt;gcloud auth&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/password-protected-website/green-build.png&quot; alt=&quot;green-build.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Success!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That&#39;s it! We now have a password-protected website, with CI/CD, that falls within the free usage tiers of GCP + CircleCI. If this site had heavy traffic or data egress it could exceed the free tier but for this internal documentation use case that is unlikely.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id=&quot;next-steps&quot; tabindex=&quot;-1&quot;&gt;Next steps&lt;/h3&gt;
&lt;p&gt;There are a couple of additional steps that I will leave as an exercise to the reader.&lt;/p&gt;
&lt;h4 id=&quot;custom-domain&quot; tabindex=&quot;-1&quot;&gt;Custom Domain&lt;/h4&gt;
&lt;p&gt;By default, App Engine assigns a domain such as &lt;a href=&quot;https://devops-directive-project.uc.r.appspot.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://devops-directive-project.uc.r.appspot.com/&lt;/a&gt; to the website. It is possible to map a custom domain to the site by following this &lt;a href=&quot;https://cloud.google.com/appengine/docs/standard/python/mapping-custom-domains&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;guide from Google&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;custom-o-auth-screen&quot; tabindex=&quot;-1&quot;&gt;Custom OAuth screen&lt;/h4&gt;
&lt;p&gt;The default OAuth consent screen is fairly boring but can be customized with a logo image, application homepage link, etc... from &lt;a href=&quot;https://console.cloud.google.com/apis/credentials/consent&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Google Cloud Console&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/password-protected-website/oauth-screen.png&quot; alt=&quot;oauth-screen.png&quot; /&gt;&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/04/password-protected-website/" />
        </entry>
        <entry>
            <title>
                kubernetes-the-hard-way-03
                
            </title>
            <updated>2025-07-10T13:11:51.358Z</updated>
            <id>https://devopsdirective.com/posts/2020/04/kubernetes-the-hard-way-03/</id>
            <content type="html">
                &lt;h1 id=&quot;kubernetes-the-hard-way-deep-dive-03-provisioning-compute-resources&quot; tabindex=&quot;-1&quot;&gt;Kubernetes the Hard Way Deep Dive -- 03 (Provisioning Compute Resources)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Setting up a Virtual Private Cloud (network), configuring firewall rules, and provisioning the Compute Engine virtual machine instances!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KTHW Lesson:&lt;/strong&gt; &lt;a href=&quot;https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/03-compute-resources.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;03-compute-resources.md&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/kubernetes-the-hard-way-03/gcp-k8s-resources.png&quot; alt=&quot;gcp-k8s-resources.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#networking&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Networking&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#default-vpc&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Default VPC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#custom-vpc--subnets&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Custom VPC + Subnets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#firewall-rules&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Firewall Rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#internal&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Internal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#external&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;External&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#compute-instances-virtual-machines&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Compute Instances (Virtual Machines)&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#pricing&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#vm-configurations&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;VM configurations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#connecting-to-vms&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Connecting to VMs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;networking&quot; tabindex=&quot;-1&quot;&gt;Networking&lt;/h2&gt;
&lt;h3 id=&quot;default-vpc&quot; tabindex=&quot;-1&quot;&gt;Default VPC&lt;/h3&gt;
&lt;p&gt;When the &lt;a href=&quot;https://console.cloud.google.com/apis/api/compute.googleapis.com/overview&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Compute Engine API&lt;/a&gt; is enabled within a GCP project, a default Virtual Private Cloud (named &amp;quot;default&amp;quot;) is created.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/kubernetes-the-hard-way-03/default-vpc.png&quot; alt=&quot;default-vpc.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Default VPC: 22 subnets (1 per GCP region) + 4 firewall rules are created by default!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Using the default network makes getting started with setting things up easy, but also enables any Compute Engine instances using that VPC to make network requests to one another using tcp, udp, or icmp protocols (based on the &lt;code&gt;default-allow-internal&lt;/code&gt; firewall rule).&lt;/p&gt;
&lt;h3 id=&quot;custom-vpc-subnets&quot; tabindex=&quot;-1&quot;&gt;Custom VPC + Subnets&lt;/h3&gt;
&lt;p&gt;To isolate the Kubernetes cluster in this tutorial and minimize the attack surface area, we create a separate VPC with a single subnet and more restrictive firewall rules. Specifying the &lt;code&gt;--subnet-mode custom&lt;/code&gt; when creating the VPC is what prevents GCP from auto-provisioning a subnet for each region. What about IP address range &lt;code&gt;--range&lt;/code&gt; specified (&lt;code&gt;10.240.0.0/24&lt;/code&gt;)?&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/24&lt;/code&gt; is what is known as classless inter-domain routing (CIDR) notation and is a shorthand way of specifying the subnet mask. 24 corresponds to the number of active bits in the subnet mask, so in this case, the subnet mask is: &lt;code&gt;11111111.11111111.11111111.00000000&lt;/code&gt; in binary (or &lt;code&gt;255.255.255.0&lt;/code&gt; in decimal).&lt;/p&gt;
&lt;p&gt;This subnet mask denotes the first 3 octets &lt;code&gt;10.240.0.&lt;/code&gt; will be the same across the entire subnet and the final octet can vary throughout its full range from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;255&lt;/code&gt;. The highest address (&lt;code&gt;10.240.0.255&lt;/code&gt;) is the &amp;quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/Broadcast_address&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;broadcast address&lt;/a&gt;&amp;quot;, leaving 254 open for IP addresses (as Kelsey states).&lt;/p&gt;
&lt;p&gt;For a more in-depth look at Network IDs and Subnet Masks, this YouTube video is a great resource:&lt;/p&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/XQ3T14SIlV4?si=0w8db6hEx_wHIHSN&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;h3 id=&quot;firewall-rules&quot; tabindex=&quot;-1&quot;&gt;Firewall Rules&lt;/h3&gt;
&lt;p&gt;We are also creating two firewall rules, one which will allow internal traffic and another for external traffic.&lt;/p&gt;
&lt;h3 id=&quot;internal&quot; tabindex=&quot;-1&quot;&gt;Internal&lt;/h3&gt;
&lt;p&gt;The internal firewall rule allows tcp, udp, and icmp traffic originating from the IP range specified for the above subnet (&lt;code&gt;10.240.0.0/24&lt;/code&gt;) as well as &lt;code&gt;10.200.0.0/16&lt;/code&gt; which is the CIDR range that will be passed into the &lt;code&gt;kube-controller-manager&lt;/code&gt; to be used for pod IP address assignment.&lt;/p&gt;
&lt;h3 id=&quot;external&quot; tabindex=&quot;-1&quot;&gt;External&lt;/h3&gt;
&lt;p&gt;The external firewall rule (it is external because the filter is &lt;code&gt;0.0.0.0/0&lt;/code&gt; indicating that &lt;strong&gt;any&lt;/strong&gt; originating IP address is acceptable) allows for tcp traffic on port 22 (used for SSH), tcp traffic on port 6443 (used to communicate with the &lt;a href=&quot;https://kubernetes.io/docs/reference/access-authn-authz/controlling-access/#transport-security&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kubernetes API server&lt;/a&gt;), and icmp on any port (used by network devices to diagnose communication issues)&lt;/p&gt;
&lt;h2 id=&quot;compute-instances-virtual-machines&quot; tabindex=&quot;-1&quot;&gt;Compute Instances (Virtual Machines)&lt;/h2&gt;
&lt;p&gt;With the networking configuration set up, we can now provision the Compute Engine Instances (VMs) for the Kubernetes control plane as well as those that will serve as the Kubernetes workers.&lt;/p&gt;
&lt;h3 id=&quot;pricing&quot; tabindex=&quot;-1&quot;&gt;Pricing&lt;/h3&gt;
&lt;p&gt;This tutorial sets up 3 control plane nodes and 3 worker nodes of type &lt;code&gt;n1-standard-1&lt;/code&gt; each with a 200 GB boot disk. looking at GCP&#39;s pricing calculator we can see within the us-central1 region this will cost &lt;a href=&quot;https://cloud.google.com/products/calculator#id=b7572205-01ef-4498-8e16-c11059136362&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;$193.63/month&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;While the Compute Engine instances comprise the bulk of the costs, a load balancer will cost an additional $18.26/month bringing the total to &lt;a href=&quot;https://cloud.google.com/products/calculator/#id=b854a15b-2c79-494a-b38c-ba28b3b084bf&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;$211.89/month&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;NOTE:&lt;/em&gt; This is higher than the number cited in the tutorial because it accounts for the persistent storage charges.&lt;/p&gt;
&lt;h3 id=&quot;vm-configurations&quot; tabindex=&quot;-1&quot;&gt;VM configurations&lt;/h3&gt;
&lt;p&gt;Within the &lt;code&gt;gcloud compute instances create&lt;/code&gt; command, there are a number of command line flags used to configure the virtual machines. Below I have detailed the noteworthy options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--can-ip-forward&lt;/code&gt;: allows the instances to send and receive packets with non-matching destination or source IP addresses.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--private-network-ip 10.240.0.1${i}&lt;/code&gt; substituting in the iterator &lt;code&gt;i&lt;/code&gt; we can see that our controller VMs will have all fall within the range of our subnet from earlier:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Controllers:
  10.240.0.10, 10.240.0.11, 10.240.0.12
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Workers:
  10.240.0.20, 10.240.0.21, 10.240.0.22
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--subnet kubernetes&lt;/code&gt;: references the subnet by name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--metadata pod-cidr=10.200.${i}.0/24&lt;/code&gt; (worker VMs only): This metadata is available to the guest operating system. It will be used in &lt;a href=&quot;https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/09-bootstrapping-kubernetes-workers.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;09-bootstrapping-kubernetes-workers.md&lt;/a&gt; to generate a network configuration file that tells k8s which internal IP addresses to assign to pods.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;connecting-to-v-ms&quot; tabindex=&quot;-1&quot;&gt;Connecting to VMs&lt;/h2&gt;
&lt;p&gt;The use of the &lt;code&gt;gcloud compute ssh&lt;/code&gt; command is straightforward and no additional commentary is needed.&lt;/p&gt;
&lt;p&gt;At this point, we now have networking + compute resources provisioned and configured. Next, we will generate certificates and keys to enable secure communications amongst the machines.&lt;/p&gt;
&lt;p&gt;See you in the next post!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/04/kubernetes-the-hard-way-03/" />
        </entry>
        <entry>
            <title>
                kubernetes-the-hard-way-01-02
                
            </title>
            <updated>2025-07-10T13:09:05.967Z</updated>
            <id>https://devopsdirective.com/posts/2020/04/kubernetes-the-hard-way-01-02/</id>
            <content type="html">
                &lt;h1 id=&quot;kubernetes-the-hard-way-deep-dive-01-and-amp-02-system-set-up&quot; tabindex=&quot;-1&quot;&gt;Kubernetes the Hard Way Deep Dive -- 01 &amp;amp; 02 (System Set Up)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Setting up GCP account, and installing various software prerequisites (&lt;code&gt;gcloud&lt;/code&gt;, &lt;code&gt;tmux&lt;/code&gt;, &lt;code&gt;cfssl&lt;/code&gt; and &lt;code&gt;kubectl&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KTHW Lessons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/01-prerequisites.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;01-prerequisites.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/02-client-tools.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;02-client-tools.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/kubernetes-the-hard-way-01-02/tmux-4pane.png&quot; alt=&quot;tmux-4pane.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Is this the Matrix?&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#gcp--gcloud&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GCP + gcloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#tmux&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;tmux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#cfssl--cfssljson&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;CFSSL &amp;amp; cfssljson&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#kubectl&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;kubectl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;gcp-gcloud&quot; tabindex=&quot;-1&quot;&gt;GCP + gcloud&lt;/h2&gt;
&lt;p&gt;I don&#39;t have anything to add for this step. The instructions are complete and will get your account/system set up to proceed.&lt;/p&gt;
&lt;p&gt;A GCP project needs to be configured with the necessary APIs enabled, and gcloud will allow us to interact with GCP via the command line.&lt;/p&gt;
&lt;h2 id=&quot;tmux&quot; tabindex=&quot;-1&quot;&gt;tmux&lt;/h2&gt;
&lt;p&gt;Embarrassingly, I had never used tmux prior to this. 😳😳😳&lt;/p&gt;
&lt;p&gt;tmux, short for &amp;quot;Terminal Multiplexer&amp;quot; is a program that allows for splitting up your terminal into multiple panes and tabs and managing the associated shell sessions. At first glance, this may sound like the &lt;code&gt;Split Pane&lt;/code&gt; feature of the macOS terminal or the &lt;code&gt;Split Horizontally/Vertically&lt;/code&gt; feature of &lt;a href=&quot;https://iterm2.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;iterm2&lt;/a&gt;, but the difference is that shell sessions are managed independently from the terminal window.&lt;/p&gt;
&lt;p&gt;There are a variety of benefits this enables, but the feature Kelsey is suggesting for this tutorial is the ability to synchronize inputs across multiple terminal panes. This will allow us to &lt;code&gt;ssh&lt;/code&gt; into multiple virtual machines, synchronize those sessions, and then execute a series of commands in parallel.&lt;/p&gt;
&lt;p&gt;To do this we can use the following workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install tmux (&lt;code&gt;$ brew install tmux&lt;/code&gt; if using Homebrew on macOS)&lt;/li&gt;
&lt;li&gt;Start a new tmux session with &lt;code&gt;$ tmux&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Split the current pane twice using &lt;code&gt;Ctrl + b&lt;/code&gt; -&amp;gt; &lt;code&gt;%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Navigate between panes using &lt;code&gt;Ctrl + b&lt;/code&gt; -&amp;gt; &lt;code&gt;←&lt;/code&gt; OR &lt;code&gt;→&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SSH into the relevant virtual machines (&lt;code&gt;gcloud ssh ...&lt;/code&gt;) in each of the panes&lt;/li&gt;
&lt;li&gt;Once connected, synchronize the panes using &lt;code&gt;Ctrl + b&lt;/code&gt; -&amp;gt; &lt;code&gt;:setw synchronize-panes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Execute the tutorial commands&lt;/li&gt;
&lt;li&gt;After finishing, shut down all running tmux sessions using &lt;code&gt;$ tmux kill-server&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is just one very specific workflow using tmux. For more information about getting started with some of the other tmux functionality (and lots of other useful tools/skills adjacent to programming), check out &lt;a href=&quot;https://missing.csail.mit.edu/2020/command-line/#terminal-multiplexers&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;The MIT Missing Semester Course&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;cfssl-and-amp-cfssljson&quot; tabindex=&quot;-1&quot;&gt;CFSSL &amp;amp; cfssljson&lt;/h2&gt;
&lt;p&gt;CFSSL (of which cfssljson is a sub-project) is an open source project from &lt;a href=&quot;https://www.cloudflare.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloudflare&lt;/a&gt; that is &amp;quot;both a command line tool and an HTTP API server for signing, verifying, and bundling TLS certificates&amp;quot;.&lt;/p&gt;
&lt;p&gt;These will be used during &lt;a href=&quot;https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/04-certificate-authority.md&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;04-certificate-authority.md&lt;/a&gt; to provision a certificate authority,  and then use it to generate TLS certificates and private keys for many k8s system components.&lt;/p&gt;
&lt;p&gt;I will go into more detail about what that actually means in the post corresponding to that step in the tutorial. For now, simply installing the software and verifying the versions are correct is sufficient.&lt;/p&gt;
&lt;h2 id=&quot;kubectl&quot; tabindex=&quot;-1&quot;&gt;kubectl&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Kubectl&lt;/code&gt; (It&#39;s okay... nobody else knows &lt;a href=&quot;https://www.youtube.com/watch?v=2wgAIvXpJqU&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;how to pronounce it&lt;/a&gt; either) is a command line tool for interacting with Kubernetes clusters. Once the cluster is up and running, &lt;code&gt;kubectl&lt;/code&gt; will be the primary mechanism for getting information about the cluster (and things running within it), as well deploying and modifying software running within the cluster.&lt;/p&gt;
&lt;p&gt;Kelsey&#39;s instructions for installation are sufficient, but if on MacOS, it can also be installed with Homebrew using:&lt;/p&gt;
&lt;p&gt;$ brew install kubectl&lt;/p&gt;
&lt;p&gt;At this point, your computer should have all the necessary prerequisite software and we can start setting up the cloud resources needed for the cluster.&lt;/p&gt;
&lt;p&gt;See you in the next post!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/04/kubernetes-the-hard-way-01-02/" />
        </entry>
        <entry>
            <title>
                kubernetes-the-hard-way-intro
                
            </title>
            <updated>2025-07-10T13:07:46.676Z</updated>
            <id>https://devopsdirective.com/posts/2020/04/kubernetes-the-hard-way-intro/</id>
            <content type="html">
                &lt;h1 id=&quot;kubernetes-the-hard-way-deep-dive-intro&quot; tabindex=&quot;-1&quot;&gt;Kubernetes the Hard Way Deep Dive -- Intro&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; This is the introduction to a series of posts in which I will work through Kelsey Hightower&#39;s &lt;a href=&quot;https://github.com/kelseyhightower/kubernetes-the-hard-way&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kubernetes The Hard Way&lt;/a&gt;, explaining key concepts in detail along the way.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/04/kubernetes-the-hard-way-intro/kubernetes-horizontal.png&quot; alt=&quot;kubernetes-horizontal.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#my-kubernetes-onboarding&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;My Kubernetes Onboarding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#the-plan&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;The Plan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;my-kubernetes-onboarding&quot; tabindex=&quot;-1&quot;&gt;My Kubernetes Onboarding&lt;/h3&gt;
&lt;p&gt;My first experience with Kubernetes (k8s) was working with an existing cluster deployed using &lt;a href=&quot;https://cloud.google.com/kubernetes-engine&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Google Kubernetes Engine (GKE)&lt;/a&gt;. The &lt;code&gt;yaml&lt;/code&gt; files all looked like gibberish, but I pattern-matched my way to getting a new service up and running.&lt;/p&gt;
&lt;p&gt;Later, I needed to set up a new cluster to support a variety of data analysis workloads and going through the process of setting up + configuring the cluster helped solidify my understanding of the k8s concepts. That being said, I would still come across situations (such as a pod reaching the dreaded &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;Unknown&lt;/code&gt;&lt;/a&gt; phase) that left me yearning for a deeper understanding of what was going on under the hood.&lt;/p&gt;
&lt;h3 id=&quot;the-plan&quot; tabindex=&quot;-1&quot;&gt;The Plan&lt;/h3&gt;
&lt;p&gt;To help illuminate the inner workings of k8s, I am going to take off the GKE training wheels and work my way through the process of setting up a Kubernetes cluster from scratch. &lt;a href=&quot;https://twitter.com/kelseyhightower&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kelsey Hightower&lt;/a&gt;* published a fantastic guide to do exactly this, but it assumes a certain level of understanding of many of the foundational tools/technologies.&lt;/p&gt;
&lt;p&gt;I plan to follow Kelsey&#39;s guide, and document the process as I try to dig one layer deeper to grok exactly what each step is doing and why it is necessary for setting up k8s.&lt;/p&gt;
&lt;p&gt;This series will be most useful for individuals who have worked with k8s, but primarily in a managed configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let&#39;s dive in!&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;If you don&#39;t know who Kelsey Hightower is, do yourself a favor and go watch a few of his presentations &lt;a href=&quot;https://www.youtube.com/results?search_query=kelsey+hightower&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;on YouTube&lt;/a&gt;. You won&#39;t regret it!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/04/kubernetes-the-hard-way-intro/" />
        </entry>
        <entry>
            <title>
                hugo-google-analytics
                
            </title>
            <updated>2025-07-08T20:30:53.660Z</updated>
            <id>https://devopsdirective.com/posts/2020/04/hugo-google-analytics/</id>
            <content type="html">
                &lt;h1 id=&quot;hugo-google-analytics&quot; tabindex=&quot;-1&quot;&gt;hugo-google-analytics&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; If using Google Analytics, most Hugo themes load the tracking script even when running locally. This causes a bunch of bogus page views when writing a new article. Creating separate config files for development and production is an easy way to fix this.&lt;/p&gt;
&lt;p&gt;~[[bogus-google-analytics.png]]&lt;br /&gt;
&lt;em&gt;Whoops... that&#39;s me visiting my site on localhost&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;To solve this, take advantage of the fact that Hugo allows for specifying multiple configuration files. In this case, create a separate &amp;quot;production&amp;quot; &lt;code&gt;config.toml&lt;/code&gt; file without the &lt;code&gt;googleAnalytics&lt;/code&gt; code specified and rearrange the directory structure as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;├── config
│   ├── _default
│   │   └── config.toml
│   └── production
│       └── config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When serving locally with &lt;code&gt;hugo serve&lt;/code&gt; the config in _default will be used, but when generating the site with &lt;code&gt;hugo&lt;/code&gt;, Hugo will default to the production configuration.&lt;/p&gt;
&lt;p&gt;If you do want to test the production configuration locally, you can specify the environment via a command flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;hugo serve --environment production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full documentation for this feature can be found on the &lt;a href=&quot;https://gohugo.io/getting-started/configuration/#configuration-directory&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;gohugo.io website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I hope that helps anyone who encounters the same situation!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/04/hugo-google-analytics/" />
        </entry>
        <entry>
            <title>
                load-testing-f1-micro
                
            </title>
            <updated>2025-07-08T18:55:50.126Z</updated>
            <id>https://devopsdirective.com/posts/2020/03/load-testing-f1-micro/</id>
            <content type="html">
                &lt;h1 id=&quot;load-testing-caddy-web-server-on-a-gcp-f1-micro-instance-using-k6-a-href-http-k6-io-target-blank-class-external-link-k6-io-a&quot; tabindex=&quot;-1&quot;&gt;Load Testing Caddy Web Server on a GCP F1-Micro Instance Using K6 (&lt;a href=&quot;http://k6.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;k6.io&lt;/a&gt;)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I used the &lt;a href=&quot;https://k6.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;K6&lt;/a&gt; load testing framework to benchmark the Compute Engine &lt;a href=&quot;https://cloud.google.com/compute/docs/machine-types#n1_shared-core_machine_types&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;f1-micro&lt;/a&gt; and &lt;a href=&quot;https://caddyserver.com/v1/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Caddy web server&lt;/a&gt; hosting this site. With Cloudflare caching turned off, the server was able to serve an onslaught of 800 virtual users continuously reloading the page (while maintaining a median request duration of &lt;code&gt;&amp;lt;400ms&lt;/code&gt;), but started dropping requests when increasing the load further.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.theverge.com/2016/5/5/11592622/this-is-fine-meme-comic&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/load-testing-f1-micro/this-is-fine.png&quot; alt=&quot;this-is-fine.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;em&gt;This is fine...&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;h4 id=&quot;table-of-contents&quot; tabindex=&quot;-1&quot;&gt;Table of Contents:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#background&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#testing&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Testing&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#site-setup&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Site Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#k6-performance-testing-framework&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;K6 Performance Testing Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#replicating-current-peak&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Replicating Current Peak&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#key-takeaways&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Key Takeaways&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#ramping-it-up&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Ramping It Up!&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#virtual-users-and-server-load&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Virtual Users and Server Load&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#snags-along-the-way&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Snags Along the Way&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#aside-total-costs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;(Aside) Total Costs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#conclusions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;DevOps Directive is a static website generated with &lt;a href=&quot;https://gohugo.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Hugo&lt;/a&gt; and hosted using &lt;a href=&quot;https://caddyserver.com/v1/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Caddy&lt;/a&gt; running on an &lt;a href=&quot;https://cloud.google.com/compute/docs/machine-types#n1_shared-core_machine_types&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;f1-micro&lt;/a&gt; GCP Compute Engine instance with &lt;a href=&quot;https://www.cloudflare.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloudflare&lt;/a&gt; in front of it (see [The Making of This Site]({{&amp;lt; ref &amp;quot;/posts/2020/02/hugo-and-caddy-on-gcp/index.md&amp;quot; &amp;gt;}}) post for details). On a normal day, the site used to get between 1 (&lt;em&gt;thanks Dad!&lt;/em&gt;) and 20 visitors, but recently, two articles made it to the front page of Hacker News ({{&amp;lt; link &amp;quot;&lt;a href=&quot;https://news.ycombinator.com/item?id=22512149&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://news.ycombinator.com/item?id=22512149&lt;/a&gt;&amp;quot; &amp;quot;Link-1&amp;quot; &amp;gt;}} and {{&amp;lt; link &amp;quot;&lt;a href=&quot;https://news.ycombinator.com/item?id=22661029&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://news.ycombinator.com/item?id=22661029&lt;/a&gt;&amp;quot; &amp;quot;Link-2&amp;quot; &amp;gt;}}) bringing outsized swells in traffic.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/load-testing-f1-micro/google-analytics-2-peaks.png&quot; alt=&quot;google-analytics-2-peaks.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;2178% increase from the previous peak!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In a single hour on March 7th, a total of 1307 people visited this site ({{&amp;lt; link &amp;quot;images/realtime-analytics.png&amp;quot; &amp;quot;from all over the world!&amp;quot; &amp;gt;}}). Thankfully, even with all of that traffic, CPU usage of the virtual machine never even reached 10% (and the short spikes correspond to redeploying the site with copy edits).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/load-testing-f1-micro/march-7-traffic.png&quot; alt=&quot;march-7-traffic.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;No flames yet...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At this point, the setup has proven itself capable of embracing a Hacker News hug without dying, but I wanted to get a sense of what kind of load it can handle.&lt;/p&gt;
&lt;h2 id=&quot;testing&quot; tabindex=&quot;-1&quot;&gt;Testing&lt;/h2&gt;
&lt;h3 id=&quot;site-setup&quot; tabindex=&quot;-1&quot;&gt;Site Setup&lt;/h3&gt;
&lt;p&gt;In order to avoid causing any impact to my actual site, I spun up an identical replica on a separate virtual machine using &lt;a href=&quot;https://github.com/sidpalas/hugo-gcp-deploy&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;this script&lt;/a&gt; and configured the &lt;a href=&quot;https://test.devopsdirective.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://test.devopsdirective.com/&lt;/a&gt; subdomain (which will likely be inactive at the time you are reading this) to resolve to it.&lt;/p&gt;
&lt;p&gt;Here is a summary of the configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compute Engine f1-micro Instance (0.2 vCPU burstable to 1 vCPU for short periods, 0.6GB Memory) running Container Optimized OS (COS)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.docker.com/r/abiosoft/caddy/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Caddy (1.0.3) container image&lt;/a&gt; with the site content files built directly into the container&lt;/li&gt;
&lt;li&gt;Cloudflare is configured to proxy traffic and set to the &amp;quot;standard&amp;quot; caching level (I performed tests with caching turned on and caching turned off)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;NOTE:&lt;/em&gt; I didn&#39;t tune/configure the COS image running on the VM, nor specify resource requests in the &lt;code&gt;docker run&lt;/code&gt; command.&lt;/p&gt;
&lt;h3 id=&quot;k6-performance-testing-framework&quot; tabindex=&quot;-1&quot;&gt;K6 Performance Testing Framework&lt;/h3&gt;
&lt;p&gt;To perform the load test I used &lt;a href=&quot;https://k6.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;k6.io&lt;/a&gt;, an open source performance testing framework designed for building automated tests with a straightforward javascript config file. It uses the concept of &amp;quot;virtual users&amp;quot; (VUs) which in &lt;a href=&quot;https://k6.io/docs/getting-started/running-k6#adding-more-vus&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;their words&lt;/a&gt; are &amp;quot;glorified, parallel &lt;code&gt;while(true)&lt;/code&gt; loops&amp;quot; to load test a site.&lt;/p&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/brasMBAezJY?si=T_QeFqWRwjFLYpp9&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
*Want to learn more about K6? Watch this video!*
&lt;h3 id=&quot;replicating-current-peak&quot; tabindex=&quot;-1&quot;&gt;Replicating Current Peak&lt;/h3&gt;
&lt;p&gt;First, I created a script to approximate the load that the two HN posts brought. The peak hour had 1443 page views or 0.4 pageviews/second. To account for the load not being constant across the entire hour, I rounded this up to 1 pageview/second.&lt;/p&gt;
&lt;p&gt;K6 is able to use a &lt;a href=&quot;https://en.wikipedia.org/wiki/HAR_(file_format)&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;HAR&lt;/a&gt; file](&lt;a href=&quot;https://en.wikipedia.org/wiki/HAR_(file_format)&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://en.wikipedia.org/wiki/HAR_(file_format)&lt;/a&gt;) to create a representative set of HTTP requests. I used 1 virtual user and adjusted the pause between iterations to achieve just over 1 pageload/second (with a &amp;quot;pageload&amp;quot; corresponding to the batch of HTTP requests). I excluded external requests for things like the Google Analytics script. The full K6 configuration script can be found as a &lt;a href=&quot;https://gist.github.com/sidpalas/7f284eb88a832ba21190b1b0cd5f5ba9&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub gist&lt;/a&gt; and the resulting output can be seen below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;check_failure_rate.........: 0.00%   ✓ 0   ✗ 138
checks.....................: 100.00% ✓ 414 ✗ 0  
data_received..............: 66 MB   549 kB/s
data_sent..................: 219 kB  1.8 kB/s
group_duration.............: avg=873.64ms min=850.89ms med=864.69ms max=1.13s    p(90)=891.35ms p(95)=908.24ms
http_req_blocked...........: avg=81.77µs  min=156ns    med=446ns    max=133.52ms p(90)=861ns    p(95)=1.42µs  
http_req_connecting........: avg=7.06µs   min=0s       med=0s       max=11.69ms  p(90)=0s       p(95)=0s      
http_req_duration..........: avg=26.89ms  min=12.6ms   med=23.11ms  max=213.29ms p(90)=38.19ms  p(95)=47.38ms 
http_req_receiving.........: avg=726.31µs min=62.01µs  med=242.75µs max=29.96ms  p(90)=1.84ms   p(95)=2.41ms  
http_req_sending...........: avg=52.23µs  min=11.27µs  med=38.27µs  max=4.07ms   p(90)=106.59µs p(95)=115.18µs
http_req_tls_handshaking...: avg=72.85µs  min=0s       med=0s       max=120.64ms p(90)=0s       p(95)=0s      
http_req_waiting...........: avg=26.11ms  min=12.37ms  med=22.51ms  max=212.53ms p(90)=37.05ms  p(95)=45.32ms 
http_reqs..................: 1656    13.799982/s
iteration_duration.........: avg=873.71ms min=850.96ms med=864.75ms max=1.13s    p(90)=891.46ms p(95)=908.31ms
iterations.................: 137     1.141665/s
vus........................: 1       min=1 max=1
vus_max....................: 1       min=1 max=1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaways&quot; tabindex=&quot;-1&quot;&gt;Key Takeaways&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;All requests returned successfully (&lt;code&gt;Status: 200 OK&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The request duration ranged from 13ms to 213ms&lt;/li&gt;
&lt;li&gt;The 95th percentile duration was 47ms&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;ramping-it-up&quot; tabindex=&quot;-1&quot;&gt;Ramping It Up!&lt;/h3&gt;
&lt;p&gt;With that test as a baseline, I proceeded to run a series of tests, each 60 seconds long, starting with 6 virtual users and increasing the number of VUs with each test. I also reduced the delay between virtual user iterations to 100ms. The most important metric is &lt;code&gt;http_req_duration&lt;/code&gt;, which represents is the total request time (&lt;code&gt;http_req_sending + http_req_waiting + http_req_receiving&lt;/code&gt;), which I have plotted below for the full set of tests.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/load-testing-f1-micro/http_req_duration.png&quot; alt=&quot;http_req_duration.png&quot; /&gt;&lt;em&gt;Unsurprisingly... a CDN with caching makes a big difference&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/load-testing-f1-micro/http_req_duration_zoomed.png&quot; alt=&quot;http_req_duration_zoomed.png|http_req_duration_zoomed.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Zoomed to &amp;lt;200VU, &amp;lt;500ms&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Up until around 50 VUs, the response time remains flat, with an uncached median of 68ms and a cached median of 31ms.&lt;/p&gt;
&lt;p&gt;After 50 VUs, the response times begin to climb linearly. At 800 VUs the uncached median was 349ms and the cached median was 67ms. As would be expected at these higher loads, most (90+%) of the &lt;code&gt;http_req_duration&lt;/code&gt; is spent in the &lt;code&gt;http_req_waiting&lt;/code&gt; stage.&lt;/p&gt;
&lt;p&gt;The uncached configuration finally gave out during the 1600 virtual user test, with only 414 successful responses, indicating that ~74% of the virtual users never received a response.&lt;/p&gt;
&lt;h4 id=&quot;virtual-users-and-server-load&quot; tabindex=&quot;-1&quot;&gt;Virtual Users and Server Load&lt;/h4&gt;
&lt;p&gt;It is important to note that while the virtual users run in parallel with each other, they run in serial with themselves. Each individual VU waits until its current page load is complete before making a new set of requests. As the server slows down under load, this causes the total rate of requests to drop in the more demanding tests. The impact is clearly seen in the total amount of data received during the tests plotted below.&lt;/p&gt;
&lt;p&gt;{{&amp;lt; img-link &amp;quot;images/data_received.png&amp;quot; &amp;quot;images/data_received.png&amp;quot; &amp;quot;Data received (and pageloads/s) peaks before the more demanding tests&amp;quot;&amp;gt;}}&lt;/p&gt;
&lt;p&gt;These were the two most informative plots, but all of the data and code to generate plots can be found in a notebook in this &lt;a href=&quot;https://github.com/sidpalas/f1-micro-caddy-benchmark&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub repo&lt;/a&gt;. You can load an interactive copy of the notebook using the following binder link:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mybinder.org/v2/gh/sidpalas/f1-micro-caddy-benchmark/master?filepath=plotting-load-test-results.ipynb&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/managed-kubernetes-comparison/binder.png&quot; alt=&quot;Posts/2020/03/managed-kubernetes-comparison/binder.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://mybinder.org/v2/gh/sidpalas/f1-micro-caddy-benchmark/master?filepath=plotting-load-test-results.ipynb&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://mybinder.org/v2/gh/sidpalas/f1-micro-caddy-benchmark/master?filepath=plotting-load-test-results.ipynb&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;snags-along-the-way&quot; tabindex=&quot;-1&quot;&gt;Snags Along the Way&lt;/h3&gt;
&lt;p&gt;I did run into some technical limitations when configuring and executing these tests. Here are the main issues and how I overcame them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bandwidth Limitations:&lt;/strong&gt; My home internet was not sufficient to support the load test. Moving to a GCP virtual machine with sufficient bandwidth (Measured @ 900+ Mbps) as the test client running K6 solved this. For the later tests in the cached configuration, this actually still became a limiting factor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Memory Limitations:&lt;/strong&gt; After moving from my laptop to an n1-standard-1 instance as the testing client, the more demanding tests caused K6 to run out of memory (&lt;code&gt;fatal error: runtime: out of memory&lt;/code&gt;). Moving to an n1-standard-8 (30GB memory) solved this.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unix Resource Limits:&lt;/strong&gt; Because each request group makes multiple HTTP requests, the final test with 1600 target virtual users surpasses the &lt;a href=&quot;https://k6.io/docs/misc/fine-tuning-os#user-resource-limits&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;default maximum number of open files&lt;/a&gt; allowed by the OS for a single process to manage at once. Using multiple test client VMs in parallel solved this, but increasing the open file limit with &lt;code&gt;ulimit -n &amp;lt;NEW_LARGER_LIMIT&amp;gt;&lt;/code&gt; is the approach I ended up using.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;aside-total-costs&quot; tabindex=&quot;-1&quot;&gt;(Aside) Total Costs&lt;/h2&gt;
&lt;p&gt;The total cost to run this experiment was $2.82:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$1.60 for 40.1 GB of network egress&lt;/li&gt;
&lt;li&gt;$0.50 for running the f1-micro server for a ~3 days&lt;/li&gt;
&lt;li&gt;$0.72 for running the n1-standard-8 testing client for a ~2 hrs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusions&quot; tabindex=&quot;-1&quot;&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;I am continuously amazed at the level of load that even such a tiny virtual machine can handle when serving static content!&lt;/p&gt;
&lt;p&gt;Utilizing a service like Cloudflare to help cache and serve content reduces the load on the server significantly. It cut the response times in half under light load and prevented the server from being overwhelmed under heavy load.&lt;/p&gt;
&lt;p&gt;I would have liked to record real-time resource (CPU + Memory usage) on the server VM but the GCP cloud monitoring agent isn&#39;t compatible with Container Optimized OS, so I settled for the rough 1 min averaged view in the GCP console:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/load-testing-f1-micro/peak-cpu.png&quot; alt=&quot;peak-cpu.png&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Now we&#39;re cooking with gas! (bursting above the 0.2 vCPU limit for a short period)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This test gives me confidence that my current server configuration should be able to handle quite a bit of growth before needing any major overhaul.&lt;/p&gt;
&lt;p&gt;In the future, I hope to do similar benchmarking across other hosting options. If someone has a contact at &lt;a href=&quot;https://twitter.com/github&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;@github&lt;/a&gt; or &lt;a href=&quot;https://twitter.com/Netlify&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;@netlify&lt;/a&gt; that could permit me to run a test against a Github Pages or Netlify Starter site let me know! Or maybe at &lt;a href=&quot;https://twitter.com/bluehost&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;@bluehost&lt;/a&gt; so I can benchmark some WordPress installs...&lt;/p&gt;
&lt;!-- -- Shout out YouTube + call to action --&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/03/load-testing-f1-micro/" />
        </entry>
        <entry>
            <title>
                always-on-minikube
                
            </title>
            <updated>2025-07-08T18:35:37.770Z</updated>
            <id>https://devopsdirective.com/posts/2020/03/always-on-minikube/</id>
            <content type="html">
                &lt;h1 id=&quot;converting-an-old-mac-book-into-an-always-on-personal-kubernetes-cluster&quot; tabindex=&quot;-1&quot;&gt;Converting an Old MacBook Into an Always-On Personal Kubernetes Cluster&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I set up Minikube on an old (2012) MacBook Air and configured it to be able to connect from outside my home network so I would have an always-on Kubernetes cluster at home to experiment with.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/always-on-minikube/macbook-air.jpg&quot; alt=&quot;macbook-air.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo Attribution: &lt;a href=&quot;https://flickr.com/photos/pahudson/5127789519/in/photolist-8P8gb8-afjVVd-sVaUtd-4vxyii-8PbmZQ-4vBEo3-4vxyGR-4vBEx9-4m6Uar-4Ykxb1-4vxysv-4xrcrc-5XtHJY-5Xpuee-5Xpuve-4m9FUL-9dUh84-5XpvMz-5XtHTq-66nicm-4Ygiht-4s33cu-5XtHrb-4YkxAy-8NuFUn-4YkxL9-4Ygi84-7daFrV-4C92kK-4YkM85-7dexwb-5XtJaL-7dexW7-7dexPG-5Xpwax-fA3EnA-5XtKSh-ea6KL6-coVFws-coVJSu-4ud9UZ-5QLfy1-7ViDVD-dfYj9B-68FXdw-68BHUH-68BHzM-68BHEk-68FWYo-68FXCf&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;flickr&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#why&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Why?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#powercost-considerations&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Power/Cost Considerations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#process&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Process&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#0-prevent-server-system-from-sleeping&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;0) Prevent Server System from Sleeping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1-install-minikube-on-the-server-system&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1) Install Minikube on the Server System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2-install-kubectl-on-the-client-system&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2) Install &lt;code&gt;kubectl&lt;/code&gt; on the Client System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3-add-port-forwarding-rule-from-the-server-system-to-the-minikube-vm&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3) Add port forwarding rule from the Server System to the Minikube VM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4-update-the-kubeconfig-on-the-client-to-point-to-the-server&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4) Update the &lt;code&gt;.kube/config&lt;/code&gt; on the Client to Point to the Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#5-optional-use-ddns--router-port-forwarding-to-access-outside-local-network&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;5) (Optional) Use DDNS + Router Port Forwarding to Access Outside Local Network&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#conclusion&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why&quot; tabindex=&quot;-1&quot;&gt;Why?&lt;/h2&gt;
&lt;p&gt;Over the past five years, Kubernetes (k8s) has risen to become the de facto standard for managing container orchestration at scale.&lt;/p&gt;
&lt;p&gt;However, with many layers of abstraction and [non-trivial costs]({{&amp;lt; ref &amp;quot;/posts/2020/03/managed-kubernetes-comparison/index.md&amp;quot; &amp;gt;}}) to run even the smallest cloud-based cluster, the barriers to entry for an individual who wants to familiarize themselves with Kubernetes can be quite high.&lt;/p&gt;
&lt;p&gt;There are a variety of options for running Kubernetes locally including &lt;a href=&quot;https://github.com/kubernetes/minikube&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Minikube&lt;/a&gt;, &lt;a href=&quot;https://www.docker.com/products/kubernetes&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Docker Desktop&lt;/a&gt;, and &lt;a href=&quot;https://microk8s.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Microk8s&lt;/a&gt;. These are all good options for aiding in the development process, but all suffer from the fact that they are tied to the power status of the system they are running on (i.e. if I shut down my laptop, the local cluster shuts down too). In order to run workloads that are either time-dependent (cronjobs) or event-driven (asynchronous data processing), it would be much better to have a cluster that is always on.&lt;/p&gt;
&lt;p&gt;I happen to have a 2012 MacBook Air sitting around unused now that it is no longer my daily driver. Amazingly, this 8-year-old system has a CPU that supports the necessary &lt;a href=&quot;https://en.wikipedia.org/wiki/X86_virtualization&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;software-based virtualization&lt;/a&gt; to run a Linux virtual machine where we can install Kubernetes.&lt;/p&gt;
&lt;h2 id=&quot;power-cost-considerations&quot; tabindex=&quot;-1&quot;&gt;Power/Cost Considerations&lt;/h2&gt;
&lt;p&gt;Because my goal is to achieve a low-cost solution, I wanted to do a quick calculation of how much power this laptop will use if I leave it running 24x7. Based on this &lt;a href=&quot;https://www.apple.com/environment/pdf/products/archive/2012/13inch_macbookair_per_june2012.pdf&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;environmental spec sheet from apple&lt;/a&gt; I estimate the system (with the display off and relatively light workloads) will draw approximately 10W on average.&lt;/p&gt;
&lt;p&gt;{{&amp;lt; img &amp;quot;images/mba-power.png&amp;quot; &amp;gt;}}&lt;/p&gt;
&lt;p&gt;Pricing Estimate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;  10 (W) 
x 1/1000 (W/kW) 
x 24 (hrs/day) 
x 30.4 (days/month) 
x 0.2 ($/kWh)
---
1.44 $/month -- 👍
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;process&quot; tabindex=&quot;-1&quot;&gt;Process&lt;/h2&gt;
&lt;h3 id=&quot;0-prevent-server-system-from-sleeping&quot; tabindex=&quot;-1&quot;&gt;0) Prevent Server System from Sleeping&lt;/h3&gt;
&lt;p&gt;Because the MacBook Air is not designed to be used in this configuration, it is necessary to disable sleep using the power management settings (&lt;code&gt;pmset&lt;/code&gt;) command line utility. The following commands will make the time to sleep infinite as well as prevent sleeping when there is no display connected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo pmset -a sleep 0; 
sudo pmset -a disablesleep 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1-install-minikube-on-the-server-system&quot; tabindex=&quot;-1&quot;&gt;1) Install Minikube on the Server System&lt;/h3&gt;
&lt;p&gt;I followed these instructions (&lt;a href=&quot;https://kubernetes.io/docs/tasks/tools/install-minikube/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Minikube Installation Instructions&lt;/a&gt;) using the VitualBox hypervisor + vm-driver to install Minikube on the MacBook air:&lt;/p&gt;
&lt;h3 id=&quot;2-install-code-kubectl-code-on-the-client-system&quot; tabindex=&quot;-1&quot;&gt;2) Install &lt;code&gt;kubectl&lt;/code&gt; on the Client System&lt;/h3&gt;
&lt;p&gt;I already had &lt;code&gt;kubectl&lt;/code&gt; installed on my primary development machine, but you could follow these instructions (&lt;a href=&quot;https://kubernetes.io/docs/tasks/tools/install-kubectl/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Kubectl Installation Instructions&lt;/a&gt;) to install it on the system you plan to access the cluster from.&lt;/p&gt;
&lt;h3 id=&quot;3-add-port-forwarding-rule-from-the-server-system-to-the-minikube-vm&quot; tabindex=&quot;-1&quot;&gt;3) Add port forwarding rule from the Server System to the Minikube VM&lt;/h3&gt;
&lt;p&gt;In order for &lt;code&gt;kubectl&lt;/code&gt; commands to make their way to the Minikube cluster, it is necessary to set up a port forwarding rule within the VM configuration to forward requests to port 8443. This can be accomplished via the VirtualBox GUI:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/always-on-minikube/minikube-port-forward.png&quot; alt=&quot;minikube-port-forward.png&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;4-update-the-code-kube-config-code-on-the-client-to-point-to-the-server&quot; tabindex=&quot;-1&quot;&gt;4) Update the &lt;code&gt;.kube/config&lt;/code&gt; on the Client to Point to the Server&lt;/h3&gt;
&lt;p&gt;When executing the &lt;code&gt;minikube start&lt;/code&gt; command on the server system, a kubectl context called &#39;minikube&#39; is added to the kubectl configuration file @ &lt;code&gt;~/.kube/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
clusters:
- cluster:
    certificate-authority: &amp;lt;PATH-TO-CONFIG&amp;gt;/.minikube/ca.crt
    server: https://192.168.99.100:8443
  name: minikube
contexts:
- context:
    cluster: minikube
    user: minikube
  name: minikube
current-context: &amp;quot;minikube&amp;quot;
kind: Config
preferences: {}
users:
- name: minikube
  user:
    client-certificate: &amp;lt;PATH-TO-CONFIG&amp;gt;/.minikube/client.crt
    client-key: &amp;lt;PATH-TO-CONFIG&amp;gt;/.minikube/client.key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This configuration needs to be copied onto the client machine (along with the &lt;code&gt;client.crt&lt;/code&gt; and &lt;code&gt;client.key&lt;/code&gt; files).&lt;/p&gt;
&lt;p&gt;I copied those files to &lt;code&gt;&amp;lt;PATH-TO-CONFIG&amp;gt;/.minikube-macbook-air&lt;/code&gt; on the client system and updated the kubectl configuration as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://&amp;lt;INTERNAL-NETWORK-IP&amp;gt;:51928
  name: minikube-macbook-air
contexts:
- context:
    cluster: minikube-macbook-air
    user: minikube-macbook-air
  name: minikube-macbook-air
current-context: &amp;quot;minikube-macbook-air&amp;quot;
kind: Config
preferences: {}
users:
- name: minikube-macbook-air
  user:
    client-certificate: &amp;lt;PATH-TO-CONFIG&amp;gt;/.minikube-macbook-air/client.crt
    client-key: &amp;lt;PATH-TO-CONFIG&amp;gt;/.minikube-macbook-air/client.key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can either be inserted as a context within the primary &lt;code&gt;.kube/config&lt;/code&gt; file or stored as a separate configuration and used by setting the &lt;code&gt;KUBECONFIG&lt;/code&gt; environment variable (&lt;a href=&quot;https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/#set-the-kubeconfig-environment-variable&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; The &lt;code&gt;insecure-skip-tls-verify: true&lt;/code&gt; is used because the certificate authority set up by Minikube is not configured to accept incoming requests originating from another system in this way.&lt;/p&gt;
&lt;h3 id=&quot;5-optional-use-ddns-router-port-forwarding-to-access-outside-local-network&quot; tabindex=&quot;-1&quot;&gt;5) (Optional) Use DDNS + Router Port Forwarding to Access Outside Local Network&lt;/h3&gt;
&lt;p&gt;At this point, I was able to connect to the cluster, but only when the Client and Server systems were connected to the same network.&lt;/p&gt;
&lt;p&gt;Two additional things needed to be configured to connect from outside of my home network:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I set up a free dynamic DNS using &lt;a href=&quot;https://www.dynu.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://www.dynu.com/&lt;/a&gt;. After doing this I replaced &lt;code&gt;&amp;lt;INTERNAL-NETWORK-IP&amp;gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;MY-DDNS-DOMAIN-NAME&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I configured my router to port forward incoming requests on port &lt;code&gt;51928&lt;/code&gt; to the MacBook Air and reserve the IP address for the MAC Address of the machine to ensure it doesn&#39;t change.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At this point I was able to successfully connect to the cluster from an external network!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/always-on-minikube/kubectl-get-nodes.png&quot; alt=&quot;kubectl-get-nodes.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; tabindex=&quot;-1&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There we have it -- For about the price of a McDonald&#39;s coffee each month in electricity, I have an always-on Kubernetes cluster (with a single 2 vCPU + 2 GB Memory node) ready for experimentation!&lt;/p&gt;
&lt;p&gt;If I didn&#39;t happen to have the MacBook Air sitting around, building a home cluster using Raspberry Pis is another popular option (and enables having more than just a single worker node).&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/03/always-on-minikube/" />
        </entry>
        <entry>
            <title>
                managed-kubernetes-comparison
                
            </title>
            <updated>2025-07-08T18:56:11.849Z</updated>
            <id>https://devopsdirective.com/posts/2020/03/managed-kubernetes-comparison/</id>
            <content type="html">
                &lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Azure and Digital Ocean don&#39;t charge for the compute resources used for the control plane, making AKS and DO the cheapest for running many, smaller clusters. For running fewer, larger clusters GKE is the most affordable option. Also, running on spot/preemptible/low-priority nodes or long-term committed nodes makes a massive impact across all of the platforms.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/managed-kubernetes-comparison/k8s-price-graph.png&quot; alt=&quot;k8s-price-graph.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#overview&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#cost-breakdown&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cost Breakdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#jupyter-notebook&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Jupyter Notebook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#takeaways&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Takeaways&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3 id=&quot;overview&quot; tabindex=&quot;-1&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;With Google Cloud&#39;s recent announcement to &lt;a href=&quot;https://news.ycombinator.com/item?id=22485625&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;start charging $0.10/hour for each cluster&lt;/a&gt; on GKE, it seemed like a good time to revisit pricing across the major managed Kubernetes offerings.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=22485625&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/managed-kubernetes-comparison/gke-price-increase.png&quot; alt=&quot;gke-price-increase.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;em&gt;This upset some people on the internet...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This article will focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Kubernetes Engine (GCP) -- &lt;a href=&quot;https://cloud.google.com/products/calculator&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;pricing calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Elastic Kubernetes Service (AWS) -- &lt;a href=&quot;https://calculator.s3.amazonaws.com/index.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;pricing calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Azure Kubernetes Service (Azure) -- &lt;a href=&quot;https://azure.microsoft.com/en-us/pricing/calculator/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;pricing calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes on Digital Ocean -- &lt;a href=&quot;https://www.digitalocean.com/pricing/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;pricing page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cost-breakdown&quot; tabindex=&quot;-1&quot;&gt;Cost Breakdown&lt;/h3&gt;
&lt;p&gt;The cost of running Kubernetes on each of these platforms is based on the following components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cluster Management Fee&lt;/li&gt;
&lt;li&gt;Load Balancer (for Ingress)&lt;/li&gt;
&lt;li&gt;Worker Node Compute Resources (vCPU &amp;amp; Memory)&lt;/li&gt;
&lt;li&gt;Data Egress&lt;/li&gt;
&lt;li&gt;Persistent Storage&lt;/li&gt;
&lt;li&gt;Load Balancer Data Processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally, the cloud providers offer large discounts if you are willing/able to use preemptible/spot/low-priority nodes OR commit to using the same nodes for 1-3 years.&lt;/p&gt;
&lt;p&gt;It is important to point out that while cost is a useful dimension to examine when evaluating providers, there are other factors that should also be considered including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uptime (Service Level Agreement)&lt;/li&gt;
&lt;li&gt;Surrounding Cloud Ecosystem&lt;/li&gt;
&lt;li&gt;k8s Version Availability&lt;/li&gt;
&lt;li&gt;Documentation/Tooling Quality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those factors are beyond the scope of this article/exploratory analysis. This &lt;a href=&quot;https://www.stackrox.com/post/2020/02/eks-vs-gke-vs-aks/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;blog post from StackRox (Feb. 2020)&lt;/a&gt; provides a detailed look at the non-price factors for EKS, AKS, and GKE.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id=&quot;jupyter-notebook&quot; tabindex=&quot;-1&quot;&gt;Jupyter Notebook&lt;/h3&gt;
&lt;p&gt;To make it easier to explore the cost tradeoffs I created a Jupyter notebook using plotly + ipywidgets to facilitate rapid exploration of cluster sizes/tradeoffs across the different cloud providers.&lt;/p&gt;
&lt;p&gt;You can interact with a live version of that notebook yourself using Binder:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mybinder.org/v2/gh/sidpalas/managed-kubernetes-pricing/master?filepath=%2Fmanaged-kubernetes-price-exploration.ipynb&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/03/managed-kubernetes-comparison/binder.png&quot; alt=&quot;Posts/2020/03/managed-kubernetes-comparison/binder.png&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://mybinder.org/v2/gh/sidpalas/managed-kubernetes-pricing/master?filepath=%2Fmanaged-kubernetes-price-exploration.ipynb&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://mybinder.org/v2/gh/sidpalas/managed-kubernetes-pricing/master?filepath=%2Fmanaged-kubernetes-price-exploration.ipynb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If any of my calculations or pricing constants look incorrect, please let me know (via a Github Issue or Pull Request! -- &lt;a href=&quot;https://github.com/sidpalas/managed-kubernetes-pricing/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Github repository&lt;/a&gt;)&lt;/p&gt;
&lt;h3 id=&quot;takeaways&quot; tabindex=&quot;-1&quot;&gt;Takeaways&lt;/h3&gt;
&lt;p&gt;There are too many variables to give much of a recommendation beyond my TL;DR up top, but here are some takeaways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AKS and Digital Ocean do not charge for the control plane resources while GKE and EKS do. If your architecture incorporates many small clusters (e.g. 1 cluster &lt;em&gt;per developer&lt;/em&gt; or &lt;em&gt;per customer&lt;/em&gt;) AKS and DO can have an cost advantage.&lt;/li&gt;
&lt;li&gt;Google&#39;s slightly cheaper compute resources result in lower costs as cluster sizes scale*.&lt;/li&gt;
&lt;li&gt;Taking advantage of preemptible and/or committed use discounts can reduce costs by &amp;gt;50% (&lt;em&gt;NOTE:&lt;/em&gt; Digital Ocean does not offer these types of discounts).&lt;/li&gt;
&lt;li&gt;While Google&#39;s data egress fees are higher, the compute resources dominated the cost calculation (unless you are sending a significant amount of data out of the cluster).&lt;/li&gt;
&lt;li&gt;Choosing machine types that match your workloads&#39; CPU and Memory needs will avoid paying for wasted capacity.&lt;/li&gt;
&lt;li&gt;Digital Ocean charges relatively less for vCPUs and more for memory which could be an important consideration depending on the nature of the compute workloads.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;*&lt;em&gt;NOTE:&lt;/em&gt; I used general-purpose compute node types for all 4 clouds (n1 GCP Compute Engine instances, m5 AWS ec2 instances, D2v3 Azure virtual machines, and dedicated CPU DO droplets). Further exploration could be done across the burstable and entry-level VM types. Also, it appeared that pricing for VMs scaled linearly with # of vCPU and GB of memory, but I am not sure that assumption holds as you move towards some of the less standard memory/cpu ratios.&lt;/p&gt;
&lt;p&gt;The article &lt;a href=&quot;https://www.replex.io/blog/the-ultimate-kubernetes-cost-guide-aws-vs-gce-vs-azure-vs-digital-ocean&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&amp;quot;The Ultimate Kubernetes Cost Guide: AWS vs GCP vs Azure vs Digital Ocean&amp;quot;&lt;/a&gt; published in 2018 used a single reference cluster with 100 vCPU cores and 400 GB of memory. As a point of comparison, here is how much my calculation shows that cluster would cost (using on-demand prices) on each of these platforms.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AKS: $51,465/year&lt;/li&gt;
&lt;li&gt;EKS: $43,138/year&lt;/li&gt;
&lt;li&gt;GKE: $30,870/year&lt;/li&gt;
&lt;li&gt;DO: $36,131/year&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Hopefully, this article + notebook will help you in your journey to evaluate the major managed Kubernetes offerings and/or save $$$ on your cloud infrastructure by taking advantage of the available cost-saving opportunities.&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/03/managed-kubernetes-comparison/" />
        </entry>
        <entry>
            <title>
                hugo-cloud-build
                
            </title>
            <updated>2025-07-09T18:25:53.819Z</updated>
            <id>https://devopsdirective.com/posts/2020/02/hugo-cloud-build/</id>
            <content type="html">
                &lt;h1 id=&quot;ci-cd-for-this-site-hugo-cloud-build&quot; tabindex=&quot;-1&quot;&gt;CI/CD for this site (Hugo + Cloud Build)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Configuring Cloud Build to automatically handle Continuous Integration and Continuous Deployment for this site based on Git triggers ended up being a bit trickier than I would have expected.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/02/hugo-cloud-build/hugo-cloud-build.png&quot; alt=&quot;hugo-cloud-build.png&quot; /&gt;&lt;/p&gt;
&lt;!--more--&gt; 
&lt;hr /&gt;
&lt;p&gt;Table of Contents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#picking-a-cicd-tool&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Picking a CI/CD Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#attempting-to-use-the-cloud-build-github-app&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Attempting to Use the Cloud Build GitHub App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#gcp-set-up&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GCP Set Up&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#video-walkthrough&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Video Walkthrough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1-mirroring-github-repo-to-cloud-source&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1) Mirroring GitHub Repo to Cloud Source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2-enable-the-cloud-build-api&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2) Enable the Cloud Build API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3-add-iam-roles-for-cloud-build-service-account&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3) Add IAM Roles for Cloud Build Service Account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4-creating-cloud-build-trigger&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4) Creating Cloud Build Trigger&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#creating-the-cloud-build-pipeline-cloudbuildyaml&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Creating the Cloud Build Pipeline (cloudbuild.yaml)&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#1-initialize-and-update-the-submodules&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;1) Initialize and Update the Submodules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#2-build-the-hugo-site&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;2) Build the Hugo Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#3-build--push-the-caddy-container-image&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;3) Build + Push the Caddy Container Image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#4-stop-running-containers--start-new-container&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;4) Stop Running Containers &amp;amp; Start New Container&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#note-pushing-images&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;NOTE: Pushing Images&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devopsdirective.com/#closing-thoughts&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;This is a continuation of the previous post ([The Making of This Site (Hugo, Caddy, + GCP)]({{&amp;lt; ref &amp;quot;/posts/2020/02/hugo-and-caddy-on-gcp/index.md&amp;quot; &amp;gt;}})) in which I walked through the set up of this site. In this post, I add automated builds/deploys to the site.&lt;/p&gt;
&lt;p&gt;All of the commands for creating the site, as well as setting up this automation can be found in this &lt;a href=&quot;https://github.com/sidpalas/hugo-gcp-deploy&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub Repo&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;picking-a-ci-cd-tool&quot; tabindex=&quot;-1&quot;&gt;Picking a CI/CD Tool&lt;/h3&gt;
&lt;p&gt;Initially, I was going to use &lt;a href=&quot;https://circleci.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Circle CI&lt;/a&gt; to automate the process of building and deploying the site. Circle CI has direct integration with Github and posted to their blog in 2018 explaining how to &lt;a href=&quot;https://circleci.com/blog/automate-your-static-site-deployment-with-circleci/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Automate Your Static Site Deployment with CircleCI&lt;/a&gt; using Hugo as the example site generator.&lt;/p&gt;
&lt;p&gt;That being said since everything in the site setup was GCP based, I decided to try out &lt;a href=&quot;https://cloud.google.com/cloud-build&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cloud Build&lt;/a&gt;. Cloud Build also has a &lt;a href=&quot;https://github.com/marketplace/google-cloud-build&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;GitHub app&lt;/a&gt; and being within the same GCP project meant I wouldn&#39;t have to deal with shuffling additional service account credentials between platforms.&lt;/p&gt;
&lt;p&gt;Also, just like with the server setup, Cloud Build is also included in the GCP free tier (up to 120 build minutes/day) so this shouldn&#39;t cost me anything.&lt;/p&gt;
&lt;h3 id=&quot;attempting-to-use-the-cloud-build-git-hub-app&quot; tabindex=&quot;-1&quot;&gt;Attempting to Use the Cloud Build GitHub App&lt;/h3&gt;
&lt;p&gt;Thinking this would be a 30-minute task, I eagerly installed the Cloud Build Github app and added a build trigger based on pushes to the master branch. When the build succeeded I was not greeted with the website, but instead with the Default Caddy home page. After manually navigating to the &lt;code&gt;/articles&lt;/code&gt; endpoint I saw the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devopsdirective.com/img/user/Posts/2020/02/hugo-cloud-build/missing-hugo-theme.png&quot; alt=&quot;missing-hugo-theme.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The content files were there, but I realized that It wasn&#39;t being rendered properly because the build failed to get the theme files. After doing more research I concluded that it has to do with the fact that the theme is not stored within the website Git repo, but is a Git submodule.&lt;/p&gt;
&lt;p&gt;In an attempt to solve this, I added a step to the build pipeline to grab the submodule files using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git submodule init
git submodule update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but Cloud Builds triggered from GitHub don&#39;t have access to the &lt;code&gt;.git&lt;/code&gt; directory within the repo and these commands will fail. The best workaround I could find was to mirror the GitHub repo into a Cloud Source Repository. This is already getting more complicated than I had hoped, but the show must go on!&lt;/p&gt;
&lt;h3 id=&quot;gcp-set-up&quot; tabindex=&quot;-1&quot;&gt;GCP Set Up&lt;/h3&gt;
&lt;p&gt;At this point, I had arrived what I thought was a viable plan that I just needed to execute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The commands that follow use $PROJECT_ID and other template variables that should reflect the relevant project and values.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PROJECT_ID=my-awesome-project-1234
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;video-walkthrough&quot; tabindex=&quot;-1&quot;&gt;Video Walkthrough&lt;/h4&gt;
&lt;p&gt;To accompany this article, I also created a full video walkthrough of setting up the pipeline. Feel free to follow along or skip it depending on if you prefer the video or written format!&lt;/p&gt;
&lt;p&gt;{{&amp;lt; youtube MF2gMZ5aDBQ &amp;gt;}}&lt;/p&gt;
&lt;h4 id=&quot;1-mirroring-git-hub-repo-to-cloud-source&quot; tabindex=&quot;-1&quot;&gt;1) Mirroring GitHub Repo to Cloud Source&lt;/h4&gt;
&lt;p&gt;As noted above, I needed to mirror the GitHub repo for my website into a Cloud Source repo. I couldn&#39;t find a good way to script this, so I just followed the guidance in this Google post: &lt;a href=&quot;https://cloud.google.com/source-repositories/docs/mirroring-a-github-repository&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Mirroring a GitHub repository&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;2-enable-the-cloud-build-api&quot; tabindex=&quot;-1&quot;&gt;2) Enable the Cloud Build API&lt;/h4&gt;
&lt;p&gt;To use Cloud Build I had to enable the cloud build API using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud services enable cloudbuild.googleapis.com --project=$PROJECT_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-add-iam-roles-for-cloud-build-service-account&quot; tabindex=&quot;-1&quot;&gt;3) Add IAM Roles for Cloud Build Service Account&lt;/h4&gt;
&lt;p&gt;When the Cloud Build API is enabled, a service account of the format &lt;code&gt;projectNumber@cloudbuild.gserviceaccount.com&lt;/code&gt; is granted some IAM roles, but for this build pipeline the service account also needs the following two roles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Compute Instance Admin (v1)
Service Account User
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which I accomplished with the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcloud projects add-iam-policy-binding $PROJECT_ID &#92;
  --member serviceAccount:$(gcloud projects describe $PROJECT_ID --format=&amp;quot;value(projectNumber)&amp;quot;)@cloudbuild.gserviceaccount.com &#92;
  --role roles/compute.instanceAdmin.v1
gcloud projects add-iam-policy-binding $PROJECT_ID &#92;
  --member serviceAccount:$(gcloud projects describe $PROJECT_ID --format=&amp;quot;value(projectNumber)&amp;quot;)@cloudbuild.gserviceaccount.com &#92;
  --role roles/iam.serviceAccountUser
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-creating-cloud-build-trigger&quot; tabindex=&quot;-1&quot;&gt;4) Creating Cloud Build Trigger&lt;/h4&gt;
&lt;p&gt;With all of the prerequisite configurations in place, it was then time to create the Cloud Build trigger:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export IMAGE_NAME=my-hugo-caddy-docker-image
export INSTANCE_NAME=my-f1-micro-instance
export ZONE=us-central1-a
gcloud beta builds triggers create cloud-source-repositories &#92;
  --project=$PROJECT_ID &#92;
  --repo=my-cloud-source-repo-mirroring-a-github-repo &#92;
  --branch-pattern=master &#92;
  --build-config=cloudbuild.yaml &#92;
  --substitutions=_IMAGE_NAME=$IMAGE_NAME,_SSH_STRING=$USER@$INSTANCE_NAME,_ZONE=$ZONE,_HOME=/home/$USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--substitutions&lt;/code&gt; represent template variables that get used in the pipeline definition as will become apparent below.&lt;/p&gt;
&lt;h3 id=&quot;creating-the-cloud-build-pipeline-cloudbuild-yaml&quot; tabindex=&quot;-1&quot;&gt;Creating the Cloud Build Pipeline (cloudbuild.yaml)&lt;/h3&gt;
&lt;p&gt;At this point, Cloud Build was primed and ready to go... but I hadn&#39;t told it what to do yet. The actual build pipeline is defined within a &lt;code&gt;cloudbuild.yaml&lt;/code&gt; file at the root of the repository.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/cloud-build/docs/build-config&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Build Configuration Documentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Each step in this pipeline takes in a container image as its &amp;quot;name&amp;quot; and can also take optional arguments which are executed inside that container.&lt;/p&gt;
&lt;p&gt;Below I have broken down this pipeline into its 6 steps:&lt;/p&gt;
&lt;h4 id=&quot;1-initialize-and-update-the-submodules&quot; tabindex=&quot;-1&quot;&gt;1) Initialize and Update the Submodules&lt;/h4&gt;
&lt;p&gt;As mentioned above, one of the initial challenges using Cloud Build was it failing to get the files associated with the Hugo theme because they are located in a git submodule. The following uses the git cloud-builders image to initialize and update the git submodules, resulting in the theme files being available for future steps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;steps:
- name: &#39;gcr.io/cloud-builders/git&#39;
  entrypoint: &#39;bash&#39;
  args:
  - -c
  - |
    git submodule init
    git submodule update
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-build-the-hugo-site&quot; tabindex=&quot;-1&quot;&gt;2) Build the Hugo Site&lt;/h4&gt;
&lt;p&gt;The whole reason for needing a build step is that only the content source files are version controlled (not the generated site files). This step runs the Hugo generator. I couldn&#39;t find a publicly available container image that was compatible with Cloud Build, so I created my own (based on &lt;a href=&quot;https://github.com/GoogleCloudPlatform/cloud-builders-community/tree/master/hugo&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;this example&lt;/a&gt;) and posted it to DockerHub: &lt;a href=&quot;https://hub.docker.com/r/sidpalas/cloud-builder-hugo&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;https://hub.docker.com/r/sidpalas/cloud-builder-hugo&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# build hugo site
- name: &#39;sidpalas/cloud-builder-hugo:0.64.1&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-build-push-the-caddy-container-image&quot; tabindex=&quot;-1&quot;&gt;3) Build + Push the Caddy Container Image&lt;/h4&gt;
&lt;p&gt;This step builds the website container image and pushes it to Google Container Registry. It uses the COMMIT_SHA (which is populated automatically by Cloud Build based on the triggering commit) to tag the image.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;- name: &#39;gcr.io/cloud-builders/docker&#39;
  # Overriding entrypoint to allow for running two docker commands
  entrypoint: &#39;bash&#39;
  args: 
    - -c
    - |
      docker build -t gcr.io/$PROJECT_ID/$_IMAGE_NAME:$COMMIT_SHA . &amp;amp;&amp;amp;
      docker push gcr.io/$PROJECT_ID/$_IMAGE_NAME:$COMMIT_SHA
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-stop-running-containers-and-amp-start-new-container&quot; tabindex=&quot;-1&quot;&gt;4) Stop Running Containers &amp;amp; Start New Container&lt;/h4&gt;
&lt;p&gt;With the new container available in GCR, the pipeline stops any running containers and then starts the new container using a gcloud container image to execute a &lt;code&gt;gcloud ssh&lt;/code&gt; command on the VM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;- name: &#39;gcr.io/cloud-builders/gcloud&#39;
  args:
  - compute
  - ssh
  - $_SSH_STRING
  - --project=$PROJECT_ID
  - --zone=$_ZONE
  - --
  - docker container stop $(docker container ls -aq) &amp;amp;&amp;amp; 
  - docker container rm $(docker container ls -aq) &amp;amp;&amp;amp;
  - docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -v $_HOME/.caddy:/root/.caddy gcr.io/$PROJECT_ID/$_IMAGE_NAME:$COMMIT_SHA
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;note-pushing-images&quot; tabindex=&quot;-1&quot;&gt;NOTE: Pushing Images&lt;/h4&gt;
&lt;p&gt;Normally, the build configuration would have an &lt;code&gt;images:&lt;/code&gt; section specifying which container images should be pushed to GCR. Because step &lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#3&quot;&gt;#3&lt;/a&gt; already tagged and pushed the container image, it is &lt;strong&gt;not&lt;/strong&gt; necessary to include an images section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;images:
- &#39;gcr.io/$PROJECT_ID/$_IMAGE_NAME:$COMMIT_SHA&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;closing-thoughts&quot; tabindex=&quot;-1&quot;&gt;Closing Thoughts&lt;/h3&gt;
&lt;p&gt;This process ended up being much more complex than I had initially hoped. The issues with Git submodules, and having to create my own Hugo builder image made it take much longer than I expected. That being said, it was quite satisfying when I got the configuration dialed in and the long stretch of red builds finally turned green.&lt;/p&gt;
&lt;p&gt;{{&amp;lt; img &amp;quot;images/&lt;em&gt;cloud-build-dashboard&lt;/em&gt;&amp;quot; &amp;quot;Success!&amp;quot;&amp;gt;}}&lt;/p&gt;
&lt;p&gt;While I am happy with the end result, I can&#39;t help but think using Circle CI might have been a smoother process. Perhaps sometime down the the road I&#39;ll attempt setting up an equivalent pipeline there and see how that goes!&lt;/p&gt;

            </content>
            <link href="https://devopsdirective.com/posts/2020/02/hugo-cloud-build/" />
        </entry>
</feed>
