Managing an Amazon EC2 instance with Go

This guide shows how to start, stop, reboot, describe, and create EC2 instances using Go with the AWS SDK v2. The examples require Go 1.21+, Go modules enabled, and credentials configured via AWS profile or environment variables.

Prerequisites

  • AWS account with EC2 permissions (principle of least privilege).
  • Go 1.21 or higher.
  • Credentials configured with aws configure or variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION.

Example of minimal IAM policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeInstanceStatus",
        "ec2:StartInstances",
        "ec2:StopInstances",
        "ec2:RebootInstances"
      ],
      "Resource": "*"
    }
  ]
}

To create/terminate instances add: ec2:RunInstances, ec2:TerminateInstances, ec2:CreateTags, plus permissions on key pairs and security groups.

Module installation

go mod init example.com/ec2-go
go get github.com/aws/aws-sdk-go-v2@latest
go get github.com/aws/aws-sdk-go-v2/config@latest
go get github.com/aws/aws-sdk-go-v2/service/ec2@latest

EC2 client configuration

// internal/aws/ec2client/ec2client.go
package ec2client

import (
  "context"
  "os"

  "github.com/aws/aws-sdk-go-v2/config"
  "github.com/aws/aws-sdk-go-v2/service/ec2"
)

func New(ctx context.Context) (*ec2.Client, error) {
  region := os.Getenv("AWS_REGION")
  opts := []func(*config.LoadOptions) error{}
  if region != "" {
    opts = append(opts, config.WithRegion(region))
  }
  cfg, err := config.LoadDefaultConfig(ctx, opts...)
  if err != nil {
    return nil, err
  }
  return ec2.NewFromConfig(cfg), nil
}

Common operations on an existing instance

Suppose we have the instance ID, e.g. i-0123456789abcdef0. Let’s create reusable functions:

Describe: state and details

// internal/ec2ops/describe.go
...
return info, nil
}

func value[T ~string](p *T) string {
  if p == nil {
    return ""
  }
  return string(*p)
}

Start, stop, reboot

// internal/ec2ops/lifecycle.go
...
  return err
}

Wait for state transitions (waiters)

We use native waiters to wait for running or stopped with a timeout.

// internal/ec2ops/waiters.go
...
  return waiter.Wait(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{id}}, 10*time.Second)
}

Create a new instance

To create an instance you need AMI, type, key pair (SSH), security group and subnet. Example with tags and user data to install Nginx on Amazon Linux 2023.

// internal/ec2ops/create.go
...
func str(s string) *string { return &s }

Secure credential and role management

  • Prefer IAM Roles on runners/instances instead of static keys.
  • For local development, use profiles in ~/.aws/credentials and AWS_PROFILE.
  • Separate read-only policies (describe) from lifecycle policies (start/stop/run/reboot).

Operational best practices

  1. Idempotency: check state before calling StartInstances/StopInstances.
  2. Backoff: handle throttling with exponential retry (middleware if needed).
  3. Waiters: use waiters for consistent states before next steps (IP assignment, health check, tagging).
  4. Tagging: adopt consistent tags for cost allocation (Project, Env, Owner).
  5. Security: protect the key pair’s private key; restrict Security Groups (SSH only from your IP).
  6. Shutdown: schedule stop of unused instances (cron/CI).

Quick diagnostics

// internal/ec2ops/status.go
...
func boolp(b bool) *bool { return &b }

Useful environment variables

AWS_ACCESS_KEY_ID=<your_access_key>
AWS_SECRET_ACCESS_KEY=<your_secret_key>
AWS_REGION=eu-central-1
AWS_PROFILE=default

Conclusion

With the AWS SDK v2 for Go and a few well-structured functions you can orchestrate the entire lifecycle of an EC2 instance: from creation with user data to state checking, up to scheduled stop. Adapt these examples to your IAM policies, security groups, and network/observability requirements.

Back to top