pulumi-network-services

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: Apache-2.0, MIT

README

pulumi-network-services

Reusable Pulumi components for running Holochain network services on DigitalOcean, written in Go.

These are the components Holochain uses to run its own infrastructure. DigitalOcean is a hard dependency: if you need another provider, the source here is a reasonable starting point to adapt.

Install

go get github.com/holochain/pulumi-network-services

Components

bootstraprelay

A Kitsune2 bootstrap/relay server. From Kitsune2 0.5.0, peer discovery (bootstrap) and connection establishment (relay) are a single service, so one component covers both roles.

import (
    "github.com/holochain/pulumi-network-services/bootstraprelay"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

relay, err := bootstraprelay.New(ctx, "bootstrap-relay", &bootstraprelay.Args{
    Hostname:     pulumi.String("relay.example.org"),
    ContactEmail: pulumi.String("ops@example.org"),
    // A literal, so a release of this library cannot move it. See Versioning.
    ContainerImage: pulumi.String("ghcr.io/holochain/kitsune2_bootstrap_srv:v0.5.0"),
})

The component creates a droplet and nothing else. It exports Ipv4Address, Ipv6Address, Hostname and Url.

For a real deployment using this library — including DNS wiring — see holochain/network-services.

You provide the DNS

Pass the hostname you intend to use and create the A and AAAA records yourself, with whatever DNS provider you use.

Certbot on the host retries with increasing backoff, but not indefinitely: 30 attempts over roughly 2.5 hours, after which the provisioning script exits non-zero and the service does not start. Records created in the same pulumi up are fine so long as they become resolvable inside that window.

Ports

The container runs with host networking, so the server binds the droplet's interfaces directly and there is no port mapping to get wrong:

Port Protocol Purpose
443 TCP Bootstrap and relay over HTTPS
7842 UDP QUIC Address Discovery (QAD)

--production enables the QAD listener, which is how peers learn their own public address in order to attempt a direct connection. Losing it does not take the service down — peers fall back to relaying — so it tends to fail quietly as degraded connectivity rather than an outage.

This component creates no firewall, and DigitalOcean droplets have none by default, so both ports are reachable as deployed. If you put a digitalocean.Firewall in front of it, note that 7842 is UDP: an inbound rule that only covers TCP will let the service look healthy while silently forcing every peer onto the relay.

You provide the SSH keys

SshKeys is empty by default, which means nobody can SSH into the droplet. Pass the DigitalOcean fingerprints you want authorised:

keys, err := digitalocean.GetSshKeys(ctx, &digitalocean.GetSshKeysArgs{}, nil)

Filter that result rather than passing all of it — every fingerprint you pass gets root.

Updates replace the droplet

DigitalOcean user data is immutable, so any change that affects cloud-init — including the container image — replaces the droplet.

That means a new IP address, so keep DNS TTLs low; records built from this component's outputs update automatically. It also means a fresh Let's Encrypt issuance, and Let's Encrypt permits five duplicate certificates per week for a given hostname. Iterating repeatedly against one hostname will lock you out.

Resource options do not all reach the droplet

pulumi.IgnoreChanges passed to New applies to the component, and Pulumi does not forward it to a component's children — so it has no effect on the droplet.

This matters if you feed SshKeys from digitalocean.GetSshKeys, which returns every key on the account: ssh_keys forces replacement on DigitalOcean, so anyone adding a key to your account replaces the droplet on your next pulumi up.

Prefer passing an explicit, stable list of fingerprints — then the input only changes when you change it. If you need to ignore the field instead, use a transformation, which does reach children:

relay, err := bootstraprelay.New(ctx, "bootstrap-relay", args,
    pulumi.Transformations([]pulumi.ResourceTransformation{
        func(a *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
            if a.Type != "digitalocean:index/droplet:Droplet" {
                return nil
            }
            return &pulumi.ResourceTransformationResult{
                Props: a.Props,
                Opts:  append(a.Opts, pulumi.IgnoreChanges([]string{"sshKeys"})),
            }
        },
    }))

Versioning

The Go API is stable and easy to keep stable. The version of the service you deploy is the thing that actually matters, so it gets an explicit contract:

Bump Means
MAJOR A Go API break, or a change that replaces your resources even if you pinned every argument
MINOR New components, new optional arguments, or a DefaultContainerImage bump
PATCH Fixes that change nothing you have deployed

The line between MAJOR and MINOR is whether pinning saves you.

Pin ContainerImage to a literal in production. If you leave it unset you get DefaultContainerImage, and a MINOR release can move that — which replaces your droplet. Pinning to bootstraprelay.DefaultContainerImage is not pinning: that constant moves with the library. Write the image reference out in full.

Releases that move DefaultContainerImage say so in their release notes, because that is the change that replaces droplets for consumers who have not pinned.

Whatever you pin, pulumi preview shows the replacement before anything applies.

Releasing

Two stages, matching the rest of the organisation. CHANGELOG.md is generated by git-cliff from commit messages, using the shared pre-1.0-cliff.toml, so write Conventional Commits.

  1. Run the Prepare a release workflow. It computes the next version from the commit history, writes CHANGELOG.md, and opens a pull request labelled hra-release. Pass force_version to override the computed bump.
  2. Merge that pull request. Publish release then tags the merge commit and creates the GitHub release.

The tag is created last, and that ordering is not cosmetic. A Go module has no manifest version — the version is the tag — and once proxy.golang.org has served a tag, that version can never be changed. Tagging before the changelog merged would publish a version that does not contain its own release notes, with no way to fix it short of burning the next patch number.

This is why the shared holochain/actions release workflows are not used here: they bump a version in Cargo.toml and publish to crates.io, neither of which has a Go counterpart. Nothing needs publishing to a registry — the proxy picks up the tag.

Releasing v2.0.0 or later

Go encodes the major version in the import path from v2 onwards, so a tag alone is not enough and the release workflows do not handle this. Before preparing the first v2 release:

  1. Change the module line in go.mod to github.com/holochain/pulumi-network-services/v2.
  2. Update imports within this repository to the /v2 path.
  3. Tell consumers to update their import paths — for Go this is a new module, so go get will not offer it as an upgrade of the old one.

Skipping this produces a v2.0.0 tag that the module proxy will not serve as .../v2, and consumers stay silently pinned to the newest v1.

Contributing

nix develop provides Go and cloud-init.

go test ./...

To lint the cloud-init the component renders, the way CI does:

CLOUD_INIT_DUMP_DIR=$(mktemp -d) go test ./bootstraprelay/ -run TestDumpRenderedCloudInit -v
cloud-init schema -c "$CLOUD_INIT_DUMP_DIR"/minimal.cloud-init.yaml

To develop against a consuming Pulumi program without publishing, use a Go workspace from the consumer's directory:

go work init .
go work use ../pulumi-network-services

Keep go.work out of version control. Never commit a replace directive in a consumer — that is how broken tags get published.

License

Licensed under either of

at your option. In SPDX terms: Apache-2.0 OR MIT.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Directories

Path Synopsis
Package bootstraprelay provisions a Kitsune2 bootstrap/relay server on DigitalOcean.
Package bootstraprelay provisions a Kitsune2 bootstrap/relay server on DigitalOcean.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL