Go developers appreciate Pulumi for its type safety, speed, and the ability to use the same language for both application and infrastructure code. This guide shows you how to manage DanubeData infrastructure with idiomatic Go.
Why Pulumi with Go?
Go is an excellent choice for infrastructure as code:
- Compiled language catches errors before deployment
- Strong static typing prevents configuration mistakes
- Fast compilation and execution
- Excellent concurrency for parallel resource operations
- Single binary deployment—no runtime dependencies
Getting Started
Install Pulumi CLI
# macOS
brew install pulumi
# Linux
curl -fsSL https://get.pulumi.com | sh
# Windows
choco install pulumi
Create a New Project
# Create a new directory
mkdir my-infrastructure && cd my-infrastructure
# Initialize a new Go project
pulumi new go
# Get the DanubeData provider
go get github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata
Configure Authentication
# Set your DanubeData API token
pulumi config set danubedata:apiToken your-api-token --secret
Project Structure
A well-organized Pulumi Go project:
my-infrastructure/
├── Pulumi.yaml # Project configuration
├── Pulumi.dev.yaml # Development stack config
├── Pulumi.prod.yaml # Production stack config
├── main.go # Main entry point
├── go.mod # Go module file
├── go.sum # Go dependencies checksum
└── pkg/
├── vps/ # VPS resources
│ └── vps.go
├── database/ # Database resources
│ └── database.go
├── cache/ # Cache resources
│ └── cache.go
└── components/ # Reusable components
└── webapp.go
Creating Your First VPS
// main.go
package main
import (
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create an SSH key for authentication
sshKey, err := danubedata.NewSshKey(ctx, "deploy-key", &danubedata.SshKeyArgs{
Name: pulumi.String("deploy-key"),
PublicKey: pulumi.String("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample... deploy@example.com"),
})
if err != nil {
return err
}
// Create a VPS instance
webServer, err := danubedata.NewVps(ctx, "web-server", &danubedata.VpsArgs{
Name: pulumi.String("web-server"),
Image: pulumi.String("ubuntu-24.04"),
Datacenter: pulumi.String("fsn1"),
ResourceProfile: pulumi.String("small_shared"),
AuthMethod: pulumi.String("ssh_key"),
SshKeyId: sshKey.ID(),
EnableIpv6: pulumi.Bool(true),
EnableBackups: pulumi.Bool(true),
})
if err != nil {
return err
}
// Export the VPS public IP
ctx.Export("webServerIp", webServer.PublicIp)
ctx.Export("webServerIpv6", webServer.Ipv6Address)
return nil
})
}
Complete Web Application Stack
Here's a complete example using Go best practices:
// main.go
package main
import (
"fmt"
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
// ResourceProfiles defines resource profiles for different environments
type ResourceProfiles struct {
VPS string
Database string
Cache string
}
func getProfiles(env string) ResourceProfiles {
profiles := map[string]ResourceProfiles{
"dev": {VPS: "nano_shared", Database: "small", Cache: "micro"},
"staging": {VPS: "small_shared", Database: "small", Cache: "small"},
"prod": {VPS: "medium_dedicated", Database: "medium", Cache: "medium"},
}
if p, ok := profiles[env]; ok {
return p
}
return profiles["dev"]
}
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
cfg := config.New(ctx, "")
appName := cfg.Require("appName")
adminIP := cfg.Require("adminIp")
environment := ctx.Stack()
profiles := getProfiles(environment)
// SSH Key
sshKey, err := danubedata.NewSshKey(ctx, "deploy-key", &danubedata.SshKeyArgs{
Name: pulumi.Sprintf("%s-%s-deploy", appName, environment),
PublicKey: cfg.RequireSecret("sshPublicKey"),
})
if err != nil {
return err
}
// PostgreSQL Database
database, err := danubedata.NewDatabase(ctx, "app-db", &danubedata.DatabaseArgs{
Name: pulumi.Sprintf("%s-%s-db", appName, environment),
Engine: pulumi.String("postgresql"),
EngineVersion: pulumi.String("16"),
ResourceProfile: pulumi.String(profiles.Database),
DatabaseName: pulumi.String(appName),
Datacenter: pulumi.String("fsn1"),
EnableSsl: pulumi.Bool(true),
})
if err != nil {
return err
}
// Redis Cache
cache, err := danubedata.NewCache(ctx, "app-cache", &danubedata.CacheArgs{
Name: pulumi.Sprintf("%s-%s-cache", appName, environment),
CacheProvider: pulumi.String("redis"),
ResourceProfile: pulumi.String(profiles.Cache),
Datacenter: pulumi.String("fsn1"),
})
if err != nil {
return err
}
// Storage Bucket
bucket, err := danubedata.NewStorageBucket(ctx, "app-storage", &danubedata.StorageBucketArgs{
Name: pulumi.Sprintf("%s-%s-assets", appName, environment),
Region: pulumi.String("fsn1"),
VersioningEnabled: pulumi.Bool(true),
})
if err != nil {
return err
}
storageKeys, err := danubedata.NewStorageAccessKey(ctx, "app-storage-key", &danubedata.StorageAccessKeyArgs{
Name: pulumi.Sprintf("%s-%s-key", appName, environment),
})
if err != nil {
return err
}
// Application VPS
appServer, err := danubedata.NewVps(ctx, "app-server", &danubedata.VpsArgs{
Name: pulumi.Sprintf("%s-%s-app", appName, environment),
Image: pulumi.String("ubuntu-24.04"),
Datacenter: pulumi.String("fsn1"),
ResourceProfile: pulumi.String(profiles.VPS),
AuthMethod: pulumi.String("ssh_key"),
SshKeyId: sshKey.ID(),
EnableIpv6: pulumi.Bool(true),
EnableBackups: pulumi.Bool(environment == "prod"),
})
if err != nil {
return err
}
// Firewall Rules
_, err = danubedata.NewFirewall(ctx, "web-firewall", &danubedata.FirewallArgs{
Name: pulumi.Sprintf("%s-%s-web-fw", appName, environment),
Rules: danubedata.FirewallRuleArray{
&danubedata.FirewallRuleArgs{
Direction: pulumi.String("inbound"),
Protocol: pulumi.String("tcp"),
Ports: pulumi.String("80"),
SourceIps: pulumi.StringArray{pulumi.String("0.0.0.0/0"), pulumi.String("::/0")},
Description: pulumi.String("Allow HTTP"),
},
&danubedata.FirewallRuleArgs{
Direction: pulumi.String("inbound"),
Protocol: pulumi.String("tcp"),
Ports: pulumi.String("443"),
SourceIps: pulumi.StringArray{pulumi.String("0.0.0.0/0"), pulumi.String("::/0")},
Description: pulumi.String("Allow HTTPS"),
},
&danubedata.FirewallRuleArgs{
Direction: pulumi.String("inbound"),
Protocol: pulumi.String("tcp"),
Ports: pulumi.String("22"),
SourceIps: pulumi.StringArray{pulumi.String(adminIP)},
Description: pulumi.String("SSH from admin only"),
},
},
})
if err != nil {
return err
}
// Database Firewall
_, err = danubedata.NewFirewall(ctx, "db-firewall", &danubedata.FirewallArgs{
Name: pulumi.Sprintf("%s-%s-db-fw", appName, environment),
Rules: danubedata.FirewallRuleArray{
&danubedata.FirewallRuleArgs{
Direction: pulumi.String("inbound"),
Protocol: pulumi.String("tcp"),
Ports: pulumi.String("5432"),
SourceIps: pulumi.StringArray{
appServer.PublicIp.ApplyT(func(ip string) string {
return fmt.Sprintf("%s/32", ip)
}).(pulumi.StringOutput),
},
Description: pulumi.String("PostgreSQL from app server"),
},
},
})
if err != nil {
return err
}
// Exports
ctx.Export("appServerIp", appServer.PublicIp)
ctx.Export("appServerIpv6", appServer.Ipv6Address)
ctx.Export("databaseEndpoint", database.Endpoint)
ctx.Export("databaseName", database.DatabaseName)
ctx.Export("cacheEndpoint", cache.Endpoint)
ctx.Export("storageEndpoint", bucket.EndpointUrl)
ctx.Export("storageAccessKey", storageKeys.AccessKey)
ctx.Export("storageSecretKey", storageKeys.SecretKey)
return nil
})
}
Using Component Resources
Create reusable Go structs for complex infrastructure:
// pkg/components/webapp.go
package components
import (
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
// WebAppArgs defines arguments for creating a WebApp component
type WebAppArgs struct {
Name string
Environment string
SSHPublicKey pulumi.StringInput
VPSProfile string
DBProfile string
CacheProfile string
EnableBackups bool
}
// WebApp is a reusable component for a complete web application stack
type WebApp struct {
pulumi.ResourceState
VPS *danubedata.Vps
Database *danubedata.Database
Cache *danubedata.Cache
Bucket *danubedata.StorageBucket
}
// NewWebApp creates a new WebApp component
func NewWebApp(ctx *pulumi.Context, name string, args *WebAppArgs,
opts ...pulumi.ResourceOption) (*WebApp, error) {
component := &WebApp{}
err := ctx.RegisterComponentResource("custom:resource:WebApp", name, component, opts...)
if err != nil {
return nil, err
}
childOpts := append(opts, pulumi.Parent(component))
// Set defaults
if args.VPSProfile == "" {
args.VPSProfile = "small_shared"
}
if args.DBProfile == "" {
args.DBProfile = "small"
}
if args.CacheProfile == "" {
args.CacheProfile = "micro"
}
// SSH Key
sshKey, err := danubedata.NewSshKey(ctx, name+"-ssh", &danubedata.SshKeyArgs{
Name: pulumi.Sprintf("%s-%s", args.Name, args.Environment),
PublicKey: args.SSHPublicKey,
}, childOpts...)
if err != nil {
return nil, err
}
// Database
component.Database, err = danubedata.NewDatabase(ctx, name+"-db", &danubedata.DatabaseArgs{
Name: pulumi.Sprintf("%s-%s-db", args.Name, args.Environment),
Engine: pulumi.String("postgresql"),
EngineVersion: pulumi.String("16"),
ResourceProfile: pulumi.String(args.DBProfile),
DatabaseName: pulumi.String(args.Name),
Datacenter: pulumi.String("fsn1"),
}, childOpts...)
if err != nil {
return nil, err
}
// Cache
component.Cache, err = danubedata.NewCache(ctx, name+"-cache", &danubedata.CacheArgs{
Name: pulumi.Sprintf("%s-%s-cache", args.Name, args.Environment),
CacheProvider: pulumi.String("redis"),
ResourceProfile: pulumi.String(args.CacheProfile),
Datacenter: pulumi.String("fsn1"),
}, childOpts...)
if err != nil {
return nil, err
}
// Storage
component.Bucket, err = danubedata.NewStorageBucket(ctx, name+"-storage", &danubedata.StorageBucketArgs{
Name: pulumi.Sprintf("%s-%s-assets", args.Name, args.Environment),
Region: pulumi.String("fsn1"),
}, childOpts...)
if err != nil {
return nil, err
}
// VPS
component.VPS, err = danubedata.NewVps(ctx, name+"-vps", &danubedata.VpsArgs{
Name: pulumi.Sprintf("%s-%s", args.Name, args.Environment),
Image: pulumi.String("ubuntu-24.04"),
Datacenter: pulumi.String("fsn1"),
ResourceProfile: pulumi.String(args.VPSProfile),
AuthMethod: pulumi.String("ssh_key"),
SshKeyId: sshKey.ID(),
EnableBackups: pulumi.Bool(args.EnableBackups),
}, childOpts...)
if err != nil {
return nil, err
}
ctx.RegisterResourceOutputs(component, pulumi.Map{
"vpsIp": component.VPS.PublicIp,
"dbEndpoint": component.Database.Endpoint,
"cacheEndpoint": component.Cache.Endpoint,
})
return component, nil
}
// main.go
package main
import (
"my-infrastructure/pkg/components"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
app, err := components.NewWebApp(ctx, "myapp", &components.WebAppArgs{
Name: "myapp",
Environment: "prod",
SSHPublicKey: pulumi.String("ssh-ed25519 AAAA..."),
VPSProfile: "medium_shared",
DBProfile: "medium",
EnableBackups: true,
})
if err != nil {
return err
}
ctx.Export("appIp", app.VPS.PublicIp)
ctx.Export("dbEndpoint", app.Database.Endpoint)
return nil
})
}
Creating Multiple Resources with Loops
// main.go
package main
import (
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type ServerConfig struct {
Name string
Profile string
}
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
servers := []ServerConfig{
{Name: "web-1", Profile: "small_shared"},
{Name: "web-2", Profile: "small_shared"},
{Name: "api-1", Profile: "medium_shared"},
{Name: "worker-1", Profile: "medium_dedicated"},
}
// Create SSH key once
sshKey, err := danubedata.NewSshKey(ctx, "shared-key", &danubedata.SshKeyArgs{
Name: pulumi.String("shared-deploy-key"),
PublicKey: pulumi.String("ssh-ed25519 AAAA..."),
})
if err != nil {
return err
}
// Create all servers
for _, server := range servers {
vps, err := danubedata.NewVps(ctx, server.Name, &danubedata.VpsArgs{
Name: pulumi.String(server.Name),
Image: pulumi.String("ubuntu-24.04"),
Datacenter: pulumi.String("fsn1"),
ResourceProfile: pulumi.String(server.Profile),
AuthMethod: pulumi.String("ssh_key"),
SshKeyId: sshKey.ID(),
})
if err != nil {
return err
}
ctx.Export(server.Name+"Ip", vps.PublicIp)
}
return nil
})
}
Serverless Containers
// serverless.go
package main
import (
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func deployServerless(ctx *pulumi.Context) error {
// Deploy from Docker image
apiService, err := danubedata.NewServerless(ctx, "api", &danubedata.ServerlessArgs{
Name: pulumi.String("api-service"),
DeploymentType: pulumi.String("image"),
ImageUrl: pulumi.String("ghcr.io/myorg/api:latest"),
Port: pulumi.Int(8080),
MinInstances: pulumi.Int(0), // Scale to zero
MaxInstances: pulumi.Int(10),
Memory: pulumi.Int(512),
Cpu: pulumi.Float64(0.5),
EnvVars: pulumi.StringMap{
"NODE_ENV": pulumi.String("production"),
},
})
if err != nil {
return err
}
// Deploy from Git repository
webApp, err := danubedata.NewServerless(ctx, "web", &danubedata.ServerlessArgs{
Name: pulumi.String("web-app"),
DeploymentType: pulumi.String("git"),
GitUrl: pulumi.String("https://github.com/myorg/web-app.git"),
GitBranch: pulumi.String("main"),
Port: pulumi.Int(3000),
MinInstances: pulumi.Int(1),
MaxInstances: pulumi.Int(5),
BuildCommand: pulumi.String("npm run build"),
StartCommand: pulumi.String("npm start"),
})
if err != nil {
return err
}
ctx.Export("apiUrl", apiService.Url)
ctx.Export("webUrl", webApp.Url)
return nil
}
Using Data Sources
// data_sources.go
package main
import (
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func queryDataSources(ctx *pulumi.Context) error {
// Get available VPS images
images, err := danubedata.GetVpsImages(ctx, nil)
if err != nil {
return err
}
for _, image := range images.Images {
ctx.Log.Info(image.Name, nil)
}
// Get existing SSH keys
keys, err := danubedata.GetSshKeys(ctx, nil)
if err != nil {
return err
}
_ = keys
// Get existing VPS instances
vpsList, err := danubedata.GetVpss(ctx, nil)
if err != nil {
return err
}
for _, vps := range vpsList.Instances {
ctx.Log.Info(vps.Name+" - "+vps.PublicIp, nil)
}
return nil
}
Error Handling Patterns
// error_handling.go
package main
import (
"fmt"
"github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
// CreateInfrastructureError wraps infrastructure creation errors
type CreateInfrastructureError struct {
Resource string
Err error
}
func (e *CreateInfrastructureError) Error() string {
return fmt.Sprintf("failed to create %s: %v", e.Resource, e.Err)
}
func createWithErrorHandling(ctx *pulumi.Context) error {
sshKey, err := danubedata.NewSshKey(ctx, "key", &danubedata.SshKeyArgs{
Name: pulumi.String("key"),
PublicKey: pulumi.String("ssh-ed25519 AAAA..."),
})
if err != nil {
return &CreateInfrastructureError{Resource: "SSH Key", Err: err}
}
_, err = danubedata.NewVps(ctx, "server", &danubedata.VpsArgs{
Name: pulumi.String("server"),
Image: pulumi.String("ubuntu-24.04"),
Datacenter: pulumi.String("fsn1"),
ResourceProfile: pulumi.String("small_shared"),
AuthMethod: pulumi.String("ssh_key"),
SshKeyId: sshKey.ID(),
})
if err != nil {
return &CreateInfrastructureError{Resource: "VPS", Err: err}
}
return nil
}
CI/CD with GitHub Actions
# .github/workflows/pulumi.yml
name: Pulumi Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
DANUBEDATA_API_TOKEN: ${{ secrets.DANUBEDATA_API_TOKEN }}
jobs:
preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go mod download
- uses: pulumi/actions@v5
with:
command: preview
stack-name: dev
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go mod download
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
Testing Infrastructure Code
// main_test.go
package main
import (
"sync"
"testing"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/stretchr/testify/assert"
)
type mocks int
func (mocks) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
return args.Name + "_id", args.Inputs, nil
}
func (mocks) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
return args.Args, nil
}
func TestInfrastructure(t *testing.T) {
err := pulumi.RunErr(func(ctx *pulumi.Context) error {
// Create your infrastructure here
// ...
return nil
}, pulumi.WithMocks("project", "stack", mocks(0)))
assert.NoError(t, err)
}
Useful Commands
# Build the project
go build
# Preview changes
pulumi preview
# Apply changes
pulumi up
# View outputs
pulumi stack output
# Destroy resources
pulumi destroy
# Import existing resource
pulumi import danubedata:index:Vps my-vps existing-vps-id
Go Module File
// go.mod
module my-infrastructure
go 1.22
require (
github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata v1.0.0
github.com/pulumi/pulumi/sdk/v3 v3.100.0
)
Cost Estimation
| Resource | Profile | Monthly Cost |
|---|---|---|
| VPS (small_shared) | 2 vCPU, 4GB RAM | 8.99 |
| PostgreSQL (small) | 1 vCPU, 2GB RAM | 19.99 |
| Redis (micro) | 256MB RAM | 4.99 |
| Storage Bucket | 1TB included | 3.99 |
| Total | 37.96/month |
Get Started
Ready to manage your infrastructure with Go?
Create your free DanubeData account
Then install the Pulumi provider:
go get github.com/AdrianSilaghi/pulumi-danubedata/sdk/go/danubedata
Full documentation and examples are available in our GitHub repository.