t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/AWS/ECS/ServicePhase2.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

275 lines
11 KiB
TypeScript

import * as AWS from "@/AWS";
import { Cluster } from "@/AWS/ECS/Cluster.ts";
import { Service } from "@/AWS/ECS/Service.ts";
import * as Test from "@/Test/Alchemy";
import * as aas from "@distilled.cloud/aws/application-auto-scaling";
import * as logs from "@distilled.cloud/aws/cloudwatch-logs";
import * as ec2 from "@distilled.cloud/aws/ec2";
import * as elbv2 from "@distilled.cloud/aws/elastic-load-balancing-v2";
import { expect } from "alchemy-test";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Redacted from "effect/Redacted";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import { getDefaultVpcNetwork } from "../DefaultVpc.ts";
const { test } = Test.make({ providers: AWS.providers() });
class RouteMismatch extends Data.TaggedError("RouteMismatch")<{
readonly path: string;
readonly status: number;
readonly body: string;
}> {}
// The container compares its secret-sourced env vars against the KNOWN seed
// values and serves PRESENCE booleans (never the values) plus a /health
// route via busybox httpd.
//
// NOTE: an Effect-native fixture (`main:` + `{ fetch }`) would be the
// preferred shape here, but the Platform class form with Effect-valued
// props (needed to reference the cluster/parameter/secret) currently OOMs
// during construction — a pre-existing Platform.ts bug independent of the
// props under test. The external `image:` form covers the same Phase-2
// surface: `secrets`, `loadBalancer.health`, `scaling`, and
// `logging.retention` are all provider/composition-level features.
const SERVE_SCRIPT = [
"mkdir -p /www",
`if [ "$PHASE2_PARAM" = "phase2-parameter-value" ] && [ "$PHASE2_SECRET" = "phase2-secret-value" ]; then echo '{"param":true,"secret":true}' > /www/secrets; else echo '{"param":false,"secret":false}' > /www/secrets; fi`,
"echo ok > /www/health",
"exec httpd -f -p 3000 -h /www",
].join(" && ");
// Phase-2 kitchen sink: one service exercising `secrets` (SSM parameter +
// Secrets Manager secret injected via valueFrom), `loadBalancer.health`
// (per-TG override to /health), `scaling` (CPU + ALB request-count target
// tracking; asserted by describing the scalable target + policies, NOT by
// waiting for a scale event), and `logging.retention` — with a live HTTP
// round-trip over the owned ALB proving the container runs, the health
// override passes, and the secrets arrived (presence only).
//
// Requires a local Docker daemon (the `image:` source mirrors into ECR).
test.provider.skipIf(!!process.env.FAST)(
"phase2 service wires secrets, health overrides, scaling, and log retention",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const net = yield* getDefaultVpcNetwork;
const client = yield* HttpClient.HttpClient;
// Only the per-AZ default subnets: concurrent suites create extra
// subnets, and an ALB rejects two subnets in one AZ.
const azSubnets = yield* ec2
.describeSubnets({
Filters: [
{ Name: "vpc-id", Values: [net.vpcId] },
{ Name: "default-for-az", Values: ["true"] },
],
})
.pipe(
Effect.map((r) =>
(r.Subnets ?? []).flatMap((s) => (s.SubnetId ? [s.SubnetId] : [])),
),
);
const deployed = yield* stack.deploy(
Effect.gen(function* () {
const cluster = yield* Cluster("Phase2Cluster", {
clusterName: "alchemy-test-ecs-phase2",
});
const parameter = yield* AWS.SSM.Parameter("Phase2Param", {
value: "phase2-parameter-value",
});
const secret = yield* AWS.SecretsManager.Secret("Phase2Secret", {
secretString: Redacted.make("phase2-secret-value"),
});
const service = yield* Service("Phase2Api", {
cluster,
image: "busybox:stable",
command: ["sh", "-c", SERVE_SCRIPT],
port: 3000,
desiredCount: 1,
// Run on ARM64 so the locally-cached (Apple Silicon) busybox the
// mirror pushes matches the Fargate runtime architecture.
runtimePlatform: {
cpuArchitecture: "ARM64",
operatingSystemFamily: "LINUX",
},
vpcId: net.vpcId as string,
subnets: azSubnets,
// Default-VPC public subnets: tasks need a public IP to pull
// the mirrored image from ECR without a NAT.
assignPublicIp: true,
loadBalancer: {
rules: [{ listen: "80/http" }],
health: {
"3000/http": {
path: "/health",
interval: "15 seconds",
successCodes: "200",
},
},
},
scaling: {
min: 1,
max: 2,
cpuUtilization: 70,
requestCount: 500,
scaleInCooldown: "2 minutes",
},
secrets: {
PHASE2_PARAM: parameter.parameterArn,
PHASE2_SECRET: secret.secretArn,
},
logging: { retention: "3 days" },
});
return {
url: service.url.as<string>(),
serviceName: service.serviceName.as<string>(),
clusterArn: service.clusterArn.as<string>(),
loadBalancerArn: service.loadBalancerArn.as<string>(),
targetGroupArn: service.targetGroupArn.as<string>(),
logGroupName: service.logGroupName.as<string>(),
};
}),
);
expect(deployed.url).toMatch(/^http:\/\//);
// ── live round-trip: /health (the overridden TG path) + /secrets ──
const fetchRoute = (path: string) =>
client
.get(`${deployed.url}${path}`)
.pipe(
Effect.flatMap((res) =>
Effect.map(res.text, (body) => ({ status: res.status, body })),
),
);
const awaitRoute = (path: string, predicate: (body: string) => boolean) =>
fetchRoute(path).pipe(
Effect.flatMap(({ status, body }) =>
status === 200 && predicate(body)
? Effect.succeed(body)
: Effect.fail(new RouteMismatch({ path, status, body })),
),
// ALB provisioning (~2 min) + image mirror + Fargate start.
Effect.retry({ schedule: Schedule.spaced("5 seconds"), times: 60 }),
);
yield* awaitRoute("/health", (body) => body.includes("ok"));
const secretsBody = yield* awaitRoute("/secrets", (body) =>
body.includes("true"),
);
const seen = JSON.parse(secretsBody) as {
param: boolean;
secret: boolean;
};
expect(seen.param).toBe(true);
expect(seen.secret).toBe(true);
// The secret VALUES never leave the container.
expect(secretsBody).not.toContain("phase2-parameter-value");
expect(secretsBody).not.toContain("phase2-secret-value");
// ── per-TG health override landed on the composed target group ────
const targetGroups = yield* elbv2.describeTargetGroups({
TargetGroupArns: [deployed.targetGroupArn],
});
const targetGroup = targetGroups.TargetGroups?.[0];
expect(targetGroup?.HealthCheckPath).toBe("/health");
expect(targetGroup?.HealthCheckIntervalSeconds).toBe(15);
expect(targetGroup?.Matcher?.HttpCode).toBe("200");
// ── scaling: scalable target (min/max) + one policy per metric ────
const resourceId = `service/alchemy-test-ecs-phase2/${deployed.serviceName}`;
const targets = yield* aas.describeScalableTargets({
ServiceNamespace: "ecs",
ResourceIds: [resourceId],
});
const scalableTarget = targets.ScalableTargets?.[0];
expect(scalableTarget?.MinCapacity).toBe(1);
expect(scalableTarget?.MaxCapacity).toBe(2);
const policies = yield* aas.describeScalingPolicies({
ServiceNamespace: "ecs",
ResourceId: resourceId,
});
const trackingConfigs = (policies.ScalingPolicies ?? []).map(
(policy) => policy.TargetTrackingScalingPolicyConfiguration,
);
expect(trackingConfigs.length).toBe(2);
const cpuPolicy = trackingConfigs.find(
(config) =>
config?.PredefinedMetricSpecification?.PredefinedMetricType ===
"ECSServiceAverageCPUUtilization",
);
expect(cpuPolicy?.TargetValue).toBe(70);
expect(cpuPolicy?.ScaleInCooldown).toBe(120);
const requestPolicy = trackingConfigs.find(
(config) =>
config?.PredefinedMetricSpecification?.PredefinedMetricType ===
"ALBRequestCountPerTarget",
);
expect(requestPolicy?.TargetValue).toBe(500);
// ResourceLabel = app/{lb-name}/{lb-id}/targetgroup/{tg-name}/{tg-id}.
const expectedLabel = `${deployed.loadBalancerArn.slice(
deployed.loadBalancerArn.indexOf("loadbalancer/") +
"loadbalancer/".length,
)}/${deployed.targetGroupArn.slice(
deployed.targetGroupArn.indexOf("targetgroup/"),
)}`;
expect(requestPolicy?.PredefinedMetricSpecification?.ResourceLabel).toBe(
expectedLabel,
);
// ── logging.retention on the auto-created log group ───────────────
const logGroups = yield* logs.describeLogGroups({
logGroupNamePrefix: deployed.logGroupName,
});
const logGroup = (logGroups.logGroups ?? []).find(
(group) => group.logGroupName === deployed.logGroupName,
);
expect(logGroup?.retentionInDays).toBe(3);
// ── destroy + zero-orphan proofs ──────────────────────────────────
yield* stack.destroy();
const targetsAfter = yield* aas.describeScalableTargets({
ServiceNamespace: "ecs",
ResourceIds: [resourceId],
});
expect((targetsAfter.ScalableTargets ?? []).length).toBe(0);
const lbGone = yield* elbv2
.describeLoadBalancers({
LoadBalancerArns: [deployed.loadBalancerArn],
})
.pipe(
Effect.map((r) => (r.LoadBalancers ?? []).length === 0),
Effect.catchTag("LoadBalancerNotFoundException", () =>
Effect.succeed(true),
),
);
expect(lbGone).toBe(true);
const logGroupsAfter = yield* logs.describeLogGroups({
logGroupNamePrefix: deployed.logGroupName,
});
expect(
(logGroupsAfter.logGroups ?? []).some(
(group) => group.logGroupName === deployed.logGroupName,
),
).toBe(false);
}),
{
tags: [
"provider:aws",
"provider:aws:applicationautoscaling",
"provider:aws:ec2",
"provider:aws:ecs",
"provider:aws:secretsmanager",
"provider:aws:ssm",
"live",
],
timeout: 900_000,
},
);