Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Aws Elastic Client Laravel Package

renoki-co/aws-elastic-client

A Laravel-friendly Elasticsearch client for AWS Elasticsearch Service that signs requests with AWS Signature Version 4, letting you authenticate using IAM credentials and use the standard Elasticsearch PHP client against your AWS domain without writing custom request-signing code.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package is: a thin Laravel bridge that wires the official elasticsearch/elasticsearch PHP client to AWS Signature Version 4, using aws/aws-sdk-php's credential provider chain. It lets you talk to Amazon Elasticsearch Service / OpenSearch (and OpenSearch Serverless) clusters that have IAM-based access control, without hand-rolling request signing.

Install and publish:

composer require renoki-co/aws-elastic-client

php artisan vendor:publish --tag=config

Version note for 1.3.0: PHP 7.4 is no longer supported. You need PHP 8.0+ (CI now runs 8.0 and 8.1). If you are still on 7.4, stay on ^1.2 — there is no functional reason to upgrade for the signer itself, only the platform bump and refreshed test matrix.

Minimum configuration — set these in .env:

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=eu-west-1
AWS_ELASTIC_HOST=https://search-my-domain-abcdef.eu-west-1.es.amazonaws.com

Leave the key/secret blank in production if the app runs on EC2, ECS, or Lambda — the SDK's default provider chain will pick up the instance/task role, which is the pattern you actually want. On OpenSearch Serverless, set the service name to aoss instead of es.

First use case: resolve the configured client and do a round-trip to prove signing works before writing any query code.

use RenokiCo\AwsElasticClient\AwsElasticClient;

$client = AwsElasticClient::getClient();

$client->index([
    'index' => 'articles',
    'id'    => 1,
    'body'  => ['title' => 'Hello', 'published_at' => now()->toIso8601String()],
]);

$client->indices()->refresh(['index' => 'articles']);

$result = $client->search([
    'index' => 'articles',
    'body'  => ['query' => ['match' => ['title' => 'Hello']]],
]);

If that returns a hit, the signer, region, and endpoint are all correct. If it returns 403, the problem is IAM policy or resource policy, not the package — see Gotchas.

Where to look first: the published config file (host, region, service, credentials) and the package's service provider, which shows exactly what is bound into the container and under which key. That binding key is what you type-hint or app() in the rest of your app.

Implementation Patterns

Inject the client, don't call the facade everywhere. The package registers a singleton, so resolving it per-request is cheap — but hiding the facade behind your own service or repository keeps AWS specifics out of controllers and makes tests trivial to swap.

final class ArticleSearch
{
    public function __construct(private \Elastic\Elasticsearch\Client $client) {}

    public function index(Article $article): void { /* ... */ }
}

If the concrete client class isn't bound under the interface you want, add your own binding in a service provider that returns AwsElasticClient::getClient(). One line, and your domain code never mentions AWS again.

Treat the client as one connection, not a global. Multi-cluster setups (per-tenant domains, a separate logging cluster, blue/green reindex targets) are common. Build each client from an explicit config array rather than mutating the singleton:

$client = AwsElasticClient::getClient([
    'hosts'   => [$tenant->es_endpoint],
    'region'  => $tenant->aws_region,
    'service' => 'es',
]);

Keep a small factory that memoizes per endpoint so you don't rebuild handlers on every call.

Bulk indexing is the workhorse. For anything beyond single-document writes, use the bulk API with explicit chunking and inspect the response — the bulk endpoint returns HTTP 200 even when individual items failed:

foreach ($models->chunk(500) as $chunk) {
    $response = $client->bulk([
        'body' => $chunk->flatMap(fn ($m) => [
            ['index' => ['_index' => 'articles', '_id' => $m->id]],
            $m->toSearchableArray(),
        ])->all(),
    ]);

    if ($response['errors']) {
        // log $response['items'] where item['index']['status'] >= 300
    }
}

Write through a queue job, not in the request cycle. Elasticsearch latency and AWS throttling shouldn't sit in a user's HTTP request. Dispatch an indexing job on model save; let retries and failed() handle cluster blips. Give the job a $backoff — 429s from AWS need a delay, not an immediate retry.

Local and CI environments. Point AWS_ELASTIC_HOST at a local opensearchproject/opensearch container and skip signing there, or point at a real domain with CI credentials. Pin the container image tag to the same major version as production; the 1.3.0 test-matrix bump is a reminder that client/ES version skew causes subtle query and mapping surprises.

Testing. Bind a fake client in the container in your TestCase and assert on the request body your code builds. For integration tests against a real container, keep them in a separate suite — signed-request tests that depend on live AWS credentials will make your pipeline flaky and slow.

Observability. Wrap the client in a decorator that logs query duration and the X-Opensearch-Request-Id response header from failures. AWS support will ask for that header when you open a ticket about throttling.

Gotchas and Tips

The 1.3.0 bump is a platform break, not an API break. Nothing in the public surface changed; only composer.json's PHP constraint and the CI matrix moved. The practical failure mode is a transitive conflict: if any other dependency pins php: ^7.4, Composer will now fail to resolve. Run composer why-not php 8.0 before upgrading in an older app, and check that your base image / CI runner is actually on 8.0+ — a PHP 7.4 Dockerfile will surface as a confusing build failure, not a clear version error.

"The request signature we calculated does not match" is almost never the package. It's nearly always one of: wrong region; a proxy or load balancer rewriting the Host header; an endpoint URL with a trailing path or port that isn't in the signed canonical request; or clock skew on the host. Verify date -u on the box and compare your configured host string character-for-character with the domain endpoint in the AWS console (no trailing slash).

403 vs 401 matters. A 403 with a signing error is IAM. A 403 with {"Message":"User: ... is not authorized to perform: es:ESHttpPost"} is the identity or resource policy — grant on the domain's access policy and the IAM role, and remember OpenSearch Serverless needs a data access policy, which is a separate concept from an IAM policy. A 401 usually means credentials weren't found at all; check that AWS_ACCESS_KEY_ID isn't set to an empty string in .env, which shadows the instance role rather than falling through to it.

Don't let static credentials reach production. The default credential provider chain is the whole point. Static keys in .env are fine for local dev; on ECS/Lambda they should be absent entirely. Note that the SDK caches resolved credentials, so long-running queue workers will refresh role credentials automatically — but if you ever override the credentials object manually, you own the refresh.

The client is a singleton. If you mutate config at runtime (e.g. switching endpoints for a tenant) after the client has been resolved once, you'll silently keep hitting the old cluster. Rebuild the client explicitly, or make sure the tenant-scoped resolution happens before anything else resolves the singleton in that request.

Timeouts and retries compound. The Elasticsearch client retries failed nodes by default, and the AWS SDK retries its own calls. Against a throttled domain this multiplies request volume. For bulk operations especially, cap retries low and handle 429 at the job level where you control backoff.

Refresh after writes in tests, not in production code paths. Near-real-time indexing means a search immediately after an index may miss the document. Reaching for indices()->refresh() in application code is a latency trap on large indices — do it in tests and in your indexing job's final step only if the UI genuinely requires read-after-write.

Never proxy signed requests from the browser. SigV4 requires the secret key server-side. If you need client-side search UX, expose a narrow, authenticated endpoint of your own — and rate-limit it, because your cluster's access policy won't protect you from an application-layer abuse.

Extension points worth knowing: the package builds an Elasticsearch client with a signing handler pushed onto the handler stack. If you need custom middleware (correlation IDs, request logging, response caching), insert it into that stack rather than wrapping facade calls — you'll capture retries and node failover that a wrapper would miss.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource