Lesson 7.1: Packaging and Distribution

Introduction

Before deploying operators to production, they need to be packaged and distributed. This lesson covers building container images, creating Helm charts, and packaging operators for distribution via OLM (Operator Lifecycle Manager).

Theory: Packaging and Distribution

Packaging operators enables reliable, repeatable deployments across environments.

Why Packaging Matters

Reproducibility:

  • Same operator version everywhere
  • Consistent deployments
  • Version control
  • Rollback capability

Distribution:

  • Share operators with teams
  • Deploy to multiple clusters
  • Enable operator marketplace
  • Simplify installation

Deployment:

  • Standard deployment methods
  • Helm charts for easy install
  • OLM for operator marketplace
  • Container images for portability

Packaging Strategies

Container Images:

  • Standard format
  • Works everywhere
  • Versioned
  • Portable

Helm Charts:

  • Package operator + dependencies
  • Parameterized configuration
  • Easy upgrades
  • Community standard

OLM Bundles:

  • Operator marketplace format
  • Metadata and manifests
  • Version management
  • Dependency resolution

Versioning

Semantic Versioning:

  • Major: Breaking changes
  • Minor: New features
  • Patch: Bug fixes

Version Tags:

  • latest: Latest version
  • v1.2.3: Specific version
  • v1.2: Latest patch of minor version
  • stable: Stable release

Understanding packaging helps you distribute operators effectively.

Operator Packaging Flow

Here’s how operators are packaged and distributed:

graph TB
    SOURCE[Source Code] --> BUILD[Build Image]
    BUILD --> REGISTRY[Container Registry]
    
    SOURCE --> HELM[Create Helm Chart]
    HELM --> CHART[Helm Chart]
    
    SOURCE --> OLM[Create OLM Bundle]
    OLM --> BUNDLE[OLM Bundle]
    
    REGISTRY --> DEPLOY[Deploy]
    CHART --> DEPLOY
    BUNDLE --> DEPLOY
    
    style BUILD fill:#90EE90
    style REGISTRY fill:#FFB6C1

Building Container Images

Kubebuilder generates a Dockerfile for you when you scaffold a project. It uses multi-stage builds for optimal image size and security.

Kubebuilder-Generated Dockerfile

When you run kubebuilder init, it creates a Dockerfile in your project root:

# Build stage
FROM golang:1.24 as builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# Cache deps before building and copying source
RUN go mod download

# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/ internal/

# Build
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -a -o manager cmd/main.go

# Runtime stage
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]

Note: The internal/ directory is copied entirely because it contains both:

  • internal/controller/ - Your reconciliation logic
  • internal/webhook/ - Webhook handlers (if you created webhooks in Module 5)

Building with Kubebuilder’s Makefile

Kubebuilder provides Makefile targets for building images:

# Build the container image
make docker-build IMG=<registry>/postgres-operator:v0.1.0

# Push to registry
make docker-push IMG=<registry>/postgres-operator:v0.1.0

# Build and push in one command
make docker-build docker-push IMG=<registry>/postgres-operator:v0.1.0

Image Build Process

sequenceDiagram
    participant Dev
    participant Make as Makefile
    participant Docker
    participant Registry
    
    Dev->>Make: make docker-build IMG=...
    Make->>Docker: docker build
    Docker->>Docker: Build Go binary
    Docker->>Docker: Create distroless image
    Docker->>Docker: Tag image
    Dev->>Make: make docker-push IMG=...
    Make->>Docker: docker push
    Docker->>Registry: Push image
    Registry-->>Dev: Image available
    
    Note over Docker: Multi-stage build<br/>for smaller images

Loading Images to kind

For local development with kind clusters:

# Build the image
make docker-build IMG=postgres-operator:latest

# Load into kind cluster
kind load docker-image postgres-operator:latest --name k8s-operators-course

Helm Charts for Operators

While kubebuilder uses Kustomize for deployment by default (config/ directory), you can create Helm charts for wider distribution. The manifests generated by kubebuilder can be used as a basis for Helm templates.

What an Operator Helm Chart Needs

A complete operator Helm chart must include all components from kubebuilder’s config/ directory:

Component Source Directory Purpose
CRDs config/crd/ Custom Resource Definitions
RBAC config/rbac/ ServiceAccount, ClusterRole, ClusterRoleBinding
Deployment config/manager/ Controller manager pod
Webhooks config/webhook/ Validating/Mutating webhooks (if used)
Certificates config/certmanager/ Webhook certificates (if using cert-manager)

Important: A Helm chart with only the Deployment won’t work! The operator needs RBAC permissions to function and CRDs must be installed for the operator to manage custom resources.

Chart Structure

graph TB
    CHART[Helm Chart]
    
    CHART --> TEMPLATES[templates/]
    CHART --> VALUES[values.yaml]
    CHART --> CHARTS[Chart.yaml]
    
    TEMPLATES --> CRD[crds.yaml]
    TEMPLATES --> RBAC[rbac.yaml]
    TEMPLATES --> DEPLOYMENT[deployment.yaml]
    TEMPLATES --> WEBHOOK[webhook.yaml]
    TEMPLATES --> HELPERS[_helpers.tpl]
    
    style CHART fill:#90EE90
    style CRD fill:#FFB6C1
    style RBAC fill:#FFB6C1

Kubebuilder’s Kustomize vs Helm

Kubebuilder generates Kustomize manifests in config/:

config/
├── crd/                    # CRD definitions
│   └── bases/
├── default/                # Default deployment configuration
├── manager/                # Controller deployment
├── rbac/                   # RBAC rules
├── webhook/                # Webhook configuration
└── samples/                # Sample CR manifests

To deploy with Kustomize (recommended for development):

# Deploy the operator
make deploy IMG=<registry>/postgres-operator:v0.1.0

# This runs: kustomize build config/default | kubectl apply -f -

To create a Helm chart (for distribution):

# Create Helm chart directory
mkdir -p charts/postgres-operator/templates

# Export kustomize output as a starting point
kustomize build config/default > charts/postgres-operator/templates/all.yaml

# Then split into separate files and add templating

OLM Bundles

OLM Bundle Structure

graph TB
    BUNDLE[OLM Bundle]
    
    BUNDLE --> MANIFESTS[manifests/]
    BUNDLE --> METADATA[metadata/]
    
    MANIFESTS --> CRD[CRDs]
    MANIFESTS --> CSV[ClusterServiceVersion]
    MANIFESTS --> RBAC[RBAC]
    
    METADATA --> ANNOTATIONS[annotations.yaml]
    
    style BUNDLE fill:#FFB6C1

Bundle Creation

While kubebuilder focuses on controller development, you can use operator-sdk alongside kubebuilder for OLM bundle generation:

# Initialize operator-sdk integration (if not already done)
operator-sdk init --plugins=manifests

# Generate OLM bundle from kubebuilder manifests
operator-sdk generate bundle \
  --version 0.1.0 \
  --package postgres-operator \
  --channels stable

# Creates:
# bundle/
#   manifests/
#     postgres-operator.clusterserviceversion.yaml
#     database.example.com_databases.yaml
#   metadata/
#     annotations.yaml

Note: For most use cases, kubebuilder’s built-in make deploy with Kustomize is sufficient. OLM bundles are primarily needed when publishing to operator marketplaces like OperatorHub.

Versioning Strategy

Semantic Versioning

graph LR
    VERSION[Version]
    
    VERSION --> MAJOR[Major: Breaking]
    VERSION --> MINOR[Minor: Features]
    VERSION --> PATCH[Patch: Fixes]
    
    MAJOR --> 1.0.0
    MINOR --> 1.1.0
    PATCH --> 1.1.1
    
    style VERSION fill:#90EE90

Version format: v<major>.<minor>.<patch>

  • Major: Breaking API changes
  • Minor: New features, backward compatible
  • Patch: Bug fixes, backward compatible

Distribution Strategies

Strategy 1: Container Registry

graph LR
    BUILD[Build] --> TAG[Tag]
    TAG --> PUSH[Push]
    PUSH --> REGISTRY[Registry]
    REGISTRY --> PULL[Pull]
    PULL --> DEPLOY[Deploy]
    
    style REGISTRY fill:#FFB6C1

Strategy 2: Helm Repository

graph LR
    PACKAGE[Package Chart] --> REPO[Helm Repo]
    REPO --> ADD[helm repo add]
    ADD --> INSTALL[helm install]
    
    style REPO fill:#90EE90

Strategy 3: OLM Catalog

graph LR
    BUNDLE[Create Bundle] --> CATALOG[OLM Catalog]
    CATALOG --> SUBSCRIBE[Subscribe]
    SUBSCRIBE --> INSTALL[Install]
    
    style CATALOG fill:#FFB6C1

Image Optimization

Multi-Stage Builds

# Stage 1: Build
FROM golang:1.24 AS builder
# ... build steps ...

# Stage 2: Runtime
FROM gcr.io/distroless/static:nonroot
# ... copy binary only ...

Benefits:

  • Smaller final image
  • No build tools in production
  • Better security (distroless)

Image Size Comparison

graph LR
    FULL[Full Image<br/>~800MB] --> OPTIMIZED[Optimized<br/>~50MB]
    
    OPTIMIZED --> DISTROLESS[Distroless<br/>~20MB]
    
    style FULL fill:#FFB6C1
    style OPTIMIZED fill:#FFE4B5
    style DISTROLESS fill:#90EE90

Automating with CI/CD

GitHub Actions for Releases

Automate releases with GitHub Actions:

# .github/workflows/release.yaml
name: Release
on:
  push:
    tags: ['v*']
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push image
        run: make docker-build docker-push IMG=ghcr.io/${{ github.repository }}:${{ github.ref_name }}
      - name: Generate and push Helm chart
        run: |
          make helm-chart helm-package
          helm push dist/*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts

Helm Chart Distribution

Modern approach: Push Helm charts to OCI registries (like GHCR):

# Push chart to OCI registry
helm push postgres-operator-0.1.0.tgz oci://ghcr.io/myorg/charts

# Install from OCI registry
helm install my-operator oci://ghcr.io/myorg/charts/postgres-operator --version 0.1.0

Key Takeaways

  • Kubebuilder generates a production-ready Dockerfile
  • make docker-build builds container images with proper tagging
  • Kustomize is the default deployment method in kubebuilder
  • make helm-chart can generate Helm charts from Kustomize
  • OCI registries can host both images AND Helm charts
  • GitHub Actions automate releases and chart publishing
  • Semantic versioning tracks operator versions
  • Multi-stage builds create smaller, secure images

Understanding for Building Operators

When packaging kubebuilder operators:

  • Use make docker-build IMG=... to build images
  • Use make docker-push IMG=... to push to registry
  • Use make deploy IMG=... for Kustomize-based deployment
  • Use make helm-chart to generate Helm charts from Kustomize
  • Set up GitHub Actions for automated releases
  • Push Helm charts to OCI registries for distribution
  • Follow semantic versioning for your operator
  • Use kind’s image loading for local development

References

Official Documentation

Further Reading

  • Kubernetes Operators by Jason Dobies and Joshua Wood - Chapter 12: Packaging
  • Docker Deep Dive by Nigel Poulton - Container image best practices
  • Helm Best Practices

Next Steps

Now that you understand packaging, let’s learn about RBAC and security.