Skip to content
Templates & Vars

Args and Vars Merge Strategy

Args and Vars can be set at multiple levels and Nstance Server uses the same merge stragegy for both.

Merges work by replacing the higher priority layer over the lower priorities layer, except where both values are objects and then it merges the object fields and recurses down into each value of that object.

For example with Args:

defaults.args:

{
  "a": 1,
  "b": { "hello": "world", "always": "here" },
  "c": { "goodbye": "world" }
}

templates.example.args:

{
  "a": "2",
  "b": { "hello": "there", "location": "world" },
  "c": 3
}

merged output:

{
  "a": "2",
  "b": { "hello": "there", "always": "here", "location": "world" },
  "c": 3
}

This is a straightforward recursive Go function:

func deepMerge(dst, src map[string]any) {
	for k, v := range src {
		if vMap, ok := v.(map[string]any); ok {
			if dMap, ok := dst[k].(map[string]any); ok {
				deepMerge(dMap, vMap)
				continue
			}
		}
		dst[k] = v
	}
}

Image Resolution

Nstance supports automatic resolution of cloud provider images (e.g., latest Debian AMI) based on predefined criteria, eliminating the need to hardcode region-specific image IDs.

  • Provider-specific filters to lookup latest AMIs/images
  • Cached in SQLite, refreshed periodically (default 6 hours)
  • Optional fallback image IDs if lookup fails

Provider Support

Currently this feature is only useful for AWS.

Different cloud providers handle image references differently:

  • AWS (EC2): Uses region-specific AMI IDs that must be looked up. Nstance automatically queries EC2 DescribeImages API with filters and caching.
  • GCP (Compute Engine): Uses image families (e.g., projects/debian-cloud/global/images/family/debian-13) that automatically resolve to latest. No lookup needed.
  • Oracle Cloud (OCI): Would benefit from lookup mechanism (similar to AWS), and may be added in the future.

Configuration

Images are configured in the images block with provider-specific filters:

{
  "images": {
    "debian_13_arm64": {
      "provider": "aws",
      "filters": [
        {"name": "name", "values": ["debian-13-arm64-*"]},
        {"name": "owner-id", "values": ["136693071363"]}  // Debian
      ],
      "sort": "creation-date",
      "order": "desc",
      "fallback": "ami-026fccd88446aa0bf"  // Optional fallback if lookup fails
    }
  }
}

Note that image name/map key should be a valid Go template identifiers (alphanumeric + underscores, starting with letter/underscore).

Configuration Fields:

  • provider: Cloud provider (aws) - must match server provider
  • filters: Array of provider-specific filters (e.g., AWS filter name/values)
  • sort: Field to sort results by (creation-date, name)
  • order: Sort order (asc, desc)
  • fallback: Optional fallback image ID if lookup fails

Usage in Templates

Resolved images are available in userdata and args templates via .Image map:

{
  "templates": {
    "knc": {
      "args": {
        "ImageId": "{{ .Image.debian_13_arm64 }}"  // Resolved AMI ID
      }
    }
  }
}

Alternative syntax using index helper for kebab-case names:

{{ index .Image "debian-13-arm64" }}

Resolution Process

  1. On Shard Leader Election: Service starts and performs initial image resolution
  2. Lookup: Queries provider API (e.g., EC2 DescribeImages) with configured filters
  3. Sort & Select: Sorts results and selects first match (e.g., latest by creation date)
  4. Cache: Stores resolved images in SQLite (images table)
  5. Periodic Refresh: Re-resolves at configured interval (default 6 hours)

Error Handling & Fallback

If image resolution fails:

  1. Try Cache: Load previously resolved value from SQLite
  2. Use Fallback: Use configured fallback image ID if specified
  3. Fail Provisioning: If no cache and no fallback, instance provisioning fails with error log

Cache persists across server restarts and does not expire (persists until next successful lookup).

Args

Args are passed to the provider VM creation SDK function call. Args are provider-specific and support different fields for AWS (RunInstances) and GCP (Instance) configurations. For example on AWS, Args effectively becomes AWS RunInstances input.

Args are processed as a text/template after the merge strategy has applied, recursively interpolating string values in nested objects and arrays. Note that templating can only be used inside JSON string values, for example:

{
    "someKey": "{{ .Var.SOME_VALUE }}"
}

AWS Args

AWS Args map directly to RunInstances input fields. You cannot set InstanceType, SubnetId, UserData, MinCount, or MaxCount (these are managed by Nstance).

GCP Args

GCP Args are applied to the computepb.Instance resource. The following fields are supported:

ArgTypeDescription
SourceImagestringBoot disk image (e.g., "projects/debian-cloud/global/images/family/debian-13")
ServiceAccountstringService account email. Sets cloud-platform scope automatically.
ServiceAccountsarrayFull service account config (alternative to ServiceAccount for custom scopes)
LabelsobjectAdditional GCP labels merged with Nstance-managed labels
NetworkTagsarrayGCP network tags for firewall rule targeting (e.g., ["nstance-agent-us-central1-a"])
PreemptibleboolUse preemptible VM scheduling
ProvisioningModelstringVM provisioning model (e.g., "SPOT")
OnHostMaintenancestringHost maintenance behavior (e.g., "TERMINATE")
AutomaticRestartboolAutomatically restart on failure
MinCpuPlatformstringMinimum CPU platform (e.g., "Intel Cascade Lake")
GuestAcceleratorsarrayGPU accelerator configs
ShieldedInstanceConfigobjectShielded VM settings
ConfidentialInstanceConfigobjectConfidential VM settings
ResourcePoliciesarrayResource policy URLs

Example:

{
  "defaults": {
    "args": {
      "SourceImage": "projects/debian-cloud/global/images/family/debian-13",
      "ServiceAccount": "[email protected]"
    }
  },
  "groups": {
    "default": {
      "workers": {
        "template": "default",
        "size": 3,
        "args": {
          "NetworkTags": ["nstance-agent-us-central1-a"],
          "Labels": {
            "nstance-group": "workers"
          }
        }
      }
    }
  }
}

Note: Nstance automatically manages core GCP labels (nstance-managed, nstance-instance-id, nstance-cluster-id, nstance-shard, nstance-group, nstance-template, nstance-instance-kind) and uses these labels (not network tags) for instance identification and filtering.

Vars

Vars are set in a hierarchy, with order of precedence:

  • (MachinePool): Group -> Template -> Global
  • (Machine): Group -> Template -> Global

For example:

{
  "vars": {
    "EXAMPLE": "one",
  },
  "templates": {
    "default": {
      "vars": {
        "EXAMPLE": "two",
      },
    },
  },
  "groups": {
    "blue": {},
    "green": {
      "template": "default",
    },
    "red": {
      "template": "default",
      "vars": {
        "EXAMPLE": "three",
      },
    },
    "black": {
      "vars": {
        "EXAMPLE": "four",
      },
    },
  },
}

Given the above example config, the .Var.EXAMPLE value in userdata:

  • for group blue is one.
  • for group green is two.
  • for group red is three.
  • for group black is four.

Userdata Templates

The defaults and templates block can specify a userdata configuration object:

{
  "templates": {
    "default": {
      "userdata": {
        "source": "inline",       // "inline" (default) or "url"
        "encoding": "plain",      // "plain" (default), "base64", "gzip", "base64+gzip"
        "content": "#!/bin/bash\necho 'Hello {{ .Instance.ID }}'" // or the URL
      }
    }
  }
}

Fields:

  • source: Where the content comes from

    • "inline" (default): The content field contains the userdata directly
    • "url": The content field contains a URL to fetch the userdata from
  • encoding: How the content is encoded

    • "plain" (default): Plain text with escaped newlines
    • "base64": Base64 encoded content
    • "gzip": Gzip compressed content (only valid with source: "url")
    • "base64+gzip": Base64 encoded, gzip compressed content
  • content: The userdata content or URL (required)

The default userdata template is only used if no userdata template is specified for the template.

The userdata template is processed as a Go text/template, where the template can reference:

  • Merged Vars e.g. {{ .Vars.MY_VAR }} (see Args & Vars Merge Strategy)

  • Server information such as:

    • .Server.Shard e.g. us-west-2a-1
    • .Server.RegistrationAddr i.e. the IP and port for agent registration (e.g. 10.0.0.1:8992)
    • .Server.AgentAddr i.e. the IP and port for agent gRPC service (e.g. 10.0.0.1:8994)
  • Provider information such as:

    • .Provider.Kind e.g. aws
    • .Provider.Region e.g. us-west-2
    • .Provider.Zone e.g. us-west-2a
  • Instance information (derived from Pool/MachinePool or Instance/Machine configuration, which can be derived from the instance template) such as:

    • .Instance.Arch e.g. arm64
    • .Instance.Type e.g. t4g.medium.

Templated Files

In addition to certificates and secrets, the Nstance Server can generate and send templated files to agents. These files are dynamically generated using Go text/template with access to instance, cluster, and configuration data.

Template Types

Templated files support three distinct template formats:

kind: env

For environment variable files (.env format), use kind: "env" with a key-value template object:

"instance.env": {
  "kind": "env",
  "template": {
    "INSTANCE_ID": "{{ .Instance.ID }}",
    "ENVIRONMENT": "{{ .Vars.Environment }}",
    "K8S_NODE_LABELS": "{{ .Vars.KUBELET_NODE_LABELS }}",
    "CLUSTER_FQDN": "{{ .Cluster.FQDN }}"
  }
}

This generates standard environment file format:

INSTANCE_ID=knc0000000001r010000000000000
ENVIRONMENT=production
K8S_NODE_LABELS=controlplane
CLUSTER_FQDN=example-cluster.cluster.cool

kind: json

For JSON configuration files, use kind: "json" with a template object where string values can contain templates:

"kubelet-config.json": {
  "kind": "json",
  "template": {
    "kind": "KubeletConfiguration",
    "apiVersion": "kubelet.config.k8s.io/v1beta1",
    "address": "{{ .Instance.IP4 }}",
    "port": 10250,
    "clusterDomain": "cluster.local",
    "nodeLabels": {
      "instance.example.com/id": "{{ .Instance.ID }}",
    }
  }
}

String values are processed as templates while preserving JSON structure, numbers, booleans, and arrays.

This generates a JSON document file format e.g.:

{
  "kind": "KubeletConfiguration",
  "apiVersion": "kubelet.config.k8s.io/v1beta1",
  "address": "172.18.0.1",
  "port": 10250,
  "clusterDomain": "cluster.local",
  "nodeLabels": {
    "instance.example.com/id": "knc0000000001r010000000000000",
  }
}

kind: string

For custom file formats or when you need full control over the output format:

"custom-script.sh": {
  "kind": "string",
  "template": "#!/bin/bash\necho \"Instance {{ .Instance.ID }} starting\"\nexport ENV={{ .Vars.Environment }}"
}

e.g.

#!/bin/bash
echo "Instance knc0000000001r010000000000000 starting"
export ENV=production

Template Data

All template types have access to the same template data:

  • Instance: {{ .Instance.ID }}, {{ .Instance.Type }}, {{ .Instance.Hostname }}, {{ .Instance.IP4 }}, {{ .Instance.IP6 }}
  • Server: {{ .Server.Shard }}, {{ .Server.RegistrationAddr }}, {{ .Server.AgentAddr }}
  • Provider: {{ .Provider.Kind }}, {{ .Provider.Region }}, {{ .Provider.Zone }}
  • Cluster: {{ .Cluster.Slug }}, {{ .Cluster.FQDN }}
  • Variables: {{ .Vars.ENVIRONMENT }} - All merged variables from global → template → group hierarchy
  • Images: {{ .Image.debian_13_arm64 }} - Resolved image IDs from Image Resolution configuration

Template data uses the same variable merging strategy as userdata templates.