Skip to content

Federated identity

A runtime on one cloud often needs a resource on the other: a Lambda publishing to a Pub/Sub topic, or a Cloud Run service sending to an SQS queue. When the runtime already runs as an identity of its own cloud, Alchemy lets the other cloud trust that identity. There is no service account key or access key to create, store, or rotate.

You write the binding the same way as on a native runtime. Federation is what Alchemy picks for you.

Identity Federation covers how federation works across every cloud.

A Lambda runs as an IAM role. Bind a Pub/Sub topic to it like you would on Cloud Run:

src/worker.ts
import * as AWS from "@alchemy.run/aws";
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Events = GCP.PubSub.Topic("Events", {});
export default class Worker extends AWS.Lambda.Function<Worker>()(
"Worker",
{ main: import.meta.url, functionUrl: true },
Effect.gen(function* () {
const publish = yield* GCP.PubSub.Publish(yield* Events);
return {
fetch: publish({ body: { messages: [{ data: btoa("hello") }] } }).pipe(
Effect.as(HttpServerResponse.text("published")),
Effect.orDie,
),
};
}).pipe(Effect.provide(GCP.PubSub.PublishHttp)),
) {}

The binding creates a service account for the Lambda and grants it the publisher role on the topic, as it would for any runtime. Instead of a key, the Lambda’s role may impersonate that account.

The trust goes through a workload identity pool with an AWS provider for your account. The stack shares one pool, so every AWS runtime in the stack uses it. The provider maps each caller to the role it assumed, and the account grants roles/iam.workloadIdentityUser to that one role:

principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/<pool>/attribute.aws_role/arn:aws:sts::111122223333:assumed-role/<role>
LambdaIAM roleGoogle STSworkload identity poolAWS STSIAM CredentialsGoogleEventsPub/Sub topicsigned GetCallerIdentityreplay the requestcaller: the Lambda's rolefederated tokenimpersonate the service accountaccess token · 1 hourpublish
The Lambda proves who it is with its own AWS credentials, and leaves with a short-lived Google token for the service account the binding created. No key is stored anywhere.

At runtime the Lambda signs an STS GetCallerIdentity request with its own role credentials and sends it to Google STS. Google replays the request against AWS to learn which role is calling, and returns a federated token. The Lambda trades that for the service account’s token, which lasts an hour and is exchanged again before it expires.

A Fly Service runs on Fly Machines, and every Machine can mint an OIDC token signed by https://oidc.fly.io/<org>. Bind the topic the same way:

src/api.ts
import * as Fly from "@alchemy.run/fly";
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Events = GCP.PubSub.Topic("Events", {});
export default class Api extends Fly.Service<Api>()(
"Api",
{ main: import.meta.url, port: 3000 },
Effect.gen(function* () {
const publish = yield* GCP.PubSub.Publish(yield* Events);
return {
fetch: publish({ body: { messages: [{ data: btoa("hello") }] } }).pipe(
Effect.as(HttpServerResponse.text("published")),
Effect.orDie,
),
};
}).pipe(Effect.provide(GCP.PubSub.PublishHttp)),
) {}

The stack gets a second pool, whose provider trusts Fly’s issuer for your organization. The provider maps each token to its App, and the account grants roles/iam.workloadIdentityUser to that one App:

principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/<pool>/attribute.fly_app/<app>

At runtime the Machine asks its /.fly/api socket for a token addressed to the provider and sends it to Google STS, then trades the federated token for the service account’s token. The socket answers only root, the default user in the images Alchemy builds. See Fly federated identity for the tokens themselves.

A Cloud Run service runs as a Google service account. Bind an SQS queue to it:

src/api.ts
import * as AWS from "@alchemy.run/aws";
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Jobs = AWS.SQS.Queue("Jobs");
export default class Api extends GCP.Run.Service<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const send = yield* AWS.SQS.SendMessage(Jobs);
return {
fetch: send({ MessageBody: "hello" }).pipe(
Effect.as(HttpServerResponse.text("sent")),
Effect.orDie,
),
};
}).pipe(Effect.provide(AWS.SQS.SendMessageHttp)),
) {}

The binding creates an IAM role that trusts Google’s ID tokens for the service’s runtime account, matched by the account’s unique ID. At runtime the service asks its metadata server for an ID token and trades it for the role’s credentials with AssumeRoleWithWebIdentity. The credentials last an hour and are assumed again, with a fresh token, shortly before they expire.

Lambda functions, ECS tasks and services, EC2 instances and launch templates, App Runner services, Batch jobs, EKS pods, and Fly Services reach GCP this way. Cloud Run services, jobs, and worker pools, Cloud Functions, and Fly Services reach AWS this way.

Runtimes without an identity the other cloud can trust still get a key: a Cloudflare Worker, a Railway service, a Fly Sprite, and a GKE pod calling AWS. The key belongs to an identity that may do only what that one host needs. The binding code is the same either way.

A federated grant is not usable right away. A grant to a workload identity principal can take a few minutes to land, and until it does the Lambda’s calls fail with iam.serviceAccounts.getAccessToken denied. Retry the first call after a deploy.

See IAM & bindings for the roles each binding grants.