Gestire un’istanza Amazon EC2 con Go

Questa guida mostra come avviare, fermare, riavviare, descrivere e creare istanze EC2 usando Go con l’AWS SDK v2. Gli esempi richiedono Go 1.21+, moduli Go attivati e credenziali configurate via profilo AWS o variabili d’ambiente.

Prerequisiti

  • Account AWS con permessi su EC2 (principio del least privilege).
  • Go 1.21 o superiore.
  • Credenziali configurate con aws configure oppure variabili AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION.

Esempio di policy IAM minima

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

Per creare/terminare istanze aggiungi: ec2:RunInstances, ec2:TerminateInstances, ec2:CreateTags, oltre ai permessi su key pair e security group.

Installazione moduli

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

Configurazione del client EC2

// 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
}

Operazioni comuni su istanza esistente

Supponiamo di avere l’ID istanza, ad es. i-0123456789abcdef0. Creiamo funzioni riutilizzabili:

Describe: stato e dettagli

// internal/ec2ops/describe.go
package ec2ops

import (
  "context"
  "errors"
  "time"

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

type InstanceInfo struct {
  ID        string
  State     string
  Type      string
  AZ        string
  PublicIP  string
  PrivateIP string
  Launch    string
}

func DescribeInstance(ctx context.Context, cli *ec2.Client, instanceID string) (*InstanceInfo, error) {
  out, err := cli.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
    InstanceIds: []string{instanceID},
  })
  if err != nil {
    return nil, err
  }
  if len(out.Reservations) == 0 || len(out.Reservations[0].Instances) == 0 {
    return nil, errors.New("istanza non trovata")
  }
  inst := out.Reservations[0].Instances[0]
  info := &InstanceInfo{
    ID:        value(inst.InstanceId),
    State:     value(inst.State.Name),
    Type:      string(inst.InstanceType),
    AZ:        value(inst.Placement.AvailabilityZone),
    PublicIP:  value(inst.PublicIpAddress),
    PrivateIP: value(inst.PrivateIpAddress),
    Launch:    inst.LaunchTime.In(time.Local).Format("2006-01-02 15:04:05"),
  }
  return info, nil
}

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

Avviare, fermare, riavviare

// internal/ec2ops/lifecycle.go
package ec2ops

import (
  "context"

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

func Start(ctx context.Context, cli *ec2.Client, id string) error {
  _, err := cli.StartInstances(ctx, &ec2.StartInstancesInput{InstanceIds: []string{id}})
  return err
}

func Stop(ctx context.Context, cli *ec2.Client, id string, hibernate bool) error {
  _, err := cli.StopInstances(ctx, &ec2.StopInstancesInput{
    InstanceIds: []string{id},
    Hibernate:   &hibernate,
  })
  return err
}

func Reboot(ctx context.Context, cli *ec2.Client, id string) error {
  _, err := cli.RebootInstances(ctx, &ec2.RebootInstancesInput{InstanceIds: []string{id}})
  return err
}

Attendere transizioni di stato (waiters)

Usiamo i waiter nativi per attendere running o stopped con timeout.

// internal/ec2ops/waiters.go
package ec2ops

import (
  "context"
  "time"

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

func WaitRunning(ctx context.Context, cli *ec2.Client, id string, maxWait time.Duration) error {
  waiter := ec2.NewInstanceRunningWaiter(cli)
  ctx, cancel := context.WithTimeout(ctx, maxWait)
  defer cancel()
  return waiter.Wait(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{id}}, 10*time.Second)
}

func WaitStopped(ctx context.Context, cli *ec2.Client, id string, maxWait time.Duration) error {
  waiter := ec2.NewInstanceStoppedWaiter(cli)
  ctx, cancel := context.WithTimeout(ctx, maxWait)
  defer cancel()
  return waiter.Wait(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{id}}, 10*time.Second)
}

Creare una nuova istanza

Per creare un’istanza servono AMI, tipo, key pair (SSH), security group e subnet. Esempio con tag e user data per installare Nginx su Amazon Linux 2023.

// internal/ec2ops/create.go
package ec2ops

import (
  "context"
  "encoding/base64"
  "errors"
  "sort"
  "time"

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

func LatestAL2023AMI(ctx context.Context, cli *ec2.Client) (string, error) {
  out, err := cli.DescribeImages(ctx, &ec2.DescribeImagesInput{
    Owners: []string{"amazon"},
    Filters: []ec2types.Filter{
      {Name: str("name"), Values: []string{"al2023-ami-*-x86_64"}},
      {Name: str("state"), Values: []string{"available"}},
      {Name: str("architecture"), Values: []string{"x86_64"}},
      {Name: str("root-device-type"), Values: []string{"ebs"}},
    },
  })
  if err != nil {
    return "", err
  }
  images := out.Images
  sort.Slice(images, func(i, j int) bool {
    ti, _ := time.Parse(time.RFC3339, value(images[i].CreationDate))
    tj, _ := time.Parse(time.RFC3339, value(images[j].CreationDate))
    return ti.After(tj)
  })
  if len(images) == 0 || images[0].ImageId == nil {
    return "", errors.New("AMI non trovata")
  }
  return *images[0].ImageId, nil
}

type CreateParams struct {
  AMI              string
  InstanceType     string
  KeyName          string
  SecurityGroupIDs []string
  SubnetID         string
  Tags             map[string]string
}

func CreateInstance(ctx context.Context, cli *ec2.Client, params CreateParams) (string, error) {
  imageID := params.AMI
  var err error
  if imageID == "" {
    imageID, err = LatestAL2023AMI(ctx, cli)
    if err != nil {
      return "", err
    }
  }

  userData := base64.StdEncoding.EncodeToString([]byte(`#!/bin/bash
    dnf -y update
    dnf -y install nginx
    systemctl enable nginx
    systemctl start nginx
  `))

  out, err := cli.RunInstances(ctx, &ec2.RunInstancesInput{
    ImageId:      &imageID,
    InstanceType: ec2types.InstanceType(params.InstanceType),
    KeyName:      sp(params.KeyName),
    SecurityGroupIds: params.SecurityGroupIDs,
    SubnetId:     sp(params.SubnetID),
    MinCount:     ip(1),
    MaxCount:     ip(1),
    UserData:     &userData,
    TagSpecifications: []ec2types.TagSpecification{
    {
      ResourceType: ec2types.ResourceTypeInstance,
      Tags:         toTags(params.Tags),
    },
   },
  })
  if err != nil {
    return "", err
  }
 if len(out.Instances) == 0 || out.Instances[0].InstanceId == nil {
    return "", errors.New("creazione istanza fallita")
 }
  return *out.Instances[0].InstanceId, nil
}

func toTags(m map[string]string) []ec2types.Tag {
  var tags []ec2types.Tag
  for k, v := range m {
    k, v := k, v
    tags = append(tags, ec2types.Tag{Key: &k, Value: &v})
  }
  return tags
}

func sp(s string) *string { if s == "" { return nil }; return &s }
func ip(v int32) *int32  { return &v }
func str(s string) *string { return &s }

Gestione sicura di credenziali e ruoli

  • Preferisci IAM Role su runner/istanze rispetto a chiavi statiche.
  • Per sviluppo locale, usa profili in ~/.aws/credentials e AWS_PROFILE.
  • Separa policy di lettura (describe) da quelle di ciclo di vita (start/stop/run/reboot).

Best practice operative

  1. Idempotenza: controlla lo stato prima di invocare StartInstances/StopInstances.
  2. Backoff: gestisci il throttling con retry esponenziale (eventuale middleware).
  3. Waiters: usa i waiter per stati consistenti prima di step successivi (assegnazione IP, health check, tagging).
  4. Tagging: adotta tag coerenti per cost allocation (Project, Env, Owner).
  5. Sicurezza: proteggi la private key del key pair; restringi i Security Group (SSH solo dal tuo IP).
  6. Spegnimento: programma stop delle istanze non usate (cron/CI).

Diagnostica rapida

// internal/ec2ops/status.go
package ec2ops

import (
  "context"

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

func StatusChecks(ctx context.Context, cli *ec2.Client, id string) (*ec2.DescribeInstanceStatusOutput, error) {
  return cli.DescribeInstanceStatus(ctx, &ec2.DescribeInstanceStatusInput{
    InstanceIds:         []string{id},
    IncludeAllInstances: boolp(true),
  })
}

func boolp(b bool) *bool { return &b }

Variabili d’ambiente utili

AWS_ACCESS_KEY_ID=<la_tua_access_key>
AWS_SECRET_ACCESS_KEY=<la_tua_secret_key>
AWS_REGION=eu-central-1
AWS_PROFILE=default

Conclusione

Con l’AWS SDK v2 per Go e poche funzioni ben strutturate puoi orchestrare l’intero ciclo di vita di un’istanza EC2: dalla creazione con user data al controllo dello stato, fino allo stop programmato. Adatta questi esempi alle tue policy IAM, ai tuoi security group e ai requisiti di rete e osservabilità.

Torna su