Blueprint Best Practices

Follow these guidelines to create blueprints that are maintainable, secure, reusable, and cost-effective.

Structure & Organization

Use Descriptive Names

# ✅ Good - Clear, descriptive names
metadata:
  name: kubernetes-cicd-workshop
  
resources:
  clusters:
    - name: training-cluster
  vms:
    - name: participant-workstation-{{ .Index }}

# ❌ Avoid - Vague names
metadata:
  name: workshop-1

resources:
  clusters:
    - name: main
  vms:
    - name: vm-{{ .Index }}

Organize Variables Logically

Group related variables and use UI hints:

variables:
  # Group 1: General settings
  - name: participant_count
    type: integer
    default: 10
    ui:
      label: "Participant Count"
      group: "General"
      order: 1

  - name: environment_ttl
    type: string
    default: "8h"
    ui:
      label: "Environment Lifetime"
      group: "General"
      order: 2

  # Group 2: Cluster configuration
  - name: k8s_version
    type: string
    default: "1.28"
    ui:
      label: "Kubernetes Version"
      group: "Cluster"
      order: 1

  - name: worker_count
    type: integer
    default: 3
    ui:
      label: "Worker Nodes"
      group: "Cluster"
      order: 2

  # Group 3: Advanced (hidden by default)
  - name: custom_cni
    type: string
    default: "calico"
    ui:
      label: "CNI Plugin"
      group: "Advanced"
      hidden: true

Document Everything

metadata:
  name: cicd-workshop
  description: |
    Complete CI/CD training environment with GitLab, runners,
    and participant workstations.
    
    Includes:
    - GitLab CE with container registry
    - Configurable number of CI runners
    - Individual participant VMs with dev tools
    
    Recommended for groups of 5-30 participants.

variables:
  - name: gitlab_version
    type: string
    default: "16.8"
    ui:
      label: "GitLab Version"
      description: |
        GitLab CE version to deploy. Check compatibility
        with your runner images before changing.

Security

Never Hardcode Secrets

# ❌ NEVER do this
resources:
  secrets:
    - name: api-key
      data:
        token: "sk-abc123def456"  # Hardcoded secret!

# ✅ Generate secrets
resources:
  secrets:
    - name: api-key
      type: generated
      spec:
        length: 32
        charset: alphanumeric

# ✅ Reference external secrets
resources:
  secrets:
    - name: api-key
      type: external
      spec:
        provider: vault
        path: secret/data/api-keys/myservice
        key: token

Use Minimal Permissions

# ✅ Restrict participant permissions
namespaceConfig:
  # Limited resource quotas
  resourceQuota:
    cpu: "4"
    memory: "8Gi"
    pods: "20"
  
  # Network isolation
  networkPolicy:
    isolate: true
    allowEgress: true
    allowIngress: false

# ✅ Don't give cluster-admin to participants
access:
  authentication:
    defaultRole: edit  # Not admin or cluster-admin

Validate Input

variables:
  - name: domain_prefix
    type: string
    required: true
    constraints:
      minLength: 3
      maxLength: 20
      # Only allow safe characters
      pattern: "^[a-z][a-z0-9-]*$"
    ui:
      description: "Letters, numbers, and hyphens only. Must start with a letter."

  - name: admin_email
    type: string
    required: true
    constraints:
      # Validate email format
      pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"

Restrict Image Sources

# ✅ Use specific image tags, not :latest
resources:
  vms:
    - name: workstation
      image: ubuntu-22.04  # Specific version
      # Not: ubuntu:latest

  helm:
    - name: app
      values:
        image:
          repository: myregistry.com/app
          tag: "1.5.2"  # Pinned version
          # Not: latest

Cost Optimization

Set Appropriate Defaults

# ✅ Default to smaller resources
variables:
  - name: worker_size
    type: string
    default: "cx21"  # Small by default
    constraints:
      enum: ["cx21", "cx31", "cx41", "cx51"]
    ui:
      description: "Larger instances cost more. Start small."

  - name: worker_count
    type: integer
    default: 2  # Minimum viable
    constraints:
      min: 1
      max: 10

Always Set TTL

# ✅ Prevent runaway costs
environment:
  ttl: 8h  # Auto-destroy after 8 hours
  
  # Or make it configurable with a reasonable default
variables:
  - name: ttl
    type: string
    default: "8h"
    constraints:
      enum: ["4h", "8h", "24h", "48h", "72h"]
    ui:
      label: "Environment Lifetime"
      description: "Environment auto-destroys after this time"

environment:
  ttl: "{{ .Variables.ttl }}"

Enable Sleep Mode

# ✅ Auto-sleep inactive environments
environment:
  sleepMode:
    enabled: true
    idleTimeout: 30m  # Sleep after 30 min of inactivity
    mode: scale-to-zero

Right-Size Resources

# ✅ Scale resources based on participant count
resources:
  clusters:
    - name: main
      nodes:
        # Scale workers with participants, but within limits
        workers: "{{ min 10 (max 2 (div .Variables.participant_count 5)) }}"

# Example: 
# 5 participants → 2 workers (minimum)
# 20 participants → 4 workers
# 50 participants → 10 workers (maximum)

Use Appropriate Isolation

# ✅ Choose isolation based on actual needs
# For basic training: namespace (cheapest)
environment:
  isolation: namespace

# For admin training: vcluster (moderate)
environment:
  isolation: vcluster

# Only when necessary: dedicated cluster (expensive)
environment:
  isolation: dedicated-cluster

Reusability

Use Variables for Everything Configurable

# ❌ Hardcoded values
resources:
  clusters:
    - name: main
      nodes:
        workers: 3
      version: "1.28"

# ✅ Everything is configurable
variables:
  - name: cluster_name
    default: "main"
  - name: worker_count
    default: 3
  - name: k8s_version
    default: "1.28"

resources:
  clusters:
    - name: "{{ .Variables.cluster_name }}"
      nodes:
        workers: "{{ .Variables.worker_count }}"
      version: "{{ .Variables.k8s_version }}"

Create Composable Blueprints

# base-cluster.yaml - Reusable base
apiVersion: teabar.dev/v1
kind: Blueprint
metadata:
  name: base-cluster
spec:
  variables:
    - name: cluster_name
      default: "main"
    - name: worker_count
      default: 3
  
  resources:
    clusters:
      - name: "{{ .Variables.cluster_name }}"
        # Base configuration

# workshop.yaml - Extends base
spec:
  extends:
    - name: org/base-cluster
  
  # Only add workshop-specific resources
  resources:
    helm:
      - name: gitlab

Make Extension Points Clear

metadata:
  description: |
    Base Kubernetes cluster blueprint.
    
    Extension points:
    - Override `worker_count` for different sizes
    - Override `addons` to change installed components
    - Add resources in extending blueprint

variables:
  - name: worker_count
    ui:
      description: "Override in extending blueprint for different sizes"
  
  - name: addons
    type: object
    default:
      metricsServer: true
      ingressNginx: true
      certManager: false
    ui:
      description: "Override to enable/disable cluster addons"

Maintainability

Use Semantic Versioning

metadata:
  name: my-blueprint
  version: 1.2.3
  # Major.Minor.Patch
  # 
  # Increment MAJOR when you make breaking changes
  # Increment MINOR when you add features (backward compatible)
  # Increment PATCH when you fix bugs (backward compatible)

Keep a Changelog

metadata:
  name: cicd-workshop
  version: 2.1.0
  description: |
    CI/CD workshop environment.
    
    Changelog:
    - 2.1.0: Added ArgoCD support, updated GitLab to 16.x
    - 2.0.0: BREAKING: Changed isolation to vcluster
    - 1.5.0: Added participant VM option
    - 1.4.0: Added monitoring stack

Test Before Publishing

# 1. Validate syntax
teactl blueprint validate -f blueprint.yaml

# 2. Render with edge cases
teactl blueprint render -f blueprint.yaml --var participant_count=1
teactl blueprint render -f blueprint.yaml --var participant_count=50

# 3. Create test environment
teactl env create --blueprint ./blueprint.yaml --name test --dry-run
teactl env create --blueprint ./blueprint.yaml --name test

# 4. Verify all resources
teactl env status test --verbose

# 5. Test participant access
teactl participant create test --name tester --generate-password

# 6. Clean up
teactl env destroy test --yes

Handle Deprecation Gracefully

variables:
  # Old variable (deprecated)
  - name: num_workers
    type: integer
    ui:
      label: "Worker Count (DEPRECATED)"
      description: "Use worker_count instead. Will be removed in v3.0"
      hidden: true

  # New variable
  - name: worker_count
    type: integer
    default: "{{ default 3 .Variables.num_workers }}"
    ui:
      label: "Worker Count"

Performance

Minimize Provisioning Time

# ✅ Provision independent resources in parallel
# Teabar automatically parallelizes when there are no dependencies

resources:
  # These provision in parallel (no dependencies)
  clusters:
    - name: cluster-a
    - name: cluster-b
  
  vms:
    - name: bastion
  
  # These wait for clusters (have dependencies)
  helm:
    - name: argocd
      cluster: cluster-a  # Depends on cluster-a

Use Efficient Images

# ✅ Use minimal images
resources:
  vms:
    - name: workstation
      image: ubuntu-22.04-minimal  # Smaller image = faster boot
      
  helm:
    - name: app
      values:
        image:
          repository: myapp
          tag: "1.0-alpine"  # Alpine = smaller image

Lazy-Load Optional Components

variables:
  - name: enable_monitoring
    type: boolean
    default: false
    ui:
      description: "Enable monitoring stack (adds 2-3 min to provisioning)"

resources:
  helm:
    {{- if .Variables.enable_monitoring }}
    - name: prometheus
      chart: prometheus-community/kube-prometheus-stack
    {{- end }}

Common Patterns

Participant Numbering

# 1-based numbering (more natural for users)
resources:
  vms:
    - name: "participant-{{ add .Index 1 }}"
      count: "{{ .Variables.participant_count }}"
      # Creates: participant-1, participant-2, ...

Environment-Aware URLs

resources:
  helm:
    - name: app
      values:
        ingress:
          hosts:
            - "app.{{ .Environment.Domain }}"
        env:
          BASE_URL: "https://app.{{ .Environment.Domain }}"
          ENVIRONMENT: "{{ .Environment.Name }}"

Conditional Components

variables:
  - name: tier
    type: string
    default: "basic"
    constraints:
      enum: ["basic", "standard", "premium"]

resources:
  helm:
    # Always included
    - name: core-app
    
    # Standard and above
    {{- if or (eq .Variables.tier "standard") (eq .Variables.tier "premium") }}
    - name: monitoring
    {{- end }}
    
    # Premium only
    {{- if eq .Variables.tier "premium" }}
    - name: advanced-analytics
    {{- end }}

Default + Override Pattern

variables:
  - name: helm_values_override
    type: object
    default: {}
    ui:
      label: "Helm Values Override"
      description: "Deep-merged with default values"
      component: code

resources:
  helm:
    - name: app
      values: |
        {{- $defaults := dict "replicas" 2 "debug" false }}
        {{- $merged := merge .Variables.helm_values_override $defaults }}
        {{- toYaml $merged | nindent 8 }}

Checklist

Before publishing a blueprint, verify:

file-text

Documentation

- [ ] Meaningful name and description - [ ] All variables have descriptions - [ ] UI labels and groups set - [ ] Changelog maintained

Security

- [ ] No hardcoded secrets - [ ] Input validation on all variables - [ ] Minimal permissions for participants - [ ] Network policies configured
dollar-sign

Cost

- [ ] TTL configured - [ ] Sleep mode enabled (if applicable) - [ ] Resource limits set - [ ] Reasonable defaults

Testing

- [ ] Validates without errors - [ ] Renders with edge case values - [ ] Successfully creates environment - [ ] All resources provision correctly

Next Steps

ende