DEV Community

ThankGod Chibugwum Obobo
ThankGod Chibugwum Obobo

Posted on • Originally published at actocodes.hashnode.dev

Goodbye YAML? My First Month with Pkl (Apple's Configuration Language)

YAML has been the configuration language of the cloud-native era by default, not by design. It is whitespace-sensitive in ways that cause real production incidents. It has no type system. It provides no mechanism for reuse, short of anchors and aliases that quickly become unreadable. It silently coerces values, the notorious NO becoming false, 1.0 becoming a float, and on becoming true, in ways that require memorization rather than reasoning.

Engineering teams have tolerated these limitations because the alternatives were worse, or at least less familiar. HCL is powerful but Terraform-specific. CUE is expressive but has a steep learning curve. Dhall is principled but alien to most developers. Jsonnet is underused and under-tooled.

Then Apple published Pkl (pronounced "pickle"), an open-source configuration language designed explicitly for the problems YAML fails to solve, type safety, reuse, abstraction, and validation built into the language rather than bolted on afterward.

After one month of using Pkl across real infrastructure configuration, Kubernetes manifests, application configs, and CI/CD templates, here is an honest account of what it does well, where it still struggles, and whether it is ready to replace YAML in a production environment.

What Is Pkl?

Pkl (Package Konfig Language) is a configuration language developed by Apple and open-sourced in February 2024. It compiles to JSON, YAML, XML, or plist, meaning it integrates with any toolchain that reads those formats, without requiring the entire ecosystem to adopt Pkl natively.

Its core design goals:

  • Type safety - every value has a type, enforced at evaluation time
  • Reuse through templates - configurations extend and override base templates
  • Validation built in - constraints are expressed in the language, not in external schema validators
  • Readability - syntax designed to be familiar to developers without YAML's whitespace traps
  • Evaluation, not just declaration - expressions, conditionals, and computed values are first-class

The compiler outputs are standard formats, so adopting Pkl is a source change, not an output change. Your Kubernetes cluster still receives YAML. Your application still reads JSON. Pkl lives in your repository and your CI pipeline, not in your runtime.

Syntax First: What Pkl Actually Looks Like

The fastest way to understand Pkl is to see it alongside the YAML it replaces.

Basic Value Declaration

# YAML
app:
  name: orders-service
  port: 3000
  debug: false
  replicas: 3
Enter fullscreen mode Exit fullscreen mode
# Pkl
app {
  name = "orders-service"
  port = 3000
  debug = false
  replicas = 3
}
Enter fullscreen mode Exit fullscreen mode

The structural difference is immediately visible: Pkl uses braces instead of indentation, = for assignment instead of :, and quoted strings that are unambiguously strings, "false" is a string, false is a boolean. There is no YAML parser needed to figure out which is which.

Type Annotations

// Pkl - explicit types on every field
class AppConfig {
  name: String
  port: Int(this > 1024 && this < 65535)   // inline constraint
  debug: Boolean
  replicas: Int(this >= 1 && this <= 10)
  environment: "staging" | "production" | "development"  // union type
}

app: AppConfig = new {
  name = "orders-service"
  port = 3000
  debug = false
  replicas = 3
  environment = "production"
}
Enter fullscreen mode Exit fullscreen mode

Setting replicas = 0 against this class fails at evaluation time with a clear error message, not at runtime when Kubernetes tries to create zero pods. Setting environment = "prod" (a typo) fails immediately because the value is not in the union type. This is the type safety YAML schemas attempt to provide through external validators, Pkl builds it directly into the language.

YAML Coercion Problems - Solved

# YAML - notorious coercion traps
enabled: yes        # becomes true
version: 1.0        # becomes float in some parsers
country: NO         # becomes false (Norway's ISO code!)
port: 8080          # integer
tag: "8080"         # string — same value, different type, different behavior
Enter fullscreen mode Exit fullscreen mode
# Pkl - unambiguous
enabled: Boolean = true
version: String = "1.0"
country: String = "NO"
port: Int = 8080
tag: String = "8080"
Enter fullscreen mode Exit fullscreen mode

Every value's type is explicit. The Norway NO incident, a real class of YAML bug that has affected Kubernetes configurations in production, cannot happen in Pkl.

Templates and Inheritance: The Killer Feature

YAML's anchor-and-alias system for reuse looks like this:

# YAML anchors - functional but unreadable at scale
.defaults: &defaults
  imagePullPolicy: IfNotPresent
  resources:
    requests:
      memory: "256Mi"
      cpu: "100m"
    limits:
      memory: "512Mi"
      cpu: "500m"

deployments:
  orders-service:
    <<: *defaults
    replicas: 3
  users-service:
    <<: *defaults
    replicas: 2
    resources:
      limits:
        memory: "1Gi"    # override — but does this merge or replace?
Enter fullscreen mode Exit fullscreen mode

The << merge key behavior is ambiguous in some parsers and was officially deprecated in YAML 1.2. Pkl's approach is unambiguous:

// Pkl - templates with explicit inheritance
class DeploymentDefaults {
  imagePullPolicy: String = "IfNotPresent"
  memoryRequest: String = "256Mi"
  cpuRequest: String = "100m"
  memoryLimit: String = "512Mi"
  cpuLimit: String = "500m"
}

class Deployment extends DeploymentDefaults {
  name: String
  replicas: Int = 1
}

ordersService: Deployment = new {
  name = "orders-service"
  replicas = 3
  // inherits all defaults
}

usersService: Deployment = new {
  name = "users-service"
  replicas = 2
  memoryLimit = "1Gi"    // explicitly override one field, all others inherited
}
Enter fullscreen mode Exit fullscreen mode

The inheritance is explicit, the override is unambiguous, and the IDE can show you exactly which fields are inherited versus overridden, something YAML anchors fundamentally cannot do.

Real-World Usage: Kubernetes Manifests

Here is a production-representative Pkl template for a Kubernetes Deployment, showing how templating applies to the configuration I write most often:

// k8s/templates/deployment.pkl
module deployment

import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0#/api/apps/v1/Deployment.pkl"

class AppDeployment {
  name: String
  namespace: String = "default"
  image: String
  tag: String
  replicas: Int(this >= 1) = 1
  port: Int = 8080
  cpuRequest: String = "100m"
  memoryRequest: String = "256Mi"
  cpuLimit: String = "500m"
  memoryLimit: String = "512Mi"
  env: Listing<EnvVar> = new {}
}

class EnvVar {
  name: String
  value: String
}

function toK8sDeployment(config: AppDeployment): Deployment = new {
  metadata {
    name = config.name
    namespace = config.namespace
    labels { app = config.name }
  }
  spec {
    replicas = config.replicas
    selector { matchLabels { app = config.name } }
    template {
      metadata { labels { app = config.name } }
      spec {
        containers {
          new {
            name = config.name
            image = "\(config.image):\(config.tag)"
            ports { new { containerPort = config.port } }
            resources {
              requests {
                cpu = config.cpuRequest
                memory = config.memoryRequest
              }
              limits {
                cpu = config.cpuLimit
                memory = config.memoryLimit
              }
            }
            envFrom = config.env
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
// k8s/services/orders-service.pkl
import "../templates/deployment.pkl"

output {
  value = deployment.toK8sDeployment(new deployment.AppDeployment {
    name = "orders-service"
    namespace = "production"
    image = "your-org/orders-service"
    tag = "2.4.1"
    replicas = 3
    env {
      new { name = "DATABASE_URL"; value = "postgresql://db:5432/orders" }
      new { name = "NODE_ENV"; value = "production" }
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Compile to YAML for Kubernetes:

pkl eval k8s/services/orders-service.pkl -f yaml > k8s/manifests/orders-service.yaml
Enter fullscreen mode Exit fullscreen mode

The manifest is generated, not handwritten. The template enforces the constraint that replicas is at least 1. The image and tag are separate fields, no string concatenation errors. The output is standard Kubernetes YAML that kubectl apply consumes without modification.

CI/CD Integration

Pkl integrates into CI/CD pipelines anywhere you currently generate or validate YAML:

# .github/workflows/pkl-generate.yml
name: Generate and Validate Configs

on: [push, pull_request]

jobs:
  pkl-generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Pkl CLI
        run: |
          curl -L https://github.com/apple/pkl/releases/latest/download/pkl-linux-amd64 \
            -o /usr/local/bin/pkl
          chmod +x /usr/local/bin/pkl

      - name: Validate all Pkl configurations
        run: pkl eval --multiple-file-output /dev/null pkl/**/*.pkl

      - name: Generate Kubernetes manifests
        run: |
          for file in k8s/services/*.pkl; do
            service=$(basename "$file" .pkl)
            pkl eval "$file" -f yaml > "k8s/manifests/${service}.yaml"
          done

      - name: Commit generated manifests
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add k8s/manifests/
          git diff --cached --quiet || git commit -m "chore: regenerate manifests from Pkl"
          git push
Enter fullscreen mode Exit fullscreen mode

Honest Assessment After One Month

What Pkl Gets Right

Type safety is genuinely useful. Finding configuration errors at compile time rather than at kubectl apply time, or worse, at runtime, is a real quality improvement. Inline constraints like Int(this > 0) replace entire schema validation workflows.

Templates and inheritance are the best solution I've seen. The YAML anchor problem is real. Pkl's class-based inheritance with explicit overrides is significantly cleaner than any YAML reuse mechanism.

String interpolation without escaping traps. "\(config.image):\(config.tag)" is readable and unambiguous. YAML multiline string handling is notoriously confusing.

Evaluation-time computation. Generating environment-specific configuration from a single template with conditional expressions reduces the number of files to maintain.

Where Pkl Still Struggles

Ecosystem maturity is early. The Kubernetes package library (pkl-k8s) covers most resources but not all. For anything not in the package, you're writing raw mappings to target types, which works but loses the ergonomic advantage.

IDE support is improving but incomplete. The VS Code extension provides syntax highlighting and basic completion. IntelliJ support is more mature. Neither yet matches the experience of working in a fully-supported language.

Team adoption friction. Pkl is not YAML. Developers familiar with YAML need to learn new syntax, new mental models for inheritance, and a new compilation step. This is a real cost that requires team buy-in.

Debugging compilation errors. When a constraint fails or a type mismatch occurs in a deeply nested template, the error messages are sometimes less precise than you'd want. This is improving with each release.

Not every toolchain accepts Pkl natively. Tools that consume configuration at runtime, some CI/CD platforms, older infrastructure tools, can't read Pkl directly. The compilation step to YAML/JSON is necessary, which adds pipeline complexity.

Should You Adopt Pkl?

Adopt Pkl today if:

  • You maintain large, complex YAML configurations with significant duplication
  • Type errors and coercion bugs have caused real incidents in your environment
  • Your team writes configuration as code and values IDE support
  • You're already compiling or generating YAML (Helm charts, Kustomize overlays), Pkl replaces the templating layer cleanly

Wait on Pkl if:

  • Your YAML configs are small and simple, the overhead isn't justified
  • Your team doesn't have bandwidth for adoption friction right now
  • You need native tool support without a compilation step
  • You're evaluating for a production migration within the next quarter, the ecosystem is maturing fast, but not yet complete

Conclusion

After one month, Pkl has earned a permanent place in my infrastructure toolchain, but not as a wholesale YAML replacement, yet. It is the right tool for complex, template-heavy configuration that benefits from type safety and reuse. It is not yet the obvious choice for simple, stable configuration that existing YAML handles without friction.

The trajectory is clear, Pkl is solving real problems that YAML ignores and doing so with a thoughtfulness that most configuration languages lack. The ecosystem will catch up. The IDE tooling will improve. Team familiarity will build.

YAML isn't dead. But for the first time in years, there's a credible successor that makes the case on engineering merit rather than novelty.

Have you tried Pkl in your infrastructure? What was your first-month experience, similar frustrations, different use cases? Share in the comments.

Pkl #YAML #ConfigurationAsCode #DevOps #InfrastructureAsCode

Top comments (0)