@intentius/chant-lexicon-aws 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/codegen/docs.ts +42 -0
- package/src/composites/alb-shared.ts +117 -0
- package/src/composites/composites.test.ts +193 -0
- package/src/composites/ecs-trust-policy.ts +10 -0
- package/src/composites/fargate-alb.ts +1 -11
- package/src/composites/fargate-service.ts +233 -0
- package/src/composites/index.ts +4 -0
- package/src/index.ts +2 -2
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "xxhash64",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "d04788f6043e2c1d",
|
|
5
5
|
"meta.json": "db2d39f47dd623e2",
|
|
6
6
|
"types/index.d.ts": "8ae8b196e3f4585d",
|
|
7
7
|
"rules/s3-encryption.ts": "678cd3d490eae38",
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"rules/waw025.ts": "5929f7e3e3e3b859",
|
|
33
33
|
"skills/chant-aws.md": "de8d9ccfe4dcf4bc"
|
|
34
34
|
},
|
|
35
|
-
"composite": "
|
|
35
|
+
"composite": "bccf35bdfbacd863"
|
|
36
36
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": ["src/", "dist/"],
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prepack": "bun run bundle && bun run validate"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@intentius/chant": "0.0.
|
|
25
|
+
"@intentius/chant": "0.0.10",
|
|
26
26
|
"fflate": "^0.8.2",
|
|
27
27
|
"js-yaml": "^4.1.0"
|
|
28
28
|
},
|
package/src/codegen/docs.ts
CHANGED
|
@@ -405,6 +405,8 @@ The AWS lexicon ships ready-to-use composites for common patterns. Import them f
|
|
|
405
405
|
| \`LambdaSns\` | \`topic\`, \`role\`, \`func\`, \`subscription\`, \`permission\` | SNS Topic + Lambda via Subscription. Auto-attaches invoke permission for SNS. |
|
|
406
406
|
| \`VpcDefault\` | \`vpc\`, \`igw\`, \`igwAttachment\`, \`publicSubnet1\`, \`publicSubnet2\`, \`privateSubnet1\`, \`privateSubnet2\`, \`publicRouteTable\`, \`publicRoute\`, \`publicRta1\`, \`publicRta2\`, \`privateRouteTable\`, \`privateRta1\`, \`privateRta2\`, \`natEip\`, \`natGateway\`, \`privateRoute\` | Production-ready VPC: 2 public + 2 private subnets across 2 AZs, internet gateway, single NAT gateway. |
|
|
407
407
|
| \`FargateAlb\` | \`cluster\`, \`executionRole\`, \`taskRole\`, \`logGroup\`, \`taskDef\`, \`albSg\`, \`taskSg\`, \`alb\`, \`targetGroup\`, \`listener\`, \`service\` | Fargate service behind an ALB. Accepts VPC outputs as props. |
|
|
408
|
+
| \`AlbShared\` | \`cluster\`, \`executionRole\`, \`albSg\`, \`alb\`, \`listener\` | Shared ALB infrastructure (ECS cluster, execution role, ALB, listener with 404 default). Created once, consumed by multiple \`FargateService\` instances. |
|
|
409
|
+
| \`FargateService\` | \`taskRole\`, \`logGroup\`, \`taskDef\`, \`taskSg\`, \`targetGroup\`, \`rule\`, \`service\` | Per-service Fargate resources with listener rule routing. Wire to an \`AlbShared\` instance for multi-service ALB patterns. |
|
|
408
410
|
|
|
409
411
|
All built-in composites accept \`ManagedPolicyArns\` and \`Policies\` for adding IAM permissions to the auto-created role.
|
|
410
412
|
|
|
@@ -888,6 +890,46 @@ Produces 17 CloudFormation resources: VPC, Internet Gateway, 2 public + 2 privat
|
|
|
888
890
|
|
|
889
891
|
Produces 28 CloudFormation resources: 17 from VpcDefault + 11 from FargateAlb (ECS Cluster, execution/task roles, log group, task definition, security groups, ALB, target group, listener, and ECS service).
|
|
890
892
|
|
|
893
|
+
## Multi-Service ALB
|
|
894
|
+
|
|
895
|
+
\`examples/multi-service-alb/\` — multiple Fargate services behind a single shared ALB using \`AlbShared\` + \`FargateService\`.
|
|
896
|
+
|
|
897
|
+
{{file:multi-service-alb/src/shared.ts}}
|
|
898
|
+
|
|
899
|
+
{{file:multi-service-alb/src/services.ts}}
|
|
900
|
+
|
|
901
|
+
Produces 36 CloudFormation resources: 17 from VpcDefault + 5 from AlbShared + 7×2 from FargateService (task role, log group, task definition, task security group, target group, listener rule, and ECS service per service).
|
|
902
|
+
|
|
903
|
+
## Shared ALB (Separate Projects)
|
|
904
|
+
|
|
905
|
+
\`examples/shared-alb/\`, \`examples/shared-alb-api/\`, \`examples/shared-alb-ui/\` — the same multi-service ALB pattern as above, but split across separate CloudFormation stacks for independent deployment.
|
|
906
|
+
|
|
907
|
+
### Infra stack
|
|
908
|
+
|
|
909
|
+
The shared-alb stack contains VPC, ALB, ECS cluster, and ECR repositories. It exports outputs that service stacks consume as parameters:
|
|
910
|
+
|
|
911
|
+
{{file:shared-alb/src/alb.ts}}
|
|
912
|
+
|
|
913
|
+
{{file:shared-alb/src/ecr.ts}}
|
|
914
|
+
|
|
915
|
+
{{file:shared-alb/src/outputs.ts}}
|
|
916
|
+
|
|
917
|
+
### Service stacks
|
|
918
|
+
|
|
919
|
+
Each service stack receives shared infrastructure as parameters and deploys a single Fargate service:
|
|
920
|
+
|
|
921
|
+
{{file:shared-alb-api/src/params.ts}}
|
|
922
|
+
|
|
923
|
+
{{file:shared-alb-api/src/service.ts}}
|
|
924
|
+
|
|
925
|
+
**Deployment pattern:**
|
|
926
|
+
|
|
927
|
+
1. Deploy the infra stack first — creates VPC, ALB, ECS cluster, and ECR repos
|
|
928
|
+
2. Deploy each service stack independently with \`--parameter-overrides\` mapping infra outputs to service parameters
|
|
929
|
+
3. Each service gets its own \`image\` parameter for CI/CD pipelines to inject the container image URI
|
|
930
|
+
|
|
931
|
+
The separate-project pattern enables independent team ownership and deployment cadences. See the [GitLab CI/CD lexicon examples](/chant/lexicons/gitlab/examples/) for pipeline definitions that automate this workflow.
|
|
932
|
+
|
|
891
933
|
## Lambda API (Custom Composite)
|
|
892
934
|
|
|
893
935
|
\`examples/lambda-api/\` — demonstrates building your own composite factory with presets and a custom lint rule. This is the only example that teaches custom composite authoring.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Composite } from "@intentius/chant";
|
|
2
|
+
import {
|
|
3
|
+
EcsCluster,
|
|
4
|
+
LoadBalancer,
|
|
5
|
+
Listener,
|
|
6
|
+
Listener_Action,
|
|
7
|
+
Listener_FixedResponseConfig,
|
|
8
|
+
Listener_Certificate,
|
|
9
|
+
SecurityGroup,
|
|
10
|
+
SecurityGroup_Ingress,
|
|
11
|
+
Role,
|
|
12
|
+
Role_Policy,
|
|
13
|
+
} from "../generated";
|
|
14
|
+
import { ECRActions } from "../actions/ecr";
|
|
15
|
+
import { LogsActions } from "../actions/logs";
|
|
16
|
+
import { ecsTrustPolicy } from "./ecs-trust-policy";
|
|
17
|
+
|
|
18
|
+
export interface AlbSharedProps {
|
|
19
|
+
vpcId: string;
|
|
20
|
+
publicSubnetIds: string[];
|
|
21
|
+
listenerPort?: number;
|
|
22
|
+
protocol?: "HTTP" | "HTTPS";
|
|
23
|
+
certificateArn?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const AlbShared = Composite<AlbSharedProps>((props) => {
|
|
27
|
+
const listenerPort = props.listenerPort ?? 80;
|
|
28
|
+
const protocol = props.protocol ?? "HTTP";
|
|
29
|
+
|
|
30
|
+
// ECS Cluster
|
|
31
|
+
const cluster = new EcsCluster({});
|
|
32
|
+
|
|
33
|
+
// Execution role — ECR pull + CloudWatch Logs write
|
|
34
|
+
const executionPolicyDocument = {
|
|
35
|
+
Version: "2012-10-17",
|
|
36
|
+
Statement: [
|
|
37
|
+
{
|
|
38
|
+
Effect: "Allow",
|
|
39
|
+
Action: ECRActions.Pull,
|
|
40
|
+
Resource: "*",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
Effect: "Allow",
|
|
44
|
+
Action: LogsActions.Write,
|
|
45
|
+
Resource: "*",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const executionPolicy = new Role_Policy({
|
|
51
|
+
PolicyName: "ExecutionPolicy",
|
|
52
|
+
PolicyDocument: executionPolicyDocument,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const executionRole = new Role({
|
|
56
|
+
AssumeRolePolicyDocument: ecsTrustPolicy,
|
|
57
|
+
Policies: [executionPolicy],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ALB security group — ingress on listener port from anywhere
|
|
61
|
+
const albIngress = new SecurityGroup_Ingress({
|
|
62
|
+
IpProtocol: "tcp",
|
|
63
|
+
FromPort: listenerPort,
|
|
64
|
+
ToPort: listenerPort,
|
|
65
|
+
CidrIp: "0.0.0.0/0",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const albSg = new SecurityGroup({
|
|
69
|
+
GroupDescription: "ALB security group",
|
|
70
|
+
VpcId: props.vpcId,
|
|
71
|
+
SecurityGroupIngress: [albIngress],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Application Load Balancer
|
|
75
|
+
const alb = new LoadBalancer({
|
|
76
|
+
Type: "application",
|
|
77
|
+
Scheme: "internet-facing",
|
|
78
|
+
Subnets: props.publicSubnetIds,
|
|
79
|
+
SecurityGroups: [albSg.GroupId],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Listener — default action is fixed-response 404
|
|
83
|
+
const fixedResponse = new Listener_FixedResponseConfig({
|
|
84
|
+
StatusCode: "404",
|
|
85
|
+
ContentType: "text/plain",
|
|
86
|
+
MessageBody: "Not Found",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const defaultAction = new Listener_Action({
|
|
90
|
+
Type: "fixed-response",
|
|
91
|
+
FixedResponseConfig: fixedResponse,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const listenerProps: Record<string, unknown> = {
|
|
95
|
+
LoadBalancerArn: alb.LoadBalancerArn,
|
|
96
|
+
Port: listenerPort,
|
|
97
|
+
Protocol: protocol,
|
|
98
|
+
DefaultActions: [defaultAction],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (protocol === "HTTPS" && props.certificateArn) {
|
|
102
|
+
const cert = new Listener_Certificate({
|
|
103
|
+
CertificateArn: props.certificateArn,
|
|
104
|
+
});
|
|
105
|
+
listenerProps.Certificates = [cert];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const listener = new Listener(listenerProps);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
cluster,
|
|
112
|
+
executionRole,
|
|
113
|
+
albSg,
|
|
114
|
+
alb,
|
|
115
|
+
listener,
|
|
116
|
+
};
|
|
117
|
+
}, "AlbShared");
|
|
@@ -11,6 +11,8 @@ import { LambdaS3 } from "./lambda-s3";
|
|
|
11
11
|
import { LambdaSns } from "./lambda-sns";
|
|
12
12
|
import { VpcDefault } from "./vpc-default";
|
|
13
13
|
import { FargateAlb } from "./fargate-alb";
|
|
14
|
+
import { AlbShared } from "./alb-shared";
|
|
15
|
+
import { FargateService } from "./fargate-service";
|
|
14
16
|
|
|
15
17
|
const baseProps = {
|
|
16
18
|
name: "TestFunc",
|
|
@@ -440,3 +442,194 @@ describe("FargateAlb", () => {
|
|
|
440
442
|
expect(ingress.ToPort).toBe(8080);
|
|
441
443
|
});
|
|
442
444
|
});
|
|
445
|
+
|
|
446
|
+
describe("AlbShared", () => {
|
|
447
|
+
const sharedProps = {
|
|
448
|
+
vpcId: "vpc-123",
|
|
449
|
+
publicSubnetIds: ["subnet-pub1", "subnet-pub2"],
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
test("returns 5 members with correct names", () => {
|
|
453
|
+
const instance = AlbShared(sharedProps);
|
|
454
|
+
const names = Object.keys(instance.members);
|
|
455
|
+
expect(names).toEqual(["cluster", "executionRole", "albSg", "alb", "listener"]);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("expandComposite produces correct logical names", () => {
|
|
459
|
+
const expanded = expandComposite("shared", AlbShared(sharedProps));
|
|
460
|
+
expect(expanded.has("sharedCluster")).toBe(true);
|
|
461
|
+
expect(expanded.has("sharedExecutionRole")).toBe(true);
|
|
462
|
+
expect(expanded.has("sharedAlbSg")).toBe(true);
|
|
463
|
+
expect(expanded.has("sharedAlb")).toBe(true);
|
|
464
|
+
expect(expanded.has("sharedListener")).toBe(true);
|
|
465
|
+
expect(expanded.size).toBe(5);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("listener default action is fixed-response 404", () => {
|
|
469
|
+
const instance = AlbShared(sharedProps);
|
|
470
|
+
const listenerProps = (instance.listener as any).props;
|
|
471
|
+
expect(listenerProps.DefaultActions).toHaveLength(1);
|
|
472
|
+
const action = (listenerProps.DefaultActions[0] as any).props;
|
|
473
|
+
expect(action.Type).toBe("fixed-response");
|
|
474
|
+
const fixedResponse = (action.FixedResponseConfig as any).props;
|
|
475
|
+
expect(fixedResponse.StatusCode).toBe("404");
|
|
476
|
+
expect(fixedResponse.ContentType).toBe("text/plain");
|
|
477
|
+
expect(fixedResponse.MessageBody).toBe("Not Found");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("ALB SG allows ingress on listener port (default 80)", () => {
|
|
481
|
+
const instance = AlbShared(sharedProps);
|
|
482
|
+
const sgProps = (instance.albSg as any).props;
|
|
483
|
+
expect(sgProps.SecurityGroupIngress).toHaveLength(1);
|
|
484
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
485
|
+
expect(ingress.FromPort).toBe(80);
|
|
486
|
+
expect(ingress.ToPort).toBe(80);
|
|
487
|
+
expect(ingress.CidrIp).toBe("0.0.0.0/0");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("custom listener port is applied", () => {
|
|
491
|
+
const instance = AlbShared({ ...sharedProps, listenerPort: 443 });
|
|
492
|
+
const sgProps = (instance.albSg as any).props;
|
|
493
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
494
|
+
expect(ingress.FromPort).toBe(443);
|
|
495
|
+
expect(ingress.ToPort).toBe(443);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("execution role has ECR + Logs policies", () => {
|
|
499
|
+
const instance = AlbShared(sharedProps);
|
|
500
|
+
const roleProps = (instance.executionRole as any).props;
|
|
501
|
+
expect(roleProps.Policies).toHaveLength(1);
|
|
502
|
+
const policyDoc = (roleProps.Policies[0] as any).props.PolicyDocument;
|
|
503
|
+
expect(policyDoc.Statement).toHaveLength(2);
|
|
504
|
+
expect(policyDoc.Statement[0].Action).toContain("ecr:GetAuthorizationToken");
|
|
505
|
+
expect(policyDoc.Statement[1].Action).toContain("logs:CreateLogStream");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("HTTPS adds certificate to listener", () => {
|
|
509
|
+
const instance = AlbShared({
|
|
510
|
+
...sharedProps,
|
|
511
|
+
protocol: "HTTPS",
|
|
512
|
+
certificateArn: "arn:aws:acm:us-east-1:123:certificate/abc",
|
|
513
|
+
});
|
|
514
|
+
const listenerProps = (instance.listener as any).props;
|
|
515
|
+
expect(listenerProps.Protocol).toBe("HTTPS");
|
|
516
|
+
expect(listenerProps.Certificates).toHaveLength(1);
|
|
517
|
+
const cert = (listenerProps.Certificates[0] as any).props;
|
|
518
|
+
expect(cert.CertificateArn).toBe("arn:aws:acm:us-east-1:123:certificate/abc");
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe("FargateService", () => {
|
|
523
|
+
const serviceProps = {
|
|
524
|
+
clusterArn: "arn:aws:ecs:us-east-1:123:cluster/my-cluster",
|
|
525
|
+
listenerArn: "arn:aws:elasticloadbalancing:us-east-1:123:listener/app/my-alb/abc/def",
|
|
526
|
+
albSecurityGroupId: "sg-alb123",
|
|
527
|
+
executionRoleArn: "arn:aws:iam::123:role/execution-role",
|
|
528
|
+
image: "nginx:latest",
|
|
529
|
+
priority: 100,
|
|
530
|
+
pathPatterns: ["/api/*"],
|
|
531
|
+
vpcId: "vpc-123",
|
|
532
|
+
privateSubnetIds: ["subnet-priv1", "subnet-priv2"],
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
test("returns 7 members with correct names", () => {
|
|
536
|
+
const instance = FargateService(serviceProps);
|
|
537
|
+
const names = Object.keys(instance.members);
|
|
538
|
+
expect(names).toEqual(["taskRole", "logGroup", "taskDef", "taskSg", "targetGroup", "rule", "service"]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("expandComposite produces correct logical names", () => {
|
|
542
|
+
const expanded = expandComposite("api", FargateService(serviceProps));
|
|
543
|
+
expect(expanded.has("apiTaskRole")).toBe(true);
|
|
544
|
+
expect(expanded.has("apiLogGroup")).toBe(true);
|
|
545
|
+
expect(expanded.has("apiTaskDef")).toBe(true);
|
|
546
|
+
expect(expanded.has("apiTaskSg")).toBe(true);
|
|
547
|
+
expect(expanded.has("apiTargetGroup")).toBe(true);
|
|
548
|
+
expect(expanded.has("apiRule")).toBe(true);
|
|
549
|
+
expect(expanded.has("apiService")).toBe(true);
|
|
550
|
+
expect(expanded.size).toBe(7);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("ListenerRule has correct priority and path conditions", () => {
|
|
554
|
+
const instance = FargateService(serviceProps);
|
|
555
|
+
const ruleProps = (instance.rule as any).props;
|
|
556
|
+
expect(ruleProps.Priority).toBe(100);
|
|
557
|
+
expect(ruleProps.Conditions).toHaveLength(1);
|
|
558
|
+
const condition = (ruleProps.Conditions[0] as any).props;
|
|
559
|
+
expect(condition.Field).toBe("path-pattern");
|
|
560
|
+
const pathConfig = (condition.PathPatternConfig as any).props;
|
|
561
|
+
expect(pathConfig.Values).toEqual(["/api/*"]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("host-header routing produces HostHeaderConfig condition", () => {
|
|
565
|
+
const instance = FargateService({
|
|
566
|
+
...serviceProps,
|
|
567
|
+
pathPatterns: undefined,
|
|
568
|
+
hostHeaders: ["api.example.com"],
|
|
569
|
+
});
|
|
570
|
+
const ruleProps = (instance.rule as any).props;
|
|
571
|
+
expect(ruleProps.Conditions).toHaveLength(1);
|
|
572
|
+
const condition = (ruleProps.Conditions[0] as any).props;
|
|
573
|
+
expect(condition.Field).toBe("host-header");
|
|
574
|
+
const hostConfig = (condition.HostHeaderConfig as any).props;
|
|
575
|
+
expect(hostConfig.Values).toEqual(["api.example.com"]);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("combined path + host conditions both present", () => {
|
|
579
|
+
const instance = FargateService({
|
|
580
|
+
...serviceProps,
|
|
581
|
+
hostHeaders: ["api.example.com"],
|
|
582
|
+
});
|
|
583
|
+
const ruleProps = (instance.rule as any).props;
|
|
584
|
+
expect(ruleProps.Conditions).toHaveLength(2);
|
|
585
|
+
const fields = ruleProps.Conditions.map((c: any) => c.props.Field);
|
|
586
|
+
expect(fields).toContain("path-pattern");
|
|
587
|
+
expect(fields).toContain("host-header");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("task SG references ALB SG via SourceSecurityGroupId", () => {
|
|
591
|
+
const instance = FargateService(serviceProps);
|
|
592
|
+
const sgProps = (instance.taskSg as any).props;
|
|
593
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
594
|
+
expect(ingress.SourceSecurityGroupId).toBe("sg-alb123");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("service has DependsOn on rule", () => {
|
|
598
|
+
const instance = FargateService(serviceProps);
|
|
599
|
+
const serviceAttributes = (instance.service as any).attributes;
|
|
600
|
+
expect(serviceAttributes.DependsOn).toBeDefined();
|
|
601
|
+
expect(serviceAttributes.DependsOn).toContain(instance.rule);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("throws if neither pathPatterns nor hostHeaders provided", () => {
|
|
605
|
+
expect(() =>
|
|
606
|
+
FargateService({
|
|
607
|
+
...serviceProps,
|
|
608
|
+
pathPatterns: undefined,
|
|
609
|
+
}),
|
|
610
|
+
).toThrow("FargateService requires at least one of pathPatterns or hostHeaders");
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("throws if priority is out of range", () => {
|
|
614
|
+
expect(() =>
|
|
615
|
+
FargateService({ ...serviceProps, priority: 0 }),
|
|
616
|
+
).toThrow("FargateService priority must be between 1 and 50000");
|
|
617
|
+
expect(() =>
|
|
618
|
+
FargateService({ ...serviceProps, priority: 50001 }),
|
|
619
|
+
).toThrow("FargateService priority must be between 1 and 50000");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("default values (cpu, memory, containerPort, desiredCount)", () => {
|
|
623
|
+
const instance = FargateService(serviceProps);
|
|
624
|
+
const tdProps = (instance.taskDef as any).props;
|
|
625
|
+
expect(tdProps.Cpu).toBe("256");
|
|
626
|
+
expect(tdProps.Memory).toBe("512");
|
|
627
|
+
|
|
628
|
+
const containerDef = (tdProps.ContainerDefinitions[0] as any).props;
|
|
629
|
+
const portMapping = (containerDef.PortMappings[0] as any).props;
|
|
630
|
+
expect(portMapping.ContainerPort).toBe(80);
|
|
631
|
+
|
|
632
|
+
const svcProps = (instance.service as any).props;
|
|
633
|
+
expect(svcProps.DesiredCount).toBe(2);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { Sub } from "../intrinsics";
|
|
24
24
|
import { ECRActions } from "../actions/ecr";
|
|
25
25
|
import { LogsActions } from "../actions/logs";
|
|
26
|
+
import { ecsTrustPolicy } from "./ecs-trust-policy";
|
|
26
27
|
|
|
27
28
|
export interface FargateAlbProps {
|
|
28
29
|
image: string;
|
|
@@ -42,17 +43,6 @@ export interface FargateAlbProps {
|
|
|
42
43
|
logRetentionDays?: number;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const ecsTrustPolicy = {
|
|
46
|
-
Version: "2012-10-17" as const,
|
|
47
|
-
Statement: [
|
|
48
|
-
{
|
|
49
|
-
Effect: "Allow" as const,
|
|
50
|
-
Principal: { Service: "ecs-tasks.amazonaws.com" },
|
|
51
|
-
Action: "sts:AssumeRole",
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
46
|
export const FargateAlb = Composite<FargateAlbProps>((props) => {
|
|
57
47
|
const containerPort = props.containerPort ?? 80;
|
|
58
48
|
const cpu = props.cpu ?? "256";
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { Composite } from "@intentius/chant";
|
|
2
|
+
import {
|
|
3
|
+
EcsService,
|
|
4
|
+
EcsService_LoadBalancer,
|
|
5
|
+
EcsService_NetworkConfiguration,
|
|
6
|
+
EcsService_AwsVpcConfiguration,
|
|
7
|
+
TaskDefinition,
|
|
8
|
+
TaskDefinition_ContainerDefinition,
|
|
9
|
+
TaskDefinition_PortMapping,
|
|
10
|
+
TaskDefinition_LogConfiguration,
|
|
11
|
+
TaskDefinition_KeyValuePair,
|
|
12
|
+
TargetGroup,
|
|
13
|
+
ListenerRule,
|
|
14
|
+
ListenerRule_Action,
|
|
15
|
+
ListenerRule_RuleCondition,
|
|
16
|
+
ListenerRule_PathPatternConfig,
|
|
17
|
+
ListenerRule_HostHeaderConfig,
|
|
18
|
+
SecurityGroup,
|
|
19
|
+
SecurityGroup_Ingress,
|
|
20
|
+
LogGroup,
|
|
21
|
+
Role,
|
|
22
|
+
Role_Policy,
|
|
23
|
+
} from "../generated";
|
|
24
|
+
import { Sub } from "../intrinsics";
|
|
25
|
+
import { ecsTrustPolicy } from "./ecs-trust-policy";
|
|
26
|
+
|
|
27
|
+
export interface FargateServiceProps {
|
|
28
|
+
// Wiring to shared ALB
|
|
29
|
+
clusterArn: string;
|
|
30
|
+
listenerArn: string;
|
|
31
|
+
albSecurityGroupId: string;
|
|
32
|
+
executionRoleArn: string;
|
|
33
|
+
|
|
34
|
+
// Routing — at least one required
|
|
35
|
+
priority: number;
|
|
36
|
+
pathPatterns?: string[];
|
|
37
|
+
hostHeaders?: string[];
|
|
38
|
+
|
|
39
|
+
// Container
|
|
40
|
+
image: string;
|
|
41
|
+
containerPort?: number;
|
|
42
|
+
cpu?: string;
|
|
43
|
+
memory?: string;
|
|
44
|
+
desiredCount?: number;
|
|
45
|
+
environment?: Record<string, string>;
|
|
46
|
+
command?: string[];
|
|
47
|
+
|
|
48
|
+
// Networking
|
|
49
|
+
vpcId: string;
|
|
50
|
+
privateSubnetIds: string[];
|
|
51
|
+
healthCheckPath?: string;
|
|
52
|
+
|
|
53
|
+
// IAM
|
|
54
|
+
ManagedPolicyArns?: string[];
|
|
55
|
+
Policies?: InstanceType<typeof Role_Policy>[];
|
|
56
|
+
logRetentionDays?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const FargateService = Composite<FargateServiceProps>((props) => {
|
|
60
|
+
if (!props.pathPatterns && !props.hostHeaders) {
|
|
61
|
+
throw new Error("FargateService requires at least one of pathPatterns or hostHeaders");
|
|
62
|
+
}
|
|
63
|
+
if (props.priority < 1 || props.priority > 50000) {
|
|
64
|
+
throw new Error("FargateService priority must be between 1 and 50000");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const containerPort = props.containerPort ?? 80;
|
|
68
|
+
const cpu = props.cpu ?? "256";
|
|
69
|
+
const memory = props.memory ?? "512";
|
|
70
|
+
const desiredCount = props.desiredCount ?? 2;
|
|
71
|
+
const healthCheckPath = props.healthCheckPath ?? "/";
|
|
72
|
+
const logRetentionDays = props.logRetentionDays ?? 30;
|
|
73
|
+
|
|
74
|
+
// Task role — app permissions
|
|
75
|
+
const taskRole = new Role({
|
|
76
|
+
AssumeRolePolicyDocument: ecsTrustPolicy,
|
|
77
|
+
ManagedPolicyArns: props.ManagedPolicyArns,
|
|
78
|
+
Policies: props.Policies,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Log group
|
|
82
|
+
const logGroup = new LogGroup({
|
|
83
|
+
RetentionInDays: logRetentionDays,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Container definition
|
|
87
|
+
const portMapping = new TaskDefinition_PortMapping({
|
|
88
|
+
ContainerPort: containerPort,
|
|
89
|
+
Protocol: "tcp",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const logConfiguration = new TaskDefinition_LogConfiguration({
|
|
93
|
+
LogDriver: "awslogs",
|
|
94
|
+
Options: {
|
|
95
|
+
"awslogs-group": logGroup as any,
|
|
96
|
+
"awslogs-region": Sub`\${AWS::Region}`,
|
|
97
|
+
"awslogs-stream-prefix": "ecs",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const environmentVars: InstanceType<typeof TaskDefinition_KeyValuePair>[] = [];
|
|
102
|
+
if (props.environment) {
|
|
103
|
+
for (const [name, value] of Object.entries(props.environment)) {
|
|
104
|
+
environmentVars.push(
|
|
105
|
+
new TaskDefinition_KeyValuePair({ Name: name, Value: value }),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const container = new TaskDefinition_ContainerDefinition({
|
|
111
|
+
Name: "app",
|
|
112
|
+
Image: props.image,
|
|
113
|
+
Essential: true,
|
|
114
|
+
PortMappings: [portMapping],
|
|
115
|
+
LogConfiguration: logConfiguration,
|
|
116
|
+
Environment: environmentVars.length > 0 ? environmentVars : undefined,
|
|
117
|
+
Command: props.command,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Task definition
|
|
121
|
+
const taskDef = new TaskDefinition({
|
|
122
|
+
NetworkMode: "awsvpc",
|
|
123
|
+
RequiresCompatibilities: ["FARGATE"],
|
|
124
|
+
Cpu: cpu,
|
|
125
|
+
Memory: memory,
|
|
126
|
+
ExecutionRoleArn: props.executionRoleArn,
|
|
127
|
+
TaskRoleArn: taskRole.Arn,
|
|
128
|
+
ContainerDefinitions: [container],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Task security group — ingress on container port from ALB SG
|
|
132
|
+
const taskIngress = new SecurityGroup_Ingress({
|
|
133
|
+
IpProtocol: "tcp",
|
|
134
|
+
FromPort: containerPort,
|
|
135
|
+
ToPort: containerPort,
|
|
136
|
+
SourceSecurityGroupId: props.albSecurityGroupId,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const taskSg = new SecurityGroup({
|
|
140
|
+
GroupDescription: "Fargate task security group",
|
|
141
|
+
VpcId: props.vpcId,
|
|
142
|
+
SecurityGroupIngress: [taskIngress],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Target group
|
|
146
|
+
const targetGroup = new TargetGroup({
|
|
147
|
+
TargetType: "ip",
|
|
148
|
+
Protocol: "HTTP",
|
|
149
|
+
Port: containerPort,
|
|
150
|
+
VpcId: props.vpcId,
|
|
151
|
+
HealthCheckPath: healthCheckPath,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Listener rule conditions
|
|
155
|
+
const conditions: InstanceType<typeof ListenerRule_RuleCondition>[] = [];
|
|
156
|
+
|
|
157
|
+
if (props.pathPatterns) {
|
|
158
|
+
const pathConfig = new ListenerRule_PathPatternConfig({
|
|
159
|
+
Values: props.pathPatterns,
|
|
160
|
+
});
|
|
161
|
+
conditions.push(
|
|
162
|
+
new ListenerRule_RuleCondition({
|
|
163
|
+
Field: "path-pattern",
|
|
164
|
+
PathPatternConfig: pathConfig,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (props.hostHeaders) {
|
|
170
|
+
const hostConfig = new ListenerRule_HostHeaderConfig({
|
|
171
|
+
Values: props.hostHeaders,
|
|
172
|
+
});
|
|
173
|
+
conditions.push(
|
|
174
|
+
new ListenerRule_RuleCondition({
|
|
175
|
+
Field: "host-header",
|
|
176
|
+
HostHeaderConfig: hostConfig,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Listener rule
|
|
182
|
+
const ruleAction = new ListenerRule_Action({
|
|
183
|
+
Type: "forward",
|
|
184
|
+
TargetGroupArn: targetGroup.TargetGroupArn,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const rule = new ListenerRule({
|
|
188
|
+
ListenerArn: props.listenerArn,
|
|
189
|
+
Priority: props.priority,
|
|
190
|
+
Actions: [ruleAction],
|
|
191
|
+
Conditions: conditions,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ECS Service
|
|
195
|
+
const serviceLoadBalancer = new EcsService_LoadBalancer({
|
|
196
|
+
ContainerName: "app",
|
|
197
|
+
ContainerPort: containerPort,
|
|
198
|
+
TargetGroupArn: targetGroup.TargetGroupArn,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const awsVpcConfig = new EcsService_AwsVpcConfiguration({
|
|
202
|
+
Subnets: props.privateSubnetIds,
|
|
203
|
+
SecurityGroups: [taskSg.GroupId],
|
|
204
|
+
AssignPublicIp: "DISABLED",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const networkConfig = new EcsService_NetworkConfiguration({
|
|
208
|
+
AwsvpcConfiguration: awsVpcConfig,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const service = new EcsService(
|
|
212
|
+
{
|
|
213
|
+
Cluster: props.clusterArn,
|
|
214
|
+
TaskDefinition: taskDef.TaskDefinitionArn,
|
|
215
|
+
LaunchType: "FARGATE",
|
|
216
|
+
DesiredCount: desiredCount,
|
|
217
|
+
HealthCheckGracePeriodSeconds: 60,
|
|
218
|
+
LoadBalancers: [serviceLoadBalancer],
|
|
219
|
+
NetworkConfiguration: networkConfig,
|
|
220
|
+
},
|
|
221
|
+
{ DependsOn: [rule] },
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
taskRole,
|
|
226
|
+
logGroup,
|
|
227
|
+
taskDef,
|
|
228
|
+
taskSg,
|
|
229
|
+
targetGroup,
|
|
230
|
+
rule,
|
|
231
|
+
service,
|
|
232
|
+
};
|
|
233
|
+
}, "FargateService");
|
package/src/composites/index.ts
CHANGED
|
@@ -18,3 +18,7 @@ export { VpcDefault } from "./vpc-default";
|
|
|
18
18
|
export type { VpcDefaultProps } from "./vpc-default";
|
|
19
19
|
export { FargateAlb } from "./fargate-alb";
|
|
20
20
|
export type { FargateAlbProps } from "./fargate-alb";
|
|
21
|
+
export { AlbShared } from "./alb-shared";
|
|
22
|
+
export type { AlbSharedProps } from "./alb-shared";
|
|
23
|
+
export { FargateService } from "./fargate-service";
|
|
24
|
+
export type { FargateServiceProps } from "./fargate-service";
|
package/src/index.ts
CHANGED
|
@@ -77,12 +77,12 @@ export {
|
|
|
77
77
|
LambdaApi,
|
|
78
78
|
LambdaScheduled, ScheduledLambda,
|
|
79
79
|
LambdaSqs, LambdaEventBridge, LambdaDynamoDB, LambdaS3, LambdaSns,
|
|
80
|
-
VpcDefault, FargateAlb,
|
|
80
|
+
VpcDefault, FargateAlb, AlbShared, FargateService,
|
|
81
81
|
} from "./composites/index";
|
|
82
82
|
export type {
|
|
83
83
|
LambdaFunctionProps, LambdaApiProps, ScheduledLambdaProps,
|
|
84
84
|
LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, LambdaS3Props, LambdaSnsProps,
|
|
85
|
-
VpcDefaultProps, FargateAlbProps,
|
|
85
|
+
VpcDefaultProps, FargateAlbProps, AlbSharedProps, FargateServiceProps,
|
|
86
86
|
} from "./composites/index";
|
|
87
87
|
|
|
88
88
|
// Code generation pipeline
|