Secret Manager & KMS
Plain config goes in env. Credentials go in Secret Manager, which the
service reads while it runs. Data you need to encrypt goes through
Cloud KMS, so your code never holds a key.
Plain config
Section titled “Plain config”Values that aren’t secret go on the service as environment variables:
{ main: import.meta.url, env: { LOG_LEVEL: "info" } }The service reads process.env.LOG_LEVEL.
Create a secret
Section titled “Create a secret”import * as GCP from "@alchemy.run/gcp";
export const ApiKey = GCP.SecretManager.Secret("ApiKey", {});Alchemy creates the secret, not its value. Add the value yourself, so it never passes through your Stack or deploy logs:
printf 'my-key' | gcloud secrets versions add "$SECRET_ID" --data-file=-Return ApiKey.secretId from your Stack to get $SECRET_ID.
Read it per request
Section titled “Read it per request”ReadSecret returns access(), which reads the latest version as a
string:
import * as GCP from "@alchemy.run/gcp";import * as Effect from "effect/Effect";import { HttpServerRequest } from "effect/http/HttpServerRequest";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { ApiKey } from "./resources.ts";
export default class Api extends GCP.Function<Api>()( "Api", { main: import.meta.url, invokerIamDisabled: true }, Effect.gen(function* () { const key = yield* GCP.SecretManager.ReadSecret(ApiKey);
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const expected = yield* key.access();
if (expected === undefined) { return yield* HttpServerResponse.json({ error: "not configured" }, { status: 503 }); } if (request.headers["x-api-key"] !== expected) { return yield* HttpServerResponse.json({ error: "denied" }, { status: 401 }); } return HttpServerResponse.text("welcome"); }).pipe(Effect.orDie), }; }).pipe(Effect.provide(GCP.SecretManager.ReadSecretHttp)),) {}access() returns undefined until a version exists, so the service
can refuse requests instead of letting everyone in. Reading per
request means a new version takes effect without a redeploy.
ReadSecret grants roles/secretmanager.secretAccessor on this secret
only.
Write versions from code
Section titled “Write versions from code”WriteSecret adds, disables, and destroys versions. Use it to rotate a
value from a job:
const secret = yield* GCP.SecretManager.WriteSecret(ApiKey);// inside run:yield* secret.addVersion(crypto.randomUUID());ReadWriteSecret does both.
Encrypt with Cloud KMS
Section titled “Encrypt with Cloud KMS”Create a key ring and a key:
export const Keys = GCP.KMS.KeyRing("Keys", {});
export const DataKey = Effect.gen(function* () { const keys = yield* Keys; return yield* GCP.KMS.CryptoKey("DataKey", { keyRing: keys.name, purpose: "ENCRYPT_DECRYPT", });});Bind Encrypt and Decrypt. The key material never leaves KMS:
const encrypt = yield* GCP.KMS.Encrypt(yield* DataKey);const decrypt = yield* GCP.KMS.Decrypt(yield* DataKey);
// inside fetch:const { ciphertext } = yield* encrypt({ body: { plaintext: btoa("hello") } });const { plaintext } = yield* decrypt({ body: { ciphertext } });Provide GCP.KMS.EncryptHttp and GCP.KMS.DecryptHttp. Each grants
the matching cryptoKeyEncrypter or cryptoKeyDecrypter role on this
key only.
Destroying keys
Section titled “Destroying keys”Cloud KMS has no delete. alchemy destroy schedules every key version
for destruction and leaves the empty key ring, which costs nothing.
Deploying a key with the same id again reuses it with a fresh version.
Example
Section titled “Example”examples/gcp-secrets-kms
is an encryption API gated by a Secret Manager key.
Where next
Section titled “Where next”- IAM & bindings — exactly which role each binding grants.
- Secrets —
.envvalues and Config. - SecretManager reference — every prop.