Quickstart
Welcome to the Holos Quickstart guide. Holos is an open source tool to manage software development platforms safely, easily, and consistently. We'll use Holos to manage a fictional bank's platform, the Bank of Holos. In doing so we'll take the time to explain the foundational concepts of Holos.
- Platform - Holos breaks a Platform down into Components owned by teams.
- Component - Components are CUE wrappers around unmodified upstream vendor Helm Charts, Kustomize Bases, or plain Kubernetes manifests.
- CUE - We write CUE to configure the platform. We'll cover the basics of CUE syntax and why Holos uses CUE.
- Tree Unification - CUE files are organized into a unified filesystem tree. We'll cover how unification makes it easier and safer for multiple teams to change the platform.
The Bank of Holos provides a good example of how Holos is designed to make it easier for multiple teams to deliver services on a platform. These teams are:
- Platform
- Software development
- Security
- Quality Assurance
Here's a screenshot of the retail banking application we'll build and deploy on our platform. We'll keep each of these teams in mind as we work through the guides. Each of our guides focuses on different aspects of delivering the Bank of Holos.
What you'll need
This guide is intended to be informative without needing to run the commands. If you'd like to render the platform and apply the manifests to a real Cluster, complete the Local Cluster Guide before this guide.
You'll need the following tools installed to run the commands in this guide.
- holos - to build the Platform.
- helm - to render Holos Components that wrap Helm charts.
- kubectl - to render Holos Components that render with Kustomize.
Install Holos
Start by installing the holos
command line tool with the following command.
If you don't have Go, refer to Installation to download the
executable.
- Command
- Output
go install github.com/holos-run/holos/cmd/holos@latest
go: downloading github.com/holos-run/holos v0.95.1
Nearly all day-to-day platform management tasks use the holos
command line
tool to render plain Kubernetes manifests.
Fork the Git Repository
Building a software development platform from scratch takes time so we've published an example for our guides. Fork the Bank of Holos to get started.
Clone the repository to your local machine.
- Command
- Output
# Change YourName
git clone https://github.com/YourName/bank-of-holos
cd bank-of-holos
Cloning into 'bank-of-holos'...
remote: Enumerating objects: 1177, done.
remote: Counting objects: 100% (1177/1177), done.
remote: Compressing objects: 100% (558/558), done.
remote: Total 1177 (delta 394), reused 1084 (delta 303), pack-reused 0 (from 0)
Receiving objects: 100% (1177/1177), 2.89 MiB | 6.07 MiB/s, done.
Resolving deltas: 100% (394/394), done.
Run the rest of the commands in this guide from the root of the repository.
Configure ArgoCD
The Bank of Holos platform is organized as a collection of software components. Each component represents a piece of software provided by an upstream vendor, for example ArgoCD, or software developed in-house. Components are also used to glue together, or integrate, other components into the platform.
We'll start by changing the platform to point to our fork. We need to do this so we'll be able to see changes in ArgoCD as we make changes with GitOps.
- projects/argocd-config.cue
package holos
#ArgoConfig: {
Enabled: true
RepoURL: "https://github.com/holos-run/bank-of-holos"
}
Change the RepoURL to the URL of your fork. For example:
- projects/argocd-config.cue
diff --git a/projects/argocd-config.cue b/projects/argocd-config.cue
index 5264f48..0214e99 100644
--- a/projects/argocd-config.cue
+++ b/projects/argocd-config.cue
@@ -2,5 +2,5 @@ package holos
#ArgoConfig: {
Enabled: true
- RepoURL: "https://github.com/holos-run/bank-of-holos"
+ RepoURL: "https://github.com/jeffmccune/bank-of-holos"
}
We need to render the platform manifests after we make changes.
Render the Platform
Platform rendering is is the process of looping over all the components in the platform and rendering each one into plain kubernetes manifest files. Holos is designed to write plain manifest files which can be applied to Kubernetes, but stops short of applying them so it's easier for team members to review and understand changes before they're made.
- Command
- Output
holos render platform ./platform
rendered bank-accounts-db for cluster workload in 142.661334ms
rendered bank-ledger-db for cluster workload in 144.041417ms
rendered bank-userservice for cluster workload in 157.828709ms
rendered bank-ledger-writer for cluster workload in 161.138292ms
rendered bank-backend-config for cluster workload in 168.923459ms
rendered bank-balance-reader for cluster workload in 171.877875ms
rendered bank-secrets for cluster workload in 207.958792ms
rendered gateway for cluster workload in 123.572583ms
rendered bank-contacts for cluster workload in 144.466291ms
rendered bank-transaction-history for cluster workload in 151.520041ms
rendered httproutes for cluster workload in 139.590834ms
rendered bank-frontend for cluster workload in 309.679834ms
rendered app-projects for cluster workload in 107.136083ms
rendered ztunnel for cluster workload in 160.679791ms
rendered cni for cluster workload in 238.937625ms
rendered cert-manager for cluster workload in 178.610834ms
rendered istiod for cluster workload in 340.953208ms
rendered argocd for cluster workload in 286.277ms
rendered local-ca for cluster workload in 98.720208ms
rendered external-secrets for cluster workload in 141.459708ms
rendered base for cluster workload in 454.356667ms
rendered namespaces for cluster workload in 115.401709ms
rendered gateway-api for cluster workload in 203.5625ms
rendered external-secrets-crds for cluster workload in 525.180209ms
rendered crds for cluster workload in 888.406167ms
rendered platform in 1.182857542s
Rendering the platform to plain manifest files allows us to see the changes clearly. We can see this one line change affected dozens ArgoCD Application resources across the platform.
- Command
- Output
git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: deploy/clusters/workload/gitops/app-projects.application.gen.yaml
modified: deploy/clusters/workload/gitops/argocd-crds.application.gen.yaml
modified: deploy/clusters/workload/gitops/argocd.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-accounts-db.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-backend-config.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-balance-reader.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-contacts.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-frontend.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-ledger-db.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-ledger-writer.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-secrets.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-transaction-history.application.gen.yaml
modified: deploy/clusters/workload/gitops/bank-userservice.application.gen.yaml
modified: deploy/clusters/workload/gitops/cert-manager.application.gen.yaml
modified: deploy/clusters/workload/gitops/external-secrets-crds.application.gen.yaml
modified: deploy/clusters/workload/gitops/external-secrets.application.gen.yaml
modified: deploy/clusters/workload/gitops/gateway-api.application.gen.yaml
modified: deploy/clusters/workload/gitops/httproutes.application.gen.yaml
modified: deploy/clusters/workload/gitops/istio-base.application.gen.yaml
modified: deploy/clusters/workload/gitops/istio-cni.application.gen.yaml
modified: deploy/clusters/workload/gitops/istio-gateway.application.gen.yaml
modified: deploy/clusters/workload/gitops/istio-ztunnel.application.gen.yaml
modified: deploy/clusters/workload/gitops/istiod.application.gen.yaml
modified: deploy/clusters/workload/gitops/local-ca.application.gen.yaml
modified: deploy/clusters/workload/gitops/namespaces.application.gen.yaml
modified: projects/argocd-config.cue
no changes added to commit (use "git add" and/or "git commit -a")
Take a look at the Application resource for the bank-frontend component to see
the changed spec.source.repoURL
field.
- Command
- Output
git diff deploy/clusters/workload/gitops/bank-frontend.application.gen.yaml
diff --git a/deploy/clusters/workload/gitops/bank-frontend.application.gen.yaml b/deploy/clusters/workload/gitops/bank-frontend.application.gen.yaml
index d8ede55..aed4338 100644
--- a/deploy/clusters/workload/gitops/bank-frontend.application.gen.yaml
+++ b/deploy/clusters/workload/gitops/bank-frontend.application.gen.yaml
@@ -9,5 +9,5 @@ spec:
project: bank-frontend
source:
path: ./deploy/clusters/workload/components/bank-frontend
- repoURL: https://github.com/holos-run/bank-of-holos
+ repoURL: https://github.com/jeffmccune/bank-of-holos
targetRevision: main
We'll add, commit, and push this change to our fork then take a little time to explain what happened when we made the change and rendered the platform.
- Command
- Output
git add .
git commit -m 'quickstart: change argocd repo url to our fork'
git push origin
[main f2f8bc2] quickstart: change argocd repo url to our fork
26 files changed, 26 insertions(+), 26 deletions(-)
Enumerating objects: 41, done.
Counting objects: 100% (41/41), done.
Delta compression using up to 14 threads
Compressing objects: 100% (31/31), done.
Writing objects: 100% (33/33), 2.95 KiB | 2.95 MiB/s, done.
Total 33 (delta 28), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (28/28), completed with 4 local objects.
To github.com:jeffmccune/bank-of-holos.git
c2951ec..f2f8bc2 main -> main
Platform Rendering Explained
So what happens when we run holos render platform
? We saw holos
write plain
manifest files, let's dive into how and why we implemented platform rendering
like this.
Why do we render the platform?
We built Holos to make the process of managing a platform safer, easier, and more consistent. Before Holos we used Helm, Kustomize, and scripts to glue together all of the software that goes into a platform. Then we coaxed the output of each tool into something that works with GitOps. This approach has a number of shortcomings. We wanted to see the manifests before ArgoCD or Flux applied them, so we wrote a lot of difficult to maintain scripts to get the template output into something useful. We tried avoiding the scripts by having ArgoCD handle the Helm charts directly, but we could no longer see the changes clearly during code review.
The platform rendering process allows us to have it both ways. We avoid the unsafe text templates and glue scripts by using CUE. We're able to review the exact changes that will be applied during code review because holos renders the whole platform to plain manifest files.
Finally, because we usually make each change by rendering the whole platform, we're able to see and consider how a single-line change, like the one we just made, affects the whole platform. Before we made Holos we were frustrated with how difficult it was to get this zoomed-out, broad perspective of each change we made.
How does platform rendering work?
Holos is declarative. CUE provides resources that declare what holos
needs to
do. The output of holos
is always the same for the same inputs, so holos
is
also idempotent.
When we run holos render platform
, CUE builds the Platform specification
(spec). This is a fancy way of saying a list of software to manage on each
cluster in the platform. The CUE files in the platform
directory provide the
platform spec to holos
.
Let's open up two of these CUE files to see how this works. Ignore the other files for now, they behave the same as these two.
- platform/argocd.cue
- platform/external-secrets.cue
package holos
// Manage the component on every cluster in the platform
for Fleet in #Fleets {
for Cluster in Fleet.clusters {
#Platform: Components: "\(Cluster.name)/argocd-crds": {
path: "projects/platform/components/argocd/crds"
cluster: Cluster.name
}
#Platform: Components: "\(Cluster.name)/argocd": {
path: "projects/platform/components/argocd/argocd"
cluster: Cluster.name
}
}
}
package holos
// Manage the component on every cluster in the platform
for Fleet in #Fleets {
for Cluster in Fleet.clusters {
#Platform: Components: "\(Cluster.name)/external-secrets-crds": {
path: "projects/platform/components/external-secrets-crds"
cluster: Cluster.name
}
#Platform: Components: "\(Cluster.name)/external-secrets": {
path: "projects/platform/components/external-secrets"
cluster: Cluster.name
}
}
}
There's quite a few new concepts to unpack in these two CUE files.
- A Fleet is just a collection of clusters that share a similar, but not identical configuration. Most platforms have a management fleet with one cluster to manage the platform, and a workload fleet for clusters that host the services we deploy onto the platform.
- A Cluster is a Kubernetes cluster. Each component is rendered to plain manifests for a cluster.
On lines 6 and 10 we see a Component being assigned to the Platform. We also start to dive into the syntax of CUE, which we need to understand a little before going further.
In its simplest form, CUE looks a lot like JSON. This is because CUE is a superset of JSON. Or, put differently: all valid JSON is CUE.
- C-style comments are allowed
- field names without special characters don't need to be quoted
- commas after a field are optional (and are usually omitted)
- commas after the final element of a list are allowed
- the outermost curly braces in a CUE file are optional
JSON objects are called structs in CUE. JSON arrays are called lists, Object members are called fields, which link their name, or label, to a value.
There are two important things to know about CUE to understand these two files. First, the curly braces have been omitted which is item 5 on the list above. Second, CUE is all about unification. These files could have been written like this:
- platform/argocd.cue
- platform/external-secrets.cue
package holos
// Manage the component on every cluster in the platform
for Fleet in #Fleets {
for Cluster in Fleet.clusters {
#Platform: {
// Define #Platform.Components
Components: {
"\(Cluster.name)/argocd-crds": {
path: "projects/platform/components/argocd/crds"
cluster: Cluster.name
}
}
// Define #Platform.Components again!? Error?
Components: {
"\(Cluster.name)/argocd": {
path: "projects/platform/components/argocd/argocd"
cluster: Cluster.name
}
}
}
}
}
package holos
// Manage the component on every cluster in the platform
for Fleet in #Fleets {
for Cluster in Fleet.clusters {
#Platform: {
// Define #Platform.Components
Components: {
"\(Cluster.name)/external-secrets-crds": {
path: "projects/platform/components/external-secrets-crds"
cluster: Cluster.name
}
}
// Define #Platform.Components again!? Error?
Components: {
"\(Cluster.name)/external-secrets": {
path: "projects/platform/components/external-secrets"
cluster: Cluster.name
}
}
}
}
}
Unlike most other languages, it is common to declare the same field in multiple places. CUE unifies the value of the field. We can think of CUE as a Configuration Unification Engine.
Now that we know curly braces can be omitted and values are unified, we can understand how the rest of the CUE files in the platform directory behave.
Each CUE file in the platform directory adds components to the
#Platform.Components
struct.
The final file in the directory is responsible for producing the Platform spec. It looks like this.
- platform/platform.gen.cue
package holos
#Platform.Output
This file provides the value of the #Platform.Output
field, the platform spec,
to holos
.
Let's take a look at that Output value:
- Command
- Output
cue export --out yaml ./platform
kind: Platform
apiVersion: v1alpha3
metadata:
name: guide
spec:
model: {}
components: # This is a trimmed list for readability.
- path: projects/bank-of-holos/security/components/bank-secrets
cluster: workload
- path: projects/bank-of-holos/frontend/components/bank-frontend
cluster: workload
- path: projects/platform/components/argocd/crds
cluster: workload
- path: projects/platform/components/argocd/argocd
cluster: workload
- path: projects/platform/components/external-secrets-crds
cluster: workload
- path: projects/platform/components/external-secrets
cluster: workload
You don't normally need to execute cue
, CUE is built into holos
. We use it
here to gain insight.
We see the platform spec is essentially a list of components, each assigned to a cluster.
Notice CUE unifies Components
from multiple files into one list.
We'll see this unification behavior again and again. Unification is the defining characteristic of CUE that makes it a unique, powerful, and safe configuration language.
Holos takes this list of components and builds each one by executing:
holos render component --cluster-name="example" "path/to/the/component"
We can think of platform rendering as rendering a list of components, passing
the cluster name each time. Rendering each component writes the fully rendered
manifest for that component to the deploy/
directory, organized by cluster for
GitOps.
Render a Component
Rendering a component works much the same way as rendering a platform. holos
uses CUE to produce a specification, then processes it. The specification of a
component is called a BuildPlan. A BuildPlan is a list of zero or more
kubernetes resources, Helm charts, Kustomize bases, and additional files to
write into the deploy/
directory.
Let's take a look at the cert-manager component. Notice the
platform/cert-manager.cue
file has the field path: "projects/platform/components/cert-manager"
. This path indicates where to
start working with the cert-manager component.
- projects/platform/components/cert-manager/cert-manager.cue
- projects/cert-manager.cue
package holos
// Produce a helm chart build plan.
(#Helm & Chart).BuildPlan
let Chart = {
Name: "cert-manager"
// #CertManager is defined in projects/cert-manager.cue
Version: #CertManager.Version
Namespace: #CertManager.Namespace
Repo: name: "jetstack"
Repo: url: "https://charts.jetstack.io"
// CUE offers type checking and validation of Helm values.
Values: installCRDs: true
Values: startupapicheck: enabled: false
}
package holos
// Platform wide configuration
#CertManager: {
Version: "1.15.3"
Namespace: "cert-manager"
}
// Register the namespace
// The underscore indicates the value is defined elsewhere in CUE.
#Namespaces: (#CertManager.Namespace): _
This file introduces a few new concepts.
- Line 4 indicates this component produces a BuildPlan that wraps a Helm Chart.
- On line 6
let
binds a name to an expression for the current scope. The current file in this case. - Notice Chart is referenced on line 4 before it's bound on line 6. Order is irrelevant in CUE. Complex changes are simpler and easier when we don't have to think about order.
- The chart version and namespace are defined in a different file closer to the
root,
projects/cert-manager.cue
- We define Helm values in CUE to take advantage of strong type checking and manage multiple Helm charts consistently with platform wide values.
Let's take a look at the BuildPlan this CUE configuration defines.
- Command
- Output
cue export --out yaml ./projects/platform/components/cert-manager
kind: BuildPlan
apiVersion: v1alpha3
spec:
components:
resources:
gitops/cert-manager:
kind: KubernetesObjects
apiVersion: v1alpha3
metadata:
name: gitops/cert-manager
namespace: cert-manager
deployFiles:
clusters/no-name/gitops/cert-manager.application.gen.yaml: |
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager
namespace: argocd
spec:
destination:
server: https://kubernetes.default.svc
project: platform
source:
path: ./deploy/clusters/no-name/components/cert-manager
repoURL: https://github.com/jeffmccune/bank-of-holos
targetRevision: main
skip: false
helmChartList:
- kind: HelmChart
apiVersion: v1alpha3
chart:
name: cert-manager
version: 1.15.3
release: cert-manager
repository:
name: jetstack
url: https://charts.jetstack.io
valuesContent: |
installCRDs: true
startupapicheck:
enabled: false
enableHooks: false
metadata:
name: cert-manager
namespace: cert-manager
apiObjectMap: {}
skip: false
Again, you don't normally need to execute cue
, it's built into holos
. We
use it here to show how Holos works with Helm.
Looking at the BuildPlan, we see holos
will render the Helm chart into the
deploy directory along with an ArgoCD Application resource in the gitops/
directory.
The BuildPlan API is flexible enough to write any file into the deploy/
directory. Holos uses this flexibility to support both Flux and ArgoCD.
When we run cue export
, we get back a Core API BuildPlan. The BuildPlan is
produced by the #Helm
definition on line 4 which is part of the Author API.
The Core API is the contract between CUE and holos
. As such, it's not as
friendly as the Author API. The Author API is the contract component authors
and platform engineers use to configure and manage the platform. The Author API
is meant for people, the Core API is meant for machines. This explains why we
see quite a few fields in the exported BuildPlan we didn't cover in this guide.
Day to day we don't need to be concerned with those fields because the Author
API handles them for us.
Our intent is to provide an ergonomic way to manage the platform with the Author API.
When the Author API doesn't offer a path forward, authors may use the Core API directly from CUE. We can think of the Core API as an escape hatch for the Author API. We'll see some examples of this in action in the more advanced guides.
Next Steps
Thank you for finishing the Quickstart guide. Dive deeper with the next guide on how to Deploy a Service which explains how to take one of your existing Helm charts or Deployments and manage it with Holos.