Back to Blog
August 17, 2026

How to Write a Microsoft DSC Resource in Go in Under 100 Lines

Share

How to Write a Microsoft DSC Resource in Go in Under 100 Lines

Date: 2026-08-17

Master Microsoft Desired State Configuration in Go with just 63 lines by leveraging the DSC Go Resource Development Kit. Skip boilerplate, focus on logic.

Tags: ["Microsoft DSC", "Go", "DevOps", "Infrastructure as Code"]

The complexity of writing custom Microsoft Desired State Configuration (DSC) resources has long been a barrier for many developers. While DSC’s declarative model is powerful for enforcing system configurations, the protocol’s intricate plumbing — handling JSON over commands, managing exit codes, producing manifests — often turns resource development into tedious boilerplate work.

Fortunately, recent efforts have made significant strides to simplify this. Among them, the dsc-go-rdk (Resource Development Kit) for Go abstracts away all the protocol noise, letting you write a fully functional DSC resource in under 100 lines of code. In this post, we’ll unpack what’s required by Microsoft’s DSC protocol, see how the Go RDK handles this complexity, and walk through writing a minimal, idiomatic DSC resource managing a file’s contents.

You’ll learn not only how to get started quickly but also gain insight into the protocol’s mechanics. We’ll explore schema generation, implementing resource operations (Get, Set, Delete), manifest generation, and how to drive and debug the resource manually — all culminating in a smooth integration with the DSC engine.

Architecture Overview

┌─────────────────────────────────────────────┐
│          Microsoft Desired State             │
│          Configuration Engine                │
│                                             │
│ • Invokes resource executables               │
│ • Passes JSON input/output                    │
│ • Manages resource discovery via manifests   │
└─────────────────────────────────────────────┘
                    │
                    ↓
┌─────────────────────────────────────────────┐
│           DSC Go Resource Development Kit   │
│─────────────────────────────────────────────│
│ • Abstracts DSC protocol plumbing            │
│ • Generates JSON Schema & manifest            │
│ • Handles method dispatch (Get, Set, Delete) │
│ • Maps errors to exit codes                    │
└─────────────────────────────────────────────┘
                    │
                    ↓
┌─────────────────────────────────────────────┐
│          Custom DSC Resource (File Example)  │
│─────────────────────────────────────────────│
│ • Models desired state as struct              │
│ • Implements typed handler methods            │
│ • Minimal Go code focused on domain logic     │
└─────────────────────────────────────────────┘

Key Technical Observations

  • Protocol is JSON Command-Based — DSC resources communicate via JSON input and output over command-line invocations, requiring strict conformance to a nuanced contract for methods like get, set, test, delete, and export.

  • Boilerplate Complexity is Significant — Parsing input flags and JSON, generating manifests with embedded schemas, logging with JSON to STDERR, and mapping errors to specific exit codes are tedious but necessary plumbing that distracts from resource logic.

  • RDK Abstracts Protocol Details Elegantly — The Go RDK lets you declare your resource’s state as a plain Go struct with tags for JSON schema generation, and implement only the methods you care about (Gettable, Settable, etc.). It handles command dispatch, input/output framing, and manifest generation seamlessly.

  • Idempotency & Error Handling Built-in — The library enforces best practices such as treating “not found” states as valid absence (not error), making Delete idempotent, and correctly translating Go errors into DSC exit codes.

  • Manifest Generation as Build Step — The manifest JSON describing the resource's capabilities and schema is generated by the binary itself, avoiding drift and manual upkeep.

  • Engine-Agnostic Development and Debugging — Since the resource speaks the protocol directly, you can debug and test all operations from your shell without relying on the DSC engine.

How It Works

Step 1: Model the State as a Struct

The DSC resource's state is modeled as a Go struct. This struct defines the fields representing the desired and actual configuration, decorated with JSON tags to control schema generation.

type File struct {
    dsc.ExistProperty
    Path    string `json:"path" description:"Absolute path of the file to manage."`
    Content string `json:"content,omitempty" description:"The text the file should contain."`
}
  • The dsc.ExistProperty embed defines a canonical _exist boolean to indicate if an instance should exist.
  • The required Path property is enforced by omitting omitempty.
  • Optional properties like Content have omitempty to make them not required.
  • Descriptions become part of the generated JSON Schema for documentation and validation.

Running dscfile schema outputs the resource’s JSON Schema automatically, ensuring the manifest and engine are always in sync:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["path"],
  "properties": {
    "_exist":  { "type": "boolean", "default": true, "description": "Indicates whether the instance should exist." },
    "path":    { "type": "string", "description": "Absolute path of the file to manage." },
    "content": { "type": "string", "description": "The text the file should contain." }
  }
}

Step 2: Implement the Operations You Care About

You implement typed methods on a handler struct. Only Get is mandatory; others like Set and Delete are opt-in.

type Handler struct{}

func (Handler) Get(_ context.Context, in File) (File, error) {
    if in.Path == "" {
        return in, dsc.NewExitCodeErrorf(dsc.ExitInvalidInput, "path is required")
    }
    data, err := os.ReadFile(in.Path)
    if errors.Is(err, fs.ErrNotExist) {
        return dsc.NotFound(in, "Go.Resource/File", in.Path)
    }
    if err != nil {
        return in, err
    }
    return File{Path: in.Path, Content: string(data)}, nil
}

func (Handler) Set(_ context.Context, desired File) (File, error) {
    if err := os.WriteFile(desired.Path, []byte(desired.Content), 0o644); err != nil {
        return desired, err
    }
    return File{Path: desired.Path, Content: desired.Content}, nil
}

func (Handler) Delete(_ context.Context, in File) error {
    if err := os.Remove(in.Path); err != nil && !errors.Is(err, fs.ErrNotExist) {
        return err
    }
    return nil
}
  • The Get method returns a DSC-recognized "not found" state when the file is absent — a crucial distinction from erroring out.
  • Set writes the file content to disk.
  • Delete is idempotent, succeeding even if the file is already gone.
  • Errors propagate and map transparently to DSC’s exit code specification.
  • The lack of a Test method here means the DSC engine will synthesize one via comparing Get output to the desired state.
Interface Manifest Capability
Gettable (required) get
Settable set
Testable test
Deletable delete
Exportable export

Step 3: Declare the Resource

In main.go, wrap your types and handlers using the RDK’s MustResource constructor specifying identity and behavior.

func main() {
    r := dsc.MustResource[File](Handler{}, dsc.ResourceConfig{
        Type:        "Go.Resource/File",
        Version:     "0.1.0",
        Description: "Manages the content of a text file.",
        Tags:        []string{"file", "demo"},
        SetReturn:   dsc.SetReturnStateAndDiff,
    })
    r.Main("dscfile")
}
  • The Type follows the pattern <owner>[.<group>][.<area>]/<name>.
  • The semantic Version ensures proper versioning in DSC.
  • SetReturnStateAndDiff instructs the library to automatically calculate and report changed properties by calling your Get before and after your Set.
  • A single call to r.Main("dscfile") drives the entire CLI interface, including subcommands (get, set, manifest), input parsing, output formatting, and error handling.

Step 4: Drive the Protocol by Hand

Before involving the DSC engine, you can build and run the binary directly to test operations:

go build -o dscfile.exe .

# Check a non-existent file's state
.\dscfile.exe get --input '{"path":"C:/temp/test.txt"}'
# {"_exist":false,"path":"C:/temp/test.txt"}

# Create/Update the file
.\dscfile.exe set --input '{"path":"C:/temp/test.txt","content":"hello dsc"}'
# {"path":"C:/temp/test.txt","content":"hello dsc"}
# ["content","path"]

# Update content property alone
.\dscfile.exe set --input '{"path":"C:/temp/test.txt","content":"hello again"}'
# {"path":"C:/temp/test.txt","content":"hello again"}
# ["content"]

# Delete the file (prints nothing by contract)
.\dscfile.exe delete --input '{"path":"C:/temp/test.txt"}'

The diff arrays reported after set calls demonstrate the RDK’s internal logic to detect and report changed properties automatically based on state comparisons.

Note: Inputs can be piped to the binary, and detailed debug logs are toggled with environment variables like DSC_TRACE_LEVEL=debug.

Step 5: Generate the Manifest

The manifest JSON file is essential for DSC engine discovery and integration. Your binary can generate it on demand:

.\dscfile.exe manifest --out-dir .
# Creates go.resource.file.dsc.resource.json in the directory

You add the binary's directory to the DSC engine resource path environment variable:

$env:DSC_RESOURCE_PATH = (Get-Location).Path
dsc resource list Go.Resource/File

Now the DSC engine can consume your resource, running commands such as:

dsc resource test -r Go.Resource/File --input '{"path":"C:/temp/dsc.txt","content":"managed by dsc"}'
# "inDesiredState":false,"differingProperties":["_exist"]

dsc resource set -r Go.Resource/File --input '{"path":"C:/temp/dsc.txt","content":"managed by dsc"}'
# "changedProperties":["content","path"]

dsc resource test -r Go.Resource/File --input '{"path":"C:/temp/dsc.txt","content":"managed by dsc"}'
# "inDesiredState":true,"differingProperties":[]

The engine uses your resource’s manifest to invoke methods properly, falling back to synthetic testing where no explicit Test method exists.

Quick Tips & Tricks

  1. Leverage Struct Tags for Schema Control
    Use omitempty on JSON tags to mark fields optional in the schema; omit it to mark required fields. Add description tags to improve schema documentation automatically.

  2. Return NotFound Correctly in Get
    When representing an absent resource instance (like a missing file), return dsc.NotFound(...) instead of an error. This distinction is vital for idempotency and correct engine behavior.

  3. Idempotency in Delete
    Always ensure your Delete method succeeds even if the resource does not exist by checking for fs.ErrNotExist. This avoids failing repeated cleanup runs.

  4. Use the SetReturnStateAndDiff Option
    Trust the RDK to internally call Get before and after Set to generate changed property lists automatically, reducing your code complexity and improving reliability.

  5. Debug Without Installing DSC Engine
    Since the resource binary speaks the protocol, develop and test entirely standalone by invoking it directly with JSON inputs.

  6. Regenerate Manifest on Every Build
    Include manifest generation as a build step to avoid drift and errors loading your resource in DSC.

Conclusion

Writing a Microsoft DSC resource from scratch has traditionally involved juggling low-level protocol details that distract from the actual configuration logic. With the Go dsc-go-rdk, the barrier to entry drops dramatically — you model your resource state as a simple struct, implement a few typed methods, and lean on the RDK for all the protocol chores. The entire resource fits neatly in well under 100 lines, yet integrates fully with the DSC ecosystem.

This approach accelerates creation and maintenance of robust, maintainable DSC resources, making infrastructure-as-code more approachable for Go developers. As DSC continues to evolve alongside modern DevOps practices, toolkits like this promise to keep custom resource development both efficient and coherent across languages.

Whether you extend this simple file resource or build complex DSC modules, understanding the underlying protocol combined with this RDK pattern will save time and reduce bugs. The DSC landscape is moving towards convention and automation — and now is a great time to bring Go into the fold.

Minimal Go DSC Resource Demo
Figure: Example of driving the dscfile.exe resource CLI with JSON inputs and outputs — source: idontlikeai.dev

References

  1. How to write a Microsoft DSC resource in Go in under 100 lines - idontlikeai.dev — Original article detailing the Go RDK and resource example
  2. DSC-Go-RDK GitHub Repository — The Go Resource Development Kit implemented for DSC
  3. Microsoft DSC GitHub — Core DSC engine and protocol documentation
  4. OpenDSC for .NET — Similar RDK concept implemented for .NET developers
  5. DscResource.Base PowerShell Variant — Exploration of resource development in PowerShell
  6. Go 1.26 Release Notes — Required minimum Go version for the RDK

Schema and Manifest Generation
Figure: Automatic JSON Schema and manifest emission by dscfile.exe binary — source: idontlikeai.dev