Install the Package:
composer require draw/aws-tool-kit
Ensure your Laravel project uses PHP 8.1+ and Symfony 6.4+ components.
Register Commands:
Add the package's commands to Laravel's app/Console/Kernel.php:
protected $commands = [
\Draw\AwsToolKit\Command\CloudWatchLogsCommand::class,
\Draw\AwsToolKit\Command\NewestInstanceRoleCommand::class,
];
Configure AWS SDK:
Publish the package's config (if available) or set AWS credentials in .env:
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_DEFAULT_REGION=us-east-1
First Use Case: Download CloudWatch logs for an RDS slow query log:
php artisan draw:aws:cloud-watch-logs:download \
/aws/rds/cluster/prod-dbcluster/slowquery \
prod-1 \
./storage/app/logs/slow-query.log
For appending logs to an existing file:
php artisan draw:aws:cloud-watch-logs:download \
/aws/rds/cluster/prod-dbcluster/slowquery \
prod-1 \
./storage/app/logs/slow-query.log \
--fileMode=a+
Scheduled Log Aggregation: Use Laravel's task scheduling to automate log downloads:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('draw:aws:cloud-watch-logs:download {log-group} {log-stream} {output-path}')
->dailyAt('03:00')
->withoutOverlapping();
}
Call it dynamically with parameters:
php artisan schedule:run
Event-Driven Processing: Extend the package to dispatch events after log download:
// Extend CloudWatchLogsCommand
protected function handleDownload()
{
// ... existing logic ...
event(new LogsDownloaded($this->logGroup, $this->logStream, $this->outputPath));
}
Listen for the event in Laravel:
// app/Providers/EventServiceProvider.php
protected $listen = [
LogsDownloaded::class => [
HandleLogsDownloaded::class,
],
];
Queue-Based Processing: Wrap the command in a job for async execution:
// app/Console/Commands/DownloadCloudWatchLogsJob.php
use Draw\AwsToolKit\Command\CloudWatchLogsCommand;
class DownloadCloudWatchLogsJob implements ShouldQueue
{
public function handle()
{
$command = new CloudWatchLogsCommand();
$command->handle(
$this->logGroup,
$this->logStream,
$this->outputPath,
$this->fileMode
);
}
}
Dispatch the job from a controller or another command.
Command Guard:
Use --aws-newest-instance-role to ensure a command runs only on the newest instance:
php artisan acme:purge-database --aws-newest-instance-role=prod
Extend your command to support the flag:
// app/Console/Commands/PurgeDatabaseCommand.php
protected $newestInstanceRole = null;
protected function handle()
{
if ($this->newestInstanceRole && !NewestInstanceRole::isNewest($this->newestInstanceRole)) {
$this->info('Skipping command: not the newest instance.');
return;
}
// ... rest of the logic ...
}
Dynamic Role Assignment:
Fetch the instance role dynamically in Laravel's bootstrap/app.php:
$app->singleton(NewestInstanceRole::class, function ($app) {
return new NewestInstanceRole(env('AWS_INSTANCE_ROLE'));
});
Fallback Mechanism: Implement a fallback for when IMDS is unavailable:
// app/Services/NewestInstanceRole.php
public function isNewest(string $role): bool
{
try {
return $this->checkImds($role);
} catch (ImdsException $e) {
$this->logWarning('IMDS unavailable, falling back to manual check.');
return $this->manualCheck($role);
}
}
Bind AWS SDK Client: Register the AWS SDK client in Laravel's service container:
// config/aws.php
'default' => [
'region' => env('AWS_DEFAULT_REGION'),
'version' => 'latest',
'credentials' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
],
];
Bind the client in a service provider:
// app/Providers/AwsServiceProvider.php
public function register()
{
$this->app->singleton(Aws\S3\S3Client::class, function ($app) {
return new Aws\S3\S3Client($app['config']['aws.default']);
});
}
Dependency Injection: Inject the AWS client into your commands or services:
// app/Console/Commands/DownloadCloudWatchLogsCommand.php
public function __construct(
private readonly CloudWatchLogsClient $logsClient
) {}
Mock AWS Services:
Use moto for local AWS mocking in tests:
composer require moto/moto
Example test:
use Moto\MotoMock;
use Moto\Aws\CloudWatchLogs;
public function testCloudWatchLogsDownload()
{
MotoMock::mock([CloudWatchLogs::class]);
$this->artisan('draw:aws:cloud-watch-logs:download', [
'logGroup' => '/aws/rds/test',
'logStream' => 'test-stream',
'instance' => 'test-1',
'output' => './tmp/test.log',
])->assertExitCode(0);
$this->assertFileExists('./tmp/test.log');
}
Unit Test Commands: Test command logic in isolation:
public function testNewestInstanceRole()
{
$command = new NewestInstanceRoleCommand();
$command->setNewestInstanceRole('prod');
$this->assertFalse($command->isNewestInstance());
// Mock IMDS response to return true
$this->partialMock(NewestInstanceRole::class, function ($mock) {
$mock->shouldReceive('isNewest')->andReturn(true);
});
$this->assertTrue($command->isNewestInstance());
}
AWS Credential Conflicts:
fruitcake/laravel-cors.composer.json:
"require": {
"aws/aws-sdk-php": "^3.171",
"guzzlehttp/guzzle": "^7.4"
}
IMDS Unavailability:
--aws-newest-instance-role flag relies on AWS Instance Metadata Service (IMDS), which can be unavailable or slow.public function isNewestInstance(string $role): bool
{
$retries = 3;
$delay = 100; // ms
for ($i = 0; $i < $retries; $i++) {
try {
return $this->checkImds($role);
} catch (ImdsException $e) {
if ($i === $retries - 1) throw $e;
usleep($delay * pow(2, $i));
}
}
}
Log Stream Locking:
use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Component\HttpClient\RetryStrategy;
$client = new RetryableHttpClient(
new RetryStrategy(3, 1000, true, true),
$httpClient
);
File Permission Issues:
chmod -R
How can I help you explore Laravel packages today?