Help us shape the future of Go! Take the 2026 Go Developer Survey

asg

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 20 Imported by: 0

README

asg

github.com/truvity/asg is a Pulumi Go library for running a service as an EC2 Auto Scaling group: a machine, or a few, built from user-data and replaced rather than repaired.

  • asg.New registers the group and its launch template, an optional warm pool of stopped instances, a launch lifecycle hook whose default result is ABANDON (an instance that never finished booting is replaced, not put in service half configured), an IAM role and instance profile with the least privilege the user-data needs, and a security group.
  • bootstrap composes the user-data from fragments: systemd units, files, the secrets an instance reads at boot (SSM parameters and S3 objects, by exact name) and the readiness step that completes the lifecycle hook. The IAM policy is derived from it, so the instance can read exactly what its user-data names and nothing else.

The rules the component enforces, whatever the caller passes:

  • IMDSv2 is always required;
  • a bare * action is refused;
  • no managed policy is attached by default;
  • a warm pool without a lifecycle hook is refused;
  • the image is pinned at deploy time, so a newer image is a visible diff.

Who it is for

Anyone running a service on EC2 the replace-don't-repair way with Pulumi in Go: a stateless box, or a small fleet, whose whole configuration is its user-data, behind a load balancer, a tunnel or nothing at all.

The model

One call, asg.New, registers an Auto Scaling group and everything it needs as children of one component. The user-data is a bootstrap.Plan: files, units and the secrets the instance reads at boot, by exact name. The IAM policy is derived from the plan, so an instance can read what its user-data names and nothing else. A launch lifecycle hook with a default of ABANDON means an instance that never finished booting is replaced, never put in service half configured.

Install and a worked example

go get github.com/truvity/asg
import (
    "github.com/truvity/asg"
    "github.com/truvity/asg/bootstrap"
)

Build a bootstrap.Plan, render it, and pass the result to asg.New with the group, instance, IAM and network arguments. The package documentation and statusbox_test.go show a complete box. Compat lets an existing group move onto this library without Pulumi replacing anything: it keeps the component type, the logical names and the layout the group was created with.

Consumers

The status page box in truvity/observability (pkg/statusbox/ec2).

Neighbours

truvity/aws-structure shapes the AWS organization this runs in; the code here used to live there as pkg/appliance and pkg/bootstrap.

Documentation

The package documentation, and the decisions under docs/decisions/.

The rule that makes this repository public

Mechanism only. Account ids, regions, names, images and policy statements are the caller's; nothing here defaults to one estate's value, and the tests use Pulumi mocks and never reach an AWS account. hack/leak-canary.sh enforces the mechanical half over tracked files and runs in the gate, with no exception.

Status

v0.x: the API may change in a minor release, and every change is in the CHANGELOG. v0.1.0 is the code that was pkg/appliance and pkg/bootstrap in truvity/aws-structure, moved without a behaviour change.

Development

devbox shell
just check      # build, test, lint, leak-canary

Releasing

Releases are a v* tag, which the shared release workflow turns into the Go module version and a GitHub Release. Automatic patch releases are not armed; releases are hand-cut tags.

Licence

MIT, see LICENSE.

Documentation

Overview

Package asg provisions an EC2 appliance: a machine, or a few, that is built from user-data, joins an Auto Scaling group and is replaced rather than repaired. It sits next to bootstrap, which composes the user-data; this package gives that user-data a home and the least privilege it needs.

One call, New, registers these children under one component (or, with Compat.Flat, directly under the caller's parent):

  • an Auto Scaling group of Min..Max and a launch template;
  • a warm pool of stopped instances (optional);
  • an EC2_INSTANCE_LAUNCHING lifecycle hook, on the group or as a separate resource, whose DefaultResult is ABANDON: bootstrap-ready.service sends CONTINUE on every boot, so an instance that never finished is replaced instead of put in service half configured;
  • an IAM role and instance profile whose policy is built from the user-data: ssm:GetParameter on exactly the plan's parameter names, s3:GetObject on exactly the plan's object keys and the two Auto Scaling actions the lifecycle scripts call, scoped to the group, plus the statements the caller adds;
  • a security group, its rules inline or separate;
  • the plan's S3 objects, uploaded (this is the only thing bootstrap leaves to the caller), and the CloudWatch alarms the caller asks for.

The image is found by name filter or by SSM parameter at deploy time and pinned in the launch template, so a newer image is a visible diff and a rolling refresh. IMDSv2 is always required; the hop limit is configurable (a container on the instance needs 2).

Migrating without a replace

An appliance that exists today is adopted in two steps, and both are possible with this package.

Step A. The caller passes the old user-data unchanged (bootstrap.Verbatim) and the options of Compat reproduce every logical name, parent, type and physical name; Args.IAM.Policy returns the old policy document. The preview is empty: no launch template version, no refresh.

Step B. The caller switches to a composed plan and drops the overrides. The launch template gets one new version, which an instance refresh rolls out, and the role's policy is tightened in place.

Extension points

A Module contributes bootstrap fragments, IAM statements, SSM parameters and security group rules in one piece (sshd user certificates, self-registering DNS, journald, swap are such modules). Collect merges modules into a Contribution that Args.Contribution takes and that Contribution.AddTo feeds to the plan. No module ships in this package.

Index

Constants

View Source
const (
	// TypeToken is the Pulumi type of the component unless Compat.ComponentType
	// overrides it.
	TypeToken = "truvity:aws-structure:Appliance"

	DefaultPartition        = "aws"
	DefaultHeartbeatSeconds = 900
	DefaultGracePeriod      = 300
	DefaultHopLimit         = 1
	DefaultRootDevice       = "/dev/xvda"
	DefaultRootVolumeGiB    = 8
	DefaultVolumeType       = "gp3"
	DefaultPolicyName       = "appliance"
	DefaultContentType      = "text/plain"
)

Defaults.

View Source
const (
	SidReadParameters = "ReadParameters"
	SidReadObjects    = "ReadObjects"
	SidOwnGroup       = "OwnGroup"
)

Statement ids of BootstrapStatements.

View Source
const PolicyVersion = "2012-10-17"

PolicyVersion is the IAM policy language version of every document.

Variables

This section is empty.

Functions

func AssumeRolePolicy

func AssumeRolePolicy(partition string) string

AssumeRolePolicy lets EC2 assume the instance role.

func Document

func Document(statements []Statement) (string, error)

Document renders statements as a policy document, indented by two spaces. A statement without an Effect is an Allow.

func GroupARN

func GroupARN(partition, region, account, group string) string

GroupARN is the ARN of an Auto Scaling group.

func ObjectARN

func ObjectARN(partition, bucket, key string) string

ObjectARN is the ARN of an S3 object.

func ParameterARN

func ParameterARN(partition, region, account, name string) string

ParameterARN is the ARN of an SSM parameter named like /estate/token.

Types

type Alarm

type Alarm struct {
	// Suffix names the alarm: "<Base>-<Suffix>", and its logical name.
	Suffix string
	// Name, when set, is the alarm's physical name instead.
	Name        string
	Description string
	Namespace   string
	MetricName  string
	Statistic   string
	// Comparison is GreaterThanThreshold, GreaterThanOrEqualToThreshold,
	// LessThanThreshold or LessThanOrEqualToThreshold.
	Comparison        string
	Threshold         float64
	PeriodSeconds     int
	EvaluationPeriods int
	DatapointsToAlarm int
	// TreatMissingData defaults to missing.
	TreatMissingData string
	Dimensions       map[string]string
}

Alarm is one metric alarm. Dimensions default to the group.

type AlarmsArgs

type AlarmsArgs struct {
	// ActionARNs are notified when an alarm fires (e.g. an SNS topic).
	// Empty: the alarms notify nobody.
	ActionARNs   []string
	OKActionARNs []string
	Alarms       []Alarm
}

AlarmsArgs are CloudWatch alarms on the appliance.

type Appliance

type Appliance struct {
	pulumi.ResourceState

	// Names are the physical names after defaults.
	Names Resolved
	// Group is the Auto Scaling group.
	Group *autoscaling.Group
	// LaunchTemplate carries the image, the user-data and IMDSv2.
	LaunchTemplate *ec2.LaunchTemplate
	// Role and InstanceProfile are the instance's identity.
	Role            *iam.Role
	InstanceProfile *iam.InstanceProfile
	// Policy is the role's main inline policy document.
	Policy pulumi.StringOutput
	// ManagedPolicies are the attachments of Args.IAM.ManagedPolicyARNs.
	ManagedPolicies []*iam.RolePolicyAttachment
	// SecurityGroup is the group the appliance created; nil with
	// Security.ExistingGroups.
	SecurityGroup *ec2.SecurityGroup
	// Rules are the separate rule resources (RulesSeparate, RulesLegacy).
	Rules []pulumi.Resource
	// Hook is the separate lifecycle hook; nil for an inline hook or none.
	Hook *autoscaling.LifecycleHook
	// Objects are the uploaded S3 objects, in the order of UserData.Objects
	// then Objects.Extra.
	Objects []*s3.BucketObjectv2
	// Alarms are the CloudWatch alarms, in the order of Alarms.Alarms.
	Alarms []*cloudwatch.MetricAlarm
}

Appliance is what New creates. With Compat.Flat it is a plain holder and not a registered component.

func New

func New(ctx *pulumi.Context, name string, args *Args, opts ...pulumi.ResourceOption) (*Appliance, error)

New creates the appliance. It refuses an invalid Args, all problems at once, before it registers anything.

The user-data is applied at launch: a change to it is a new launch template version, which Refresh rolls out.

type Args

type Args struct {
	// Names sets the physical names. Default: all derived from the
	// logical name.
	Names Names
	// Partition is the ARN partition. Default "aws".
	Partition string
	// Tags are added to every resource that takes tags.
	Tags map[string]string
	// Provider is the AWS provider the region, account and image lookups
	// go through. The resources take the options New is called with.
	Provider *aws.Provider

	// UserData is the instance's user-data. Required: a composed plan
	// (bootstrap.Plan.Render) or bootstrap.Verbatim. Its objects are
	// uploaded and its SSM parameters and object keys are granted to the
	// role.
	UserData *bootstrap.Rendered
	// Objects says where UserData.Objects are uploaded.
	Objects ObjectsArgs

	Image    ImageArgs
	Instance InstanceArgs
	Group    GroupArgs
	// Hook is the launch lifecycle hook; nil declares none.
	Hook *HookArgs
	// WarmPool is the warm pool; nil declares none. It needs Hook.
	WarmPool *WarmPoolArgs
	// Refresh is the group's instance refresh; nil declares none.
	Refresh *RefreshArgs

	IAM      IAMArgs
	Security SecurityArgs
	Alarms   AlarmsArgs

	// Contribution is what the modules add (see Collect).
	Contribution Contribution

	// Compat keeps the names of an existing program.
	Compat Compat
}

Args is New's whole input.

type Compat

type Compat struct {
	// ComponentType overrides the component's Pulumi type, so a program
	// that already has a component of its own type keeps its URNs.
	ComponentType string
	// Flat registers no component: the children keep the caller's
	// parent, as in a program that never had one.
	Flat bool
	// Names are the logical names of the children.
	Names Logical
	// Aliases are added to the children, by kind.
	Aliases map[Kind][]pulumi.Alias
	// ComponentAliases are added to the component.
	ComponentAliases []pulumi.Alias
	// GroupTagsExcludeCommon leaves Args.Tags off the group's propagated
	// tags (only Name and ManagedBy remain), as programs did before.
	GroupTagsExcludeCommon bool
}

Compat keeps an existing program's resources where they are.

type Contribution

type Contribution struct {
	// Fragments go into the bootstrap plan (see [Contribution.AddTo]).
	Fragments []bootstrap.Fragment
	// Statements are added to the role's policy.
	Statements []Statement
	// SSMParameters and ObjectKeys are readable by the role.
	SSMParameters []string
	ObjectKeys    []string
	// Ingress rules are added to the security group.
	Ingress []Rule
	// ManagedPolicyARNs are attached to the role.
	ManagedPolicyARNs []string
}

Contribution is what modules add to an appliance.

func Collect

func Collect(mods ...Module) (Contribution, error)

Collect merges the modules' contributions in the order given.

func (Contribution) AddTo

func (c Contribution) AddTo(p *bootstrap.Plan) error

AddTo adds the fragments to the plan and records the SSM parameters, so call it before rendering.

type GroupArgs

type GroupArgs struct {
	// Min, Max and Desired default to 1 (Max and Desired to Min).
	Min, Max, Desired int
	// SubnetIDs are required.
	SubnetIDs []pulumi.StringInput
	// HealthCheckType defaults to EC2.
	HealthCheckType string
	// GracePeriodSeconds defaults to 300.
	GracePeriodSeconds int
	// EnabledMetrics are group metrics sent to CloudWatch (an alarm on
	// GroupInServiceInstances needs it).
	EnabledMetrics []string
}

GroupArgs configures the Auto Scaling group.

type HookArgs

type HookArgs struct {
	Mode HookMode
	// HeartbeatTimeoutSeconds defaults to 900, bounded 30..7200.
	HeartbeatTimeoutSeconds int
	// DefaultResult is ABANDON (the default) or CONTINUE. CONTINUE needs
	// UnsafeDefaultContinue: an instance whose scripts never answered is
	// then put in service.
	DefaultResult         string
	UnsafeDefaultContinue bool
}

HookArgs is the EC2_INSTANCE_LAUNCHING hook.

type HookMode

type HookMode int

HookMode says where the lifecycle hook is declared.

const (
	// HookInline (the zero value) declares the hook on the group
	// (initial_lifecycle_hook), so it exists before the first instance
	// launches. A separate hook is created after the group and an instance
	// launched in between is not held by it.
	HookInline HookMode = iota
	// HookSeparate creates an aws.autoscaling.LifecycleHook resource.
	HookSeparate
)

type IAMArgs

type IAMArgs struct {
	// PermissionsBoundary is the full ARN of the boundary policy.
	PermissionsBoundary string
	// ManagedPolicyARNs are attached to the role. AmazonSSMManagedInstanceCore
	// lets the role read every parameter of the account; attach it only
	// for Session Manager.
	ManagedPolicyARNs []string
	// PolicyName is the inline policy's name. Default "appliance".
	PolicyName string
	// SSMParameters and ObjectKeys are granted in addition to the
	// user-data's.
	SSMParameters []string
	ObjectKeys    []string
	// Statements are added to the generated policy.
	Statements []Statement
	// StatementsFunc is like Statements for statements that need the
	// region, account or names.
	StatementsFunc func(PolicyContext) []Statement
	// Policy replaces the generated policy (the migration path of step
	// A). It excludes Statements, StatementsFunc, SSMParameters,
	// ObjectKeys, the user-data's grants and the modules'.
	Policy func(PolicyContext) (string, error)
	// ExtraInlinePolicies are further inline policies of the role.
	ExtraInlinePolicies []InlinePolicy
}

IAMArgs is the instance role.

type ImageArgs

type ImageArgs struct {
	// ID is a literal image id (or an output).
	ID pulumi.StringInput
	// NameFilter is an AMI name pattern, such as "al2023-ami-2023.*-kernel-6.1-arm64";
	// the newest available image wins.
	NameFilter string
	// Owners of the NameFilter lookup. Default "amazon".
	Owners []string
	// Architecture narrows the NameFilter lookup (x86_64, arm64).
	Architecture string
	// SSMParameter is a public or own parameter holding the image id.
	SSMParameter string
}

ImageArgs finds the AMI. Exactly one of ID, NameFilter and SSMParameter.

type InlinePolicy

type InlinePolicy struct {
	Name   string
	Policy func(PolicyContext) (string, error)
}

InlinePolicy is a named inline policy document.

type InstanceArgs

type InstanceArgs struct {
	// Type is required.
	Type string
	// HopLimit is the IMDS response hop limit, 1..64. Default 1. IMDSv2 is
	// always required.
	HopLimit int
	// RootVolume nil leaves the image's root volume alone.
	RootVolume *RootVolumeArgs
	// PublicIP, when set, declares a network interface that associates
	// (true) or does not associate (false) a public address; nil
	// declares no interface and the group's subnets decide.
	PublicIP *bool
	// ExtraSecurityGroupIDs are attached next to the appliance's own
	// group.
	ExtraSecurityGroupIDs []pulumi.StringInput
}

InstanceArgs configures the launch template.

type Kind

type Kind string

Kind is a kind of child, for Compat.Aliases.

const (
	KindRole            Kind = "role"
	KindInstanceProfile Kind = "instance-profile"
	KindManagedPolicy   Kind = "managed-policy"
	KindSecurityGroup   Kind = "security-group"
	KindSecurityRule    Kind = "security-rule"
	KindLaunchTemplate  Kind = "launch-template"
	KindGroup           Kind = "group"
	KindHook            Kind = "hook"
	KindAlarm           Kind = "alarm"
	KindObject          Kind = "object"
)

Kinds.

type Logical

type Logical struct {
	// Role, InstanceProfile, SecurityGroup, LaunchTemplate and Group
	// default to the component's name.
	Role, InstanceProfile, SecurityGroup, LaunchTemplate, Group string
	// Hook defaults to "<name>-hook".
	Hook string
	// ManagedPolicy defaults to "<name>-managed-<index>".
	ManagedPolicy func(index int, arn string) string
	// Ingress and Egress default to "<name>-ingress-<i>[-<j>]" and
	// "<name>-egress-<i>[-<j>]" for separate rules.
	Ingress, Egress func(index, address int) string
	// Alarm defaults to "<name>-alarm-<suffix>".
	Alarm func(suffix string) string
	// Object defaults to "<name>-object-<first 12 of the digest>".
	Object func(bootstrap.Object) string
}

Logical overrides logical names. An empty field or nil func keeps the default, which is documented on each field.

type Module

type Module interface {
	// Name identifies the module in errors.
	Name() string
	// Contribute returns what the module adds.
	Contribute() (Contribution, error)
}

Module is an optional piece of an appliance that touches several layers at once: the user-data (sshd with user certificates, self-registering DNS, journald, swap), the role and the network. A module returns plain data; it never creates a resource, so the logical names of the appliance are the same with and without it.

type Names

type Names struct {
	// Base defaults to the logical name.
	Base string
	// Group defaults to Base.
	Group string
	// LaunchTemplate defaults to Base.
	LaunchTemplate string
	// Role defaults to Base; the instance profile has the same name.
	Role string
	// InstanceProfile defaults to Role.
	InstanceProfile string
	// Hook defaults to Group + "-launch".
	Hook string
	// SecurityGroup, when set, is the group's fixed name. Empty: a
	// generated name with the prefix Group + "-", so a replacement is
	// created before the old group is deleted.
	SecurityGroup string
}

Names are the physical names. An empty field takes its default from Base.

func (Names) Resolve

func (n Names) Resolve(name string) Resolved

Resolve applies the defaults for the logical name. A caller that renders the plan before calling New needs the group and hook names for bootstrap.Options.Lifecycle: use Resolved.Lifecycle.

type ObjectsArgs

type ObjectsArgs struct {
	// Bucket is required when the user-data has objects or Extra is set.
	Bucket string
	// KMSKeyARN encrypts the objects with that key (aws:kms); empty:
	// AES256.
	KMSKeyARN string
	// ContentType defaults to "text/plain".
	ContentType string
	// Extra are objects uploaded in addition to UserData.Objects, and
	// readable by the role. A migration whose old user-data fetches
	// objects the program owns lists them here.
	Extra []bootstrap.Object
	// Tags, when set, replaces the object's tags (default: Name = the
	// resource name, ManagedBy = pulumi, Tags).
	Tags func(bootstrap.Object) map[string]string
}

ObjectsArgs says where the user-data's S3 objects go.

type PolicyContext

type PolicyContext struct {
	Partition string
	Region    string
	Account   string
	Names     Resolved
	// Bucket is Args.Objects.Bucket.
	Bucket string
	// SSMParameters and ObjectKeys are every parameter and object key the
	// appliance reads: the user-data's, Args.IAM's and the modules'.
	SSMParameters []string
	ObjectKeys    []string
}

PolicyContext is what a policy callback may use.

type PolicyInput

type PolicyInput struct {
	Partition string
	Region    string
	Account   string
	// Group is the Auto Scaling group name. Empty: no group statement.
	Group string
	// Bucket holds ObjectKeys.
	Bucket        string
	SSMParameters []string
	ObjectKeys    []string
}

PolicyInput is the input of BootstrapStatements.

type RefreshArgs

type RefreshArgs struct {
	MinHealthyPercentage int
	// WarmupSeconds defaults to 300.
	WarmupSeconds int
	// Triggers default to launch_template.
	Triggers []string
}

RefreshArgs is the rolling instance refresh the launch template change triggers.

type Resolved

type Resolved struct {
	Base            string
	Group           string
	LaunchTemplate  string
	Role            string
	InstanceProfile string
	Hook            string
	// SecurityGroup is the fixed name, or empty.
	SecurityGroup string
	// SecurityGroupPrefix is the generated-name prefix when SecurityGroup is
	// empty.
	SecurityGroupPrefix string
}

Resolved are the physical names after defaults.

func (Resolved) Lifecycle

func (r Resolved) Lifecycle() *bootstrap.Lifecycle

Lifecycle is what bootstrap.Options.Lifecycle needs to abandon the right hook of the right group.

type RootVolumeArgs

type RootVolumeArgs struct {
	// DeviceName defaults to /dev/xvda.
	DeviceName string
	// SizeGiB defaults to 8.
	SizeGiB int
	// Type defaults to gp3.
	Type string
	// KMSKeyID, when set, encrypts with that key instead of the default.
	KMSKeyID string
}

RootVolumeArgs is the root EBS volume. It is encrypted.

type Rule

type Rule struct {
	Description string
	// Protocol is tcp (default), udp, icmp, icmpv6 or -1.
	Protocol string
	// FromPort and ToPort; both 0 with -1.
	FromPort, ToPort int
	CIDRs            []string
	IPv6CIDRs        []string
	SourceGroups     []pulumi.StringInput
	// World acknowledges that an ingress rule opens a port to 0.0.0.0/0
	// or ::/0. Without it such a rule is refused.
	World bool
}

Rule is one security group rule.

type RuleStyle

type RuleStyle int

RuleStyle says how security group rules are declared.

const (
	// RulesInline (the zero value) puts the rules in the group's ingress and
	// egress blocks.
	RulesInline RuleStyle = iota
	// RulesSeparate creates one aws.vpc.SecurityGroupIngressRule or
	// SecurityGroupEgressRule per rule and address.
	RulesSeparate
	// RulesLegacy creates one aws.ec2.SecurityGroupRule per rule, the resource
	// older programs use.
	RulesLegacy
)

type SecurityArgs

type SecurityArgs struct {
	// VPCID is required unless ExistingGroups is set.
	VPCID pulumi.StringInput
	// ExistingGroups use these groups instead of creating one; Ingress
	// and Egress are then refused.
	ExistingGroups []pulumi.StringInput
	// Description cannot change in place. Default "<Base> appliance".
	Description string
	Style       RuleStyle
	Ingress     []Rule
	// Egress nil allows all outbound traffic; an empty non-nil slice
	// allows none.
	Egress []Rule
	// DeleteTimeout bounds the provider's retry of DependencyViolation
	// when a replaced group is deleted, e.g. "30m".
	DeleteTimeout string
}

SecurityArgs is the security group.

type Statement

type Statement struct {
	Sid       string                         `json:"Sid,omitempty"`
	Effect    string                         `json:"Effect"`
	Action    []string                       `json:"Action"`
	Resource  []string                       `json:"Resource"`
	Condition map[string]map[string][]string `json:"Condition,omitempty"`
}

Statement is one IAM policy statement. The JSON field order is fixed, and Action and Resource are always arrays, so a document is byte-stable.

func BootstrapStatements

func BootstrapStatements(in PolicyInput) []Statement

BootstrapStatements are the statements the bootstrap scripts need and no more: ssm:GetParameter on exactly the parameters, s3:GetObject on exactly the object keys and, when Group is set, the two Auto Scaling actions bootstrap-ready.service and the failure path call, on that group. A list that is empty yields no statement; the lists are sorted and de-duplicated.

type WarmPoolArgs

type WarmPoolArgs struct {
	// State is Stopped (default), Running or Hibernated.
	State string
	// MinSize defaults to 0.
	MinSize int
	// MaxGroupPreparedCapacity nil: no cap beyond the group's maximum.
	MaxGroupPreparedCapacity *int
}

WarmPoolArgs is a pool of prepared instances.

Directories

Path Synopsis
Package bootstrap composes the cloud-init user-data (#cloud-config) of an instance from fragments.
Package bootstrap composes the cloud-init user-data (#cloud-config) of an instance from fragments.

Jump to

Keyboard shortcuts

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