Gestire un’istanza Amazon EC2 con Laravel

Questa guida mostra come avviare, fermare, riavviare, descrivere e creare istanze EC2 da un’app Laravel usando l’AWS SDK for PHP v3. Gli esempi richiedono PHP 8.1+, Laravel 10+ e credenziali configurate tramite .env o profili locali.

Prerequisiti

  • Account AWS con permessi su EC2 (meglio via IAM Role o utente con principi di least privilege).
  • PHP 8.1+ e Laravel installati.
  • Variabili d’ambiente per le credenziali: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION.

Esempio di policy IAM minima per gestione base

{
  "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 e i permessi per key pair e security group.

Installazione pacchetti

composer require aws/aws-sdk-php

Configurazione variabili d’ambiente

cp .env .env.example.backup
AWS_ACCESS_KEY_ID=<la_tua_access_key>
AWS_SECRET_ACCESS_KEY=<la_tua_secret_key>
AWS_REGION=eu-central-1
AWS_EC2_DEFAULT_INSTANCE=i-0123456789abcdef0

Creare un service EC2 riutilizzabile

php artisan make:directory App/Services
# oppure crea la cartella manualmente
<?php
// app/Services/Ec2Service.php
namespace App\Services;

use Aws\Ec2\Ec2Client;
use Aws\Exception\AwsException;

class Ec2Service
{
public function __construct(
  private ?Ec2Client $client = null
) {
  $this->client = $client ?: new Ec2Client([
    'version' => 'latest',
    'region'  => config('app.aws_region', env('AWS_REGION', 'eu-central-1')),
    'credentials' => [
      'key'    => env('AWS_ACCESS_KEY_ID'),
      'secret' => env('AWS_SECRET_ACCESS_KEY'),
     ],
   ]);
}

public function describe(string $instanceId): array
{
    $res = $this->client->describeInstances([
        'InstanceIds' => [$instanceId],
    ]);
    $inst = $res['Reservations'][0]['Instances'][0] ?? null;
    if (!$inst) {
        throw new RuntimeException('Istanza non trovata');
    }
    return [
        'id' => $inst['InstanceId'] ?? null,
        'state' => $inst['State']['Name'] ?? null,
        'type' => $inst['InstanceType'] ?? null,
        'az' => $inst['Placement']['AvailabilityZone'] ?? null,
        'publicIp' => $inst['PublicIpAddress'] ?? null,
        'privateIp' => $inst['PrivateIpAddress'] ?? null,
        'launchTime' => (string)($inst['LaunchTime'] ?? ''),
    ];
}

public function start(string $instanceId): void
{
    $this->client->startInstances(['InstanceIds' => [$instanceId]]);
}

public function stop(string $instanceId, bool $hibernate = false): void
{
    $this->client->stopInstances([
        'InstanceIds' => [$instanceId],
        'Hibernate' => $hibernate,
    ]);
}

public function reboot(string $instanceId): void
{
    $this->client->rebootInstances(['InstanceIds' => [$instanceId]]);
}

public function statusChecks(string $instanceId): array
{
    $res = $this->client->describeInstanceStatus([
        'InstanceIds' => [$instanceId],
        'IncludeAllInstances' => true,
    ]);
    return $res['InstanceStatuses'][0] ?? [];
}

// Waiters semplici via polling
public function waitRunning(string $instanceId, int $timeout = 300): void
{
    $this->waitForState($instanceId, 'running', $timeout);
}

public function waitStopped(string $instanceId, int $timeout = 300): void
{
    $this->waitForState($instanceId, 'stopped', $timeout);
}

private function waitForState(string $instanceId, string $target, int $timeout): void
{
    $deadline = time() + $timeout;
    do {
        $state = ($this->describe($instanceId)['state']) ?? null;
        if ($state === $target) return;
        usleep(5_000_00); // 0.5s
    } while (time() < $deadline);

    throw new RuntimeException("Timeout in attesa di stato {$target}");
}

public function create(array $params): string
{
    $defaults = [
        'InstanceType' => 't3.micro',
        'MinCount' => 1,
        'MaxCount' => 1,
    ];

    $run = $this->client->runInstances(array_merge($defaults, $params));
    $instanceId = $run['Instances'][0]['InstanceId'] ?? null;
    if (!$instanceId) {
        throw new RuntimeException('Creazione istanza fallita');
    }
    return $instanceId;
}
```

}

Comandi Artisan per orchestrare EC2

Describe / Start / Stop / Reboot

php artisan make:command Ec2Describe
php artisan make:command Ec2Start
php artisan make:command Ec2Stop
php artisan make:command Ec2Reboot
<?php
// app/Console/Commands/Ec2Describe.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\Ec2Service;

class Ec2Describe extends Command
{
    protected $signature = 'ec2:describe {instanceId?}';
    protected $description = 'Mostra dettagli di un\'istanza EC2';

    public function handle(Ec2Service $ec2): int
    {
        $id = $this->argument('instanceId') ?: env('AWS_EC2_DEFAULT_INSTANCE');
        $info = $ec2->describe($id);
        $this->table(array_keys($info), [array_values($info)]);
        return self::SUCCESS;
    }
}
<?php
// app/Console/Commands/Ec2Start.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\Ec2Service;

class Ec2Start extends Command
{
    protected $signature = 'ec2:start {instanceId?} {--wait}';
    protected $description = 'Avvia un\'istanza EC2';

    public function handle(Ec2Service $ec2): int
    {
        $id = $this->argument('instanceId') ?: env('AWS_EC2_DEFAULT_INSTANCE');
        $ec2->start($id);
        $this->info("Start inviato a {$id}");
        if ($this->option('wait')) {
            $ec2->waitRunning($id);
            $this->info('Istanza in stato running');
        }
        return self::SUCCESS;
    }
}
<?php
// app/Console/Commands/Ec2Stop.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\Ec2Service;

class Ec2Stop extends Command
{
    protected $signature = 'ec2:stop {instanceId?} {--hibernate} {--wait}';
    protected $description = 'Ferma un\'istanza EC2';

    public function handle(Ec2Service $ec2): int
    {
        $id = $this->argument('instanceId') ?: env('AWS_EC2_DEFAULT_INSTANCE');
        $ec2->stop($id, (bool)$this->option('hibernate'));
        $this->info("Stop inviato a {$id}");
        if ($this->option('wait')) {
            $ec2->waitStopped($id);
            $this->info('Istanza in stato stopped');
        }
        return self::SUCCESS;
    }
}
<?php
// app/Console/Commands/Ec2Reboot.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\Ec2Service;

class Ec2Reboot extends Command
{
    protected $signature = 'ec2:reboot {instanceId?} {--wait}';
    protected $description = 'Riavvia un\'istanza EC2';

    public function handle(Ec2Service $ec2): int
    {
        $id = $this->argument('instanceId') ?: env('AWS_EC2_DEFAULT_INSTANCE');
        $ec2->reboot($id);
        $this->info("Reboot inviato a {$id}");
        if ($this->option('wait')) {
            $ec2->waitRunning($id);
            $this->info('Istanza di nuovo running');
        }
        return self::SUCCESS;
    }
}

Creare una nuova istanza EC2

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

<?php
// app/Console/Commands/Ec2Create.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\Ec2Service;
use Aws\Ec2\Ec2Client;

class Ec2Create extends Command
{
  protected $signature = 'ec2:create
    {--image-id= : AMI da usare (se omessa, passa quella giusta per la tua regione)}
    {--type=t3.micro : Istanza}
    {--key= : KeyPair}
    {--sg=* : Security Group IDs}
    {--subnet= : Subnet ID} 
    {--tag=* : Tag in formato Key=Value}
    {--wait : Attendi stato running}';

   protected $description = 'Crea una nuova istanza EC2';

   public function handle(Ec2Service $ec2): int
  {
    $imageId = $this->option('image-id');
    if (!$imageId) {
        $this->error('Specifica --image-id per evitare ambiguità.');
        return self::INVALID;
    }

    $tags = [];
    foreach ((array)$this->option('tag') as $t) {
        [$k, $v] = array_pad(explode('=', $t, 2), 2, '');
        if ($k !== '') $tags[] = ['Key' => $k, 'Value' => $v];
    }

    $userData = base64_encode(<<<'BASH'

  #!/bin/bash
   dnf -y update
   dnf -y install nginx
   systemctl enable nginx
   systemctl start nginx
   BASH);

    $params = [
        'ImageId' => $imageId,
        'InstanceType' => $this->option('type'),
        'KeyName' => $this->option('key'),
        'SecurityGroupIds' => (array)$this->option('sg'),
        'SubnetId' => $this->option('subnet'),
        'MinCount' => 1,
        'MaxCount' => 1,
        'UserData' => $userData,
        'TagSpecifications' => [[
            'ResourceType' => 'instance',
            'Tags' => $tags ?: [['Key' => 'Project','Value' => 'laravel-ec2']]
        ]]
    ];

    $id = $ec2->create($params);
    $this->info("Nuova istanza: {$id}");

    if ($this->option('wait')) {
        $ec2->waitRunning($id);
        $this->info('Istanza running');
    }

    return self::SUCCESS;
}

}

Esempi d’uso da CLI

# Descrivere
php artisan ec2:describe i-0123456789abcdef0

# Avviare e attendere running

php artisan ec2:start i-0123456789abcdef0 --wait

# Fermare con ibernazione e attendere stopped

php artisan ec2:stop i-0123456789abcdef0 --hibernate --wait

# Riavviare e attendere running

php artisan ec2:reboot i-0123456789abcdef0 --wait

# Creare una nuova istanza (parametri di esempio)

php artisan ec2:create 
\--image-id ami-0abcdef1234567890 
\--type t3.micro 
\--key my-key 
\--sg sg-0123abcd 
\--subnet subnet-0a1b2c3d 
\--tag Project=laravel-ec2 --tag Env=dev 
\--wait

Gestione sicura di credenziali e ruoli

  • Preferisci IAM Role associate a runner o a EC2/Lambda rispetto a chiavi statiche.
  • In locale usa .env e non committare mai le chiavi. Valuta l’uso di secret manager.
  • Separa permessi di sola lettura (describe) da quelli di ciclo di vita (start/stop/run/reboot).

Best practice operative

  1. Idempotenza: controlla lo stato prima di chiamare StartInstances/StopInstances.
  2. Retry e backoff: in caso di throttling applica retry esponenziale; per batch usa code/queue Laravel.
  3. Attese affidabili: usa i metodi wait (o polling) prima di step successivi come tagging o health check.
  4. Tagging: imposta tag coerenti per cost allocation (Project, Env, Owner).
  5. Sicurezza di rete: limita i Security Group (SSH solo dal tuo IP, HTTP/HTTPS secondo necessità).
  6. Spegnimento programmato: crona comandi Artisan (Scheduler) per spegnere le istanze inattive e ridurre i costi.

Scheduler Laravel: stop notturno di istanze

<?php
// app/Console/Kernel.php (estratto)
protected function schedule(\Illuminate\Console\Scheduling\Schedule $schedule): void
{
    $schedule->command('ec2:stop --wait')
        ->dailyAt('23:30')
        ->environments(['production'])
        ->withoutOverlapping();
}

Diagnostica rapida (status checks)

<?php
// uso rapido da Tinker
// php artisan tinker
// >>> app(\App\Services\Ec2Service::class)->statusChecks(env('AWS_EC2_DEFAULT_INSTANCE'));

Conclusione

Con un service dedicato e pochi comandi Artisan, Laravel può orchestrare l’intero ciclo di vita di istanze EC2: dalla creazione con user data al controllo dello stato, fino allo stop programmato. Adatta gli esempi a policy IAM, networking e osservabilità del tuo ambiente.

Torna su