Gestire un’istanza Amazon AWS EC2 con Node.js

In questa guida trovi un percorso completo per avviare, fermare, riavviare, descrivere e creare istanze EC2 usando Node.js e l’AWS SDK per JavaScript v3. Gli esempi sono pensati per Node 18+ e si basano su credenziali configurate nel profilo AWS o in variabili d’ambiente.

Prerequisiti

  • Un account AWS con permessi su EC2 (meglio tramite un role a privilegi minimi).
  • Node.js 18 o superiore installato.
  • Credenziali configurate con aws configure oppure variabili d’ambiente AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION.

Esempio di policy IAM a privilegi minimi per gestione base

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

Se devi anche creare/terminare istanze, aggiungi: ec2:RunInstances, ec2:TerminateInstances, ec2:CreateTags, e i permessi correlati a key pair e security group.

Installazione dei pacchetti

npm init -y
npm i @aws-sdk/client-ec2 @aws-sdk/credential-providers

Configurazione del client EC2

// ec2-client.js
import { EC2Client } from "@aws-sdk/client-ec2";
import { fromEnv } from "@aws-sdk/credential-providers";

export const ec2 = new EC2Client({
  region: process.env.AWS_REGION || "eu-central-1",
  credentials: process.env.AWS_ACCESS_KEY_ID
  ? fromEnv()
  : undefined // userà profilo/role locale se disponibile
});

Operazioni comuni su un’istanza esistente

Supponiamo di avere l’ID dell’istanza, ad esempio i-0123456789abcdef0. Crea funzioni riutilizzabili:

Descrivere stato e dettagli

// describe.js
import { DescribeInstancesCommand } from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";

export async function describeInstance(instanceId) {
  const cmd = new DescribeInstancesCommand({ InstanceIds: [instanceId] });
  const res = await ec2.send(cmd);
  const reservation = res.Reservations?.[0];
  const inst = reservation?.Instances?.[0];
  if (!inst) throw new Error("Istanza non trovata");
  return {
    id: inst.InstanceId,
    state: inst.State?.Name,
    type: inst.InstanceType,
    az: inst.Placement?.AvailabilityZone,
    publicIp: inst.PublicIpAddress,
    privateIp: inst.PrivateIpAddress,
    launchTime: inst.LaunchTime
  };
}

Avviare, fermare, riavviare

// lifecycle.js
import {
  StartInstancesCommand,
  StopInstancesCommand,
  RebootInstancesCommand
} from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";

export async function startInstance(instanceId) {
  await ec2.send(new StartInstancesCommand({ InstanceIds: [instanceId] }));
}

export async function stopInstance(instanceId, hibernate = false) {
  await ec2.send(new StopInstancesCommand({
    InstanceIds: [instanceId],
    Hibernate: hibernate
  }));
}

export async function rebootInstance(instanceId) {
  await ec2.send(new RebootInstancesCommand({ InstanceIds: [instanceId] }));
}

Attendere transizioni di stato (waiters)

I waiters possono attendere in modo affidabile che l’istanza sia running o stopped prima di procedere.

// waiters.js
import {
  waitUntilInstanceRunning,
  waitUntilInstanceStopped
} from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";

export async function waitRunning(instanceId, maxWaitSeconds = 300) {
  const out = await waitUntilInstanceRunning(
    { client: ec2, maxWaitTime: maxWaitSeconds },
    { InstanceIds: [instanceId] }
  );
  if (out.state !== "SUCCESS") throw new Error("Timeout in attesa di running");
}

export async function waitStopped(instanceId, maxWaitSeconds = 300) {
  const out = await waitUntilInstanceStopped(
    { client: ec2, maxWaitTime: maxWaitSeconds },
    { InstanceIds: [instanceId] }
  );
  if (out.state !== "SUCCESS") throw new Error("Timeout in attesa di stopped");
}

Creare una nuova istanza

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

// create-instance.js
import {
  RunInstancesCommand,
  CreateTagsCommand,
  DescribeImagesCommand
} from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";

// Facoltativo: recupera l'AMI più recente di Amazon Linux 2023 (x86_64) per la regione
async function getLatestAL2023Ami() {
  const res = await ec2.send(new DescribeImagesCommand({
    Owners: ["amazon"],
    Filters: [
      { Name: "name", Values: ["al2023-ami-*-x86_64"] },
      { Name: "state", Values: ["available"] },
      { Name: "architecture", Values: ["x86_64"] },
      { Name: "root-device-type", Values: ["ebs"] }
    ]
  }));
  const images = res.Images ?? [];
  images.sort((a, b) => new Date(b.CreationDate) - new Date(a.CreationDate));
  if (!images[0]?.ImageId) throw new Error("AMI non trovata");
  return images[0].ImageId;
}

export async function createInstance({
  amiId,
  instanceType = "t3.micro",
  keyName,
  securityGroupIds,
  subnetId,
  tags = { Project: "demo-node-ec2" }
}) {
  const imageId = amiId || await getLatestAL2023Ami();
  const userData = Buffer.from(`#!/bin/bash
    dnf -y update
    dnf -y install nginx
    systemctl enable nginx
    systemctl start nginx
  `).toString("base64");

  const run = new RunInstancesCommand({
    ImageId: imageId,
    InstanceType: instanceType,
    KeyName: keyName,
    SecurityGroupIds: securityGroupIds,
    SubnetId: subnetId,
    MinCount: 1,
    MaxCount: 1,
    TagSpecifications: [
      {
        ResourceType: "instance",
        Tags: Object.entries(tags).map(([Key, Value]) => ({ Key, Value }))
      }
    ],
    UserData: userData
  });

  const out = await ec2.send(run);
  const instanceId = out.Instances?.[0]?.InstanceId;
  if (!instanceId) throw new Error("Creazione istanza fallita");

  // (Opzionale) aggiungi tag al volume root
  try {
    const volumeId = out.Instances?.[0]?.BlockDeviceMappings?.[0]?.Ebs?.VolumeId;
    if (volumeId) {
      await ec2.send(new CreateTagsCommand({
        Resources: [volumeId],
        Tags: Object.entries(tags).map(([Key, Value]) => ({ Key, Value }))
      }));
    }
  } catch {}

  return instanceId;
}

Gestione sicura di credenziali e ruoli

  • Preferisci l’uso di IAM Role (ad es. su un runner in EC2/CodeBuild) invece di chiavi statiche.
  • Per ambienti locali, usa profili nel file ~/.aws/credentials e AWS_PROFILE.
  • Limita i permessi al minimo necessario: separa la policy “read/describe” da quella “lifecycle”.

Best practice operative

  1. Idempotenza: proteggi i comandi ripetuti (es. controlla lo stato prima di chiamare StartInstances/StopInstances).
  2. Retry e backoff: le API AWS possono rispondere con throttling; usa retry esponenziale se integri orchestrazioni più complesse.
  3. Waiters: affidati ai waiters per stati consistenti prima di eseguire step successivi (allocation IP, tagging, health check).
  4. Tagging: applica tag coerenti per cost allocation e ricerca (Project, Env, Owner).
  5. Sicurezza: key pair private va protetta; restringi i security group (SSH solo dal tuo IP, HTTP/HTTPS secondo necessità).
  6. Spegnimento: automatizza lo stop di istanze non usate per contenere i costi (es. cron esterno o Lambda Scheduler).

Diagnostica rapida

// utils-status.js
import { DescribeInstanceStatusCommand } from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";

export async function instanceChecks(instanceId) {
  const res = await ec2.send(new DescribeInstanceStatusCommand({
    InstanceIds: [instanceId],
    IncludeAllInstances: true
  }));
  return res.InstanceStatuses?.[0] ?? null;
}

Conclusione

Con l’AWS SDK v3 e poche funzioni ben strutturate puoi orchestrare l’intero ciclo di vita di un’istanza EC2 direttamente da Node.js: dalla creazione con user data al controllo dello stato, fino allo stop programmato. Adatta gli esempi alle tue regole IAM, ai tuoi security group e alle tue esigenze di rete e osservabilità.

Torna su