@intentius/chant-lexicon-aws 0.0.13 → 0.0.15
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 +26 -24
- package/dist/manifest.json +1 -1
- package/dist/meta.json +1093 -341
- package/dist/rules/waw030.ts +55 -0
- package/dist/rules/waw031.ts +66 -0
- package/dist/skills/chant-eks.md +175 -0
- package/dist/types/index.d.ts +841 -61
- package/package.json +29 -26
- package/src/codegen/docs.ts +103 -8
- package/src/generated/index.d.ts +841 -61
- package/src/generated/index.ts +65 -4
- package/src/generated/lexicon-aws.json +1093 -341
- package/src/lint/post-synth/waw030.test.ts +209 -1
- package/src/lint/post-synth/waw030.ts +55 -0
- package/src/lint/post-synth/waw031.test.ts +273 -0
- package/src/lint/post-synth/waw031.ts +66 -0
- package/src/plugin.ts +320 -2
- package/src/serializer.test.ts +40 -0
- package/src/serializer.ts +6 -1
package/src/plugin.ts
CHANGED
|
@@ -54,7 +54,112 @@ export const awsPlugin: LexiconPlugin = {
|
|
|
54
54
|
];
|
|
55
55
|
},
|
|
56
56
|
|
|
57
|
-
initTemplates() {
|
|
57
|
+
initTemplates(template?: string) {
|
|
58
|
+
if (template === "eks") {
|
|
59
|
+
return { src: {
|
|
60
|
+
"infra/cluster.ts": `/**
|
|
61
|
+
* EKS Cluster + Managed Node Group + OIDC Provider
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import { Cluster, Nodegroup, OIDCProvider, Role, InstanceProfile, Sub, AWS } from "@intentius/chant-lexicon-aws";
|
|
65
|
+
|
|
66
|
+
// EKS Cluster Role
|
|
67
|
+
export const clusterRole = new Role({
|
|
68
|
+
RoleName: Sub\`\${AWS.StackName}-eks-cluster-role\`,
|
|
69
|
+
AssumeRolePolicyDocument: {
|
|
70
|
+
Version: "2012-10-17",
|
|
71
|
+
Statement: [{
|
|
72
|
+
Effect: "Allow",
|
|
73
|
+
Principal: { Service: "eks.amazonaws.com" },
|
|
74
|
+
Action: "sts:AssumeRole",
|
|
75
|
+
}],
|
|
76
|
+
},
|
|
77
|
+
ManagedPolicyArns: [
|
|
78
|
+
"arn:aws:iam::aws:policy/AmazonEKSClusterPolicy",
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// EKS Cluster
|
|
83
|
+
export const cluster = new Cluster({
|
|
84
|
+
Name: Sub\`\${AWS.StackName}-cluster\`,
|
|
85
|
+
RoleArn: clusterRole,
|
|
86
|
+
Version: "1.29",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Node Role
|
|
90
|
+
export const nodeRole = new Role({
|
|
91
|
+
RoleName: Sub\`\${AWS.StackName}-eks-node-role\`,
|
|
92
|
+
AssumeRolePolicyDocument: {
|
|
93
|
+
Version: "2012-10-17",
|
|
94
|
+
Statement: [{
|
|
95
|
+
Effect: "Allow",
|
|
96
|
+
Principal: { Service: "ec2.amazonaws.com" },
|
|
97
|
+
Action: "sts:AssumeRole",
|
|
98
|
+
}],
|
|
99
|
+
},
|
|
100
|
+
ManagedPolicyArns: [
|
|
101
|
+
"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
|
|
102
|
+
"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
|
|
103
|
+
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Managed Node Group
|
|
108
|
+
export const nodeGroup = new Nodegroup({
|
|
109
|
+
ClusterName: cluster,
|
|
110
|
+
NodegroupName: Sub\`\${AWS.StackName}-nodes\`,
|
|
111
|
+
NodeRole: nodeRole,
|
|
112
|
+
ScalingConfig: {
|
|
113
|
+
MinSize: 2,
|
|
114
|
+
MaxSize: 10,
|
|
115
|
+
DesiredSize: 3,
|
|
116
|
+
},
|
|
117
|
+
InstanceTypes: ["t3.medium"],
|
|
118
|
+
});
|
|
119
|
+
`,
|
|
120
|
+
"k8s/namespace.ts": `/**
|
|
121
|
+
* K8s namespace with quotas and network isolation
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
import { NamespaceEnv } from "@intentius/chant-lexicon-k8s";
|
|
125
|
+
|
|
126
|
+
export const { namespace, resourceQuota, limitRange, networkPolicy } = NamespaceEnv({
|
|
127
|
+
name: "prod",
|
|
128
|
+
cpuQuota: "16",
|
|
129
|
+
memoryQuota: "32Gi",
|
|
130
|
+
defaultCpuRequest: "100m",
|
|
131
|
+
defaultMemoryRequest: "128Mi",
|
|
132
|
+
defaultCpuLimit: "500m",
|
|
133
|
+
defaultMemoryLimit: "512Mi",
|
|
134
|
+
defaultDenyIngress: true,
|
|
135
|
+
});
|
|
136
|
+
`,
|
|
137
|
+
"k8s/app.ts": `/**
|
|
138
|
+
* Application deployment with IRSA and autoscaling
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
import { AutoscaledService, IrsaServiceAccount } from "@intentius/chant-lexicon-k8s";
|
|
142
|
+
|
|
143
|
+
// IRSA ServiceAccount — replace with your IAM Role ARN from CloudFormation outputs
|
|
144
|
+
export const { serviceAccount } = IrsaServiceAccount({
|
|
145
|
+
name: "app-sa",
|
|
146
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/app-role", // TODO: update from CF output
|
|
147
|
+
namespace: "prod",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export const { deployment, service, hpa, pdb } = AutoscaledService({
|
|
151
|
+
name: "my-app",
|
|
152
|
+
image: "my-app:1.0",
|
|
153
|
+
port: 8080,
|
|
154
|
+
maxReplicas: 10,
|
|
155
|
+
cpuRequest: "200m",
|
|
156
|
+
memoryRequest: "256Mi",
|
|
157
|
+
namespace: "prod",
|
|
158
|
+
});
|
|
159
|
+
`,
|
|
160
|
+
} };
|
|
161
|
+
}
|
|
162
|
+
|
|
58
163
|
return { src: {
|
|
59
164
|
"config.ts": `/**
|
|
60
165
|
* Shared bucket configuration — encryption, versioning, public access
|
|
@@ -180,10 +285,11 @@ export const logsBucket = new Bucket({
|
|
|
180
285
|
const { waw028 } = require("./lint/post-synth/waw028");
|
|
181
286
|
const { waw029 } = require("./lint/post-synth/waw029");
|
|
182
287
|
const { waw030 } = require("./lint/post-synth/waw030");
|
|
288
|
+
const { waw031 } = require("./lint/post-synth/waw031");
|
|
183
289
|
return [
|
|
184
290
|
waw010, waw011, cor020, ext001, waw013, waw014, waw015, waw016, waw017,
|
|
185
291
|
waw018, waw019, waw020, waw021, waw022, waw023, waw024, waw025,
|
|
186
|
-
waw026, waw027, waw028, waw029, waw030,
|
|
292
|
+
waw026, waw027, waw028, waw029, waw030, waw031,
|
|
187
293
|
];
|
|
188
294
|
},
|
|
189
295
|
|
|
@@ -793,6 +899,218 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
793
899
|
},
|
|
794
900
|
],
|
|
795
901
|
},
|
|
902
|
+
{
|
|
903
|
+
name: "chant-eks",
|
|
904
|
+
description: "EKS end-to-end workflow — provision cluster, configure kubectl, deploy K8s workloads",
|
|
905
|
+
content: `---
|
|
906
|
+
skill: chant-eks
|
|
907
|
+
description: End-to-end EKS workflow bridging AWS infrastructure and Kubernetes workloads
|
|
908
|
+
user-invocable: true
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
# EKS End-to-End Workflow
|
|
912
|
+
|
|
913
|
+
## Overview
|
|
914
|
+
|
|
915
|
+
This skill bridges two lexicons:
|
|
916
|
+
- **\`@intentius/chant-lexicon-aws\`** — EKS cluster, node groups, IAM roles, OIDC provider (CloudFormation)
|
|
917
|
+
- **\`@intentius/chant-lexicon-k8s\`** — Kubernetes workloads, IRSA, ALB Ingress, storage, observability (K8s YAML)
|
|
918
|
+
|
|
919
|
+
## Architecture
|
|
920
|
+
|
|
921
|
+
\`\`\`
|
|
922
|
+
AWS Lexicon (CloudFormation) K8s Lexicon (kubectl apply)
|
|
923
|
+
┌────────────────────────┐ ┌────────────────────────────┐
|
|
924
|
+
│ VPC + Subnets │ │ NamespaceEnv (quotas) │
|
|
925
|
+
│ EKS Cluster │ │ AutoscaledService (app) │
|
|
926
|
+
│ Managed Node Group │──ARNs──→ │ IrsaServiceAccount (IRSA) │
|
|
927
|
+
│ OIDC Provider │ │ AlbIngress (ALB) │
|
|
928
|
+
│ IAM Roles (IRSA) │ │ EbsStorageClass (gp3) │
|
|
929
|
+
│ EKS Add-ons │ │ FluentBitAgent (logs) │
|
|
930
|
+
└────────────────────────┘ │ ExternalDnsAgent (DNS) │
|
|
931
|
+
└────────────────────────────┘
|
|
932
|
+
\`\`\`
|
|
933
|
+
|
|
934
|
+
## Step 1: Provision AWS Infrastructure
|
|
935
|
+
|
|
936
|
+
\`\`\`bash
|
|
937
|
+
# Build CloudFormation template
|
|
938
|
+
chant build src/infra/ --output infra.json
|
|
939
|
+
|
|
940
|
+
# Deploy
|
|
941
|
+
aws cloudformation deploy \\
|
|
942
|
+
--template-file infra.json \\
|
|
943
|
+
--stack-name my-eks-cluster \\
|
|
944
|
+
--capabilities CAPABILITY_NAMED_IAM
|
|
945
|
+
\`\`\`
|
|
946
|
+
|
|
947
|
+
Key AWS resources:
|
|
948
|
+
- **EKS Cluster** — control plane
|
|
949
|
+
- **Managed Node Group** — EC2 worker nodes
|
|
950
|
+
- **OIDC Provider** — enables IRSA (IAM Roles for Service Accounts)
|
|
951
|
+
- **IAM Roles** — node role, app IRSA roles, ALB controller role
|
|
952
|
+
|
|
953
|
+
## Step 2: Configure kubectl
|
|
954
|
+
|
|
955
|
+
\`\`\`bash
|
|
956
|
+
aws eks update-kubeconfig --name my-cluster --region us-east-1
|
|
957
|
+
kubectl get nodes # verify connectivity
|
|
958
|
+
\`\`\`
|
|
959
|
+
|
|
960
|
+
## Step 3: Deploy K8s Workloads
|
|
961
|
+
|
|
962
|
+
\`\`\`bash
|
|
963
|
+
# Build K8s manifests
|
|
964
|
+
chant build src/k8s/ --output manifests.yaml
|
|
965
|
+
|
|
966
|
+
# Apply
|
|
967
|
+
kubectl apply -f manifests.yaml
|
|
968
|
+
\`\`\`
|
|
969
|
+
|
|
970
|
+
### Key K8s composites for EKS
|
|
971
|
+
|
|
972
|
+
\`\`\`typescript
|
|
973
|
+
import {
|
|
974
|
+
NamespaceEnv,
|
|
975
|
+
AutoscaledService,
|
|
976
|
+
IrsaServiceAccount,
|
|
977
|
+
AlbIngress,
|
|
978
|
+
EbsStorageClass,
|
|
979
|
+
FluentBitAgent,
|
|
980
|
+
ExternalDnsAgent,
|
|
981
|
+
} from "@intentius/chant-lexicon-k8s";
|
|
982
|
+
|
|
983
|
+
// 1. Namespace with quotas and network isolation
|
|
984
|
+
const ns = NamespaceEnv({
|
|
985
|
+
name: "prod",
|
|
986
|
+
cpuQuota: "16",
|
|
987
|
+
memoryQuota: "32Gi",
|
|
988
|
+
defaultCpuRequest: "100m",
|
|
989
|
+
defaultMemoryRequest: "128Mi",
|
|
990
|
+
defaultDenyIngress: true,
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// 2. IRSA ServiceAccount (use IAM Role ARN from CloudFormation outputs)
|
|
994
|
+
const irsa = IrsaServiceAccount({
|
|
995
|
+
name: "app-sa",
|
|
996
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/app-role", // from CF output
|
|
997
|
+
namespace: "prod",
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// 3. Application with autoscaling
|
|
1001
|
+
const app = AutoscaledService({
|
|
1002
|
+
name: "api",
|
|
1003
|
+
image: "api:1.0",
|
|
1004
|
+
port: 8080,
|
|
1005
|
+
maxReplicas: 10,
|
|
1006
|
+
cpuRequest: "200m",
|
|
1007
|
+
memoryRequest: "256Mi",
|
|
1008
|
+
namespace: "prod",
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// 4. ALB Ingress (use ACM cert ARN from CloudFormation outputs)
|
|
1012
|
+
const ingress = AlbIngress({
|
|
1013
|
+
name: "api-ingress",
|
|
1014
|
+
hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
|
|
1015
|
+
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/abc", // from CF output
|
|
1016
|
+
namespace: "prod",
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// 5. Storage
|
|
1020
|
+
const storage = EbsStorageClass({ name: "gp3-encrypted", type: "gp3", encrypted: true });
|
|
1021
|
+
|
|
1022
|
+
// 6. Observability
|
|
1023
|
+
const logging = FluentBitAgent({
|
|
1024
|
+
logGroup: "/aws/eks/my-cluster/containers",
|
|
1025
|
+
region: "us-east-1",
|
|
1026
|
+
clusterName: "my-cluster",
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// 7. DNS
|
|
1030
|
+
const dns = ExternalDnsAgent({
|
|
1031
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/external-dns-role",
|
|
1032
|
+
domainFilters: ["example.com"],
|
|
1033
|
+
});
|
|
1034
|
+
\`\`\`
|
|
1035
|
+
|
|
1036
|
+
## Step 4: Verify
|
|
1037
|
+
|
|
1038
|
+
\`\`\`bash
|
|
1039
|
+
kubectl get pods -n prod
|
|
1040
|
+
kubectl get ingress -n prod
|
|
1041
|
+
kubectl logs -n amazon-cloudwatch -l app.kubernetes.io/name=fluent-bit
|
|
1042
|
+
\`\`\`
|
|
1043
|
+
|
|
1044
|
+
## Cleanup
|
|
1045
|
+
|
|
1046
|
+
\`\`\`bash
|
|
1047
|
+
# Delete K8s workloads first
|
|
1048
|
+
kubectl delete -f manifests.yaml
|
|
1049
|
+
|
|
1050
|
+
# Then delete AWS infrastructure
|
|
1051
|
+
aws cloudformation delete-stack --stack-name my-eks-cluster
|
|
1052
|
+
aws cloudformation wait stack-delete-complete --stack-name my-eks-cluster
|
|
1053
|
+
\`\`\`
|
|
1054
|
+
|
|
1055
|
+
## Cross-Lexicon Value Flow
|
|
1056
|
+
|
|
1057
|
+
CloudFormation outputs flow into K8s composite props:
|
|
1058
|
+
|
|
1059
|
+
| CloudFormation Output | K8s Composite Prop |
|
|
1060
|
+
|----------------------|-------------------|
|
|
1061
|
+
| App IAM Role ARN | \`IrsaServiceAccount.iamRoleArn\` |
|
|
1062
|
+
| ALB Controller Role ARN | \`IrsaServiceAccount.iamRoleArn\` (for ALB controller SA) |
|
|
1063
|
+
| ACM Certificate ARN | \`AlbIngress.certificateArn\` |
|
|
1064
|
+
| ExternalDNS Role ARN | \`ExternalDnsAgent.iamRoleArn\` |
|
|
1065
|
+
| EKS Cluster Name | \`FluentBitAgent.clusterName\`, \`AdotCollector.clusterName\` |
|
|
1066
|
+
| EFS Filesystem ID | \`EfsStorageClass.fileSystemId\` |
|
|
1067
|
+
|
|
1068
|
+
## EKS Init Template
|
|
1069
|
+
|
|
1070
|
+
Scaffold a dual-lexicon EKS project:
|
|
1071
|
+
|
|
1072
|
+
\`\`\`bash
|
|
1073
|
+
chant init --lexicon aws --template eks
|
|
1074
|
+
\`\`\`
|
|
1075
|
+
|
|
1076
|
+
This creates:
|
|
1077
|
+
- \`src/infra/\` — EKS cluster, node group, IAM (AWS lexicon)
|
|
1078
|
+
- \`src/k8s/\` — namespace, app, ingress, storage (K8s lexicon)
|
|
1079
|
+
- \`package.json\` with both \`@intentius/chant-lexicon-aws\` and \`@intentius/chant-lexicon-k8s\`
|
|
1080
|
+
`,
|
|
1081
|
+
triggers: [
|
|
1082
|
+
{ type: "context", value: "eks" },
|
|
1083
|
+
{ type: "context", value: "kubernetes" },
|
|
1084
|
+
{ type: "context", value: "k8s-workloads" },
|
|
1085
|
+
],
|
|
1086
|
+
preConditions: [
|
|
1087
|
+
"AWS CLI is installed and configured",
|
|
1088
|
+
"chant CLI is installed",
|
|
1089
|
+
"kubectl is installed",
|
|
1090
|
+
],
|
|
1091
|
+
postConditions: [
|
|
1092
|
+
"EKS cluster is running",
|
|
1093
|
+
"K8s workloads are deployed",
|
|
1094
|
+
],
|
|
1095
|
+
parameters: [],
|
|
1096
|
+
examples: [
|
|
1097
|
+
{
|
|
1098
|
+
title: "Full EKS deployment",
|
|
1099
|
+
description: "Deploy infrastructure and workloads end-to-end",
|
|
1100
|
+
input: "Set up a complete EKS environment with my API",
|
|
1101
|
+
output: `# 1. Build and deploy infrastructure
|
|
1102
|
+
chant build src/infra/ --output infra.json
|
|
1103
|
+
aws cloudformation deploy --template-file infra.json --stack-name my-eks --capabilities CAPABILITY_NAMED_IAM
|
|
1104
|
+
|
|
1105
|
+
# 2. Configure kubectl
|
|
1106
|
+
aws eks update-kubeconfig --name my-cluster
|
|
1107
|
+
|
|
1108
|
+
# 3. Build and deploy workloads
|
|
1109
|
+
chant build src/k8s/ --output manifests.yaml
|
|
1110
|
+
kubectl apply -f manifests.yaml`,
|
|
1111
|
+
},
|
|
1112
|
+
],
|
|
1113
|
+
},
|
|
796
1114
|
];
|
|
797
1115
|
},
|
|
798
1116
|
|
package/src/serializer.test.ts
CHANGED
|
@@ -326,6 +326,46 @@ describe("LexiconOutput serialization", () => {
|
|
|
326
326
|
});
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
+
// ── StackOutput Serialization ──────────────────────────
|
|
330
|
+
|
|
331
|
+
describe("stackOutput serialization", () => {
|
|
332
|
+
test("Id attribute uses Ref (not Fn::GetAtt)", () => {
|
|
333
|
+
const bucket = new MockBucket({ BucketName: "my-bucket" });
|
|
334
|
+
const idRef = new AttrRef(bucket, "Id");
|
|
335
|
+
idRef._setLogicalName("MyBucket");
|
|
336
|
+
|
|
337
|
+
const output = stackOutput(idRef);
|
|
338
|
+
|
|
339
|
+
const entities = new Map<string, Declarable>();
|
|
340
|
+
entities.set("MyBucket", bucket);
|
|
341
|
+
entities.set("MyBucketId", output as unknown as Declarable);
|
|
342
|
+
|
|
343
|
+
const result = awsSerializer.serialize(entities);
|
|
344
|
+
const template = JSON.parse(result as string);
|
|
345
|
+
|
|
346
|
+
expect(template.Outputs.MyBucketId.Value).toEqual({ Ref: "MyBucket" });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("non-Id attribute uses Fn::GetAtt", () => {
|
|
350
|
+
const bucket = new MockBucket({ BucketName: "my-bucket" });
|
|
351
|
+
const arnRef = new AttrRef(bucket, "Arn");
|
|
352
|
+
arnRef._setLogicalName("MyBucket");
|
|
353
|
+
|
|
354
|
+
const output = stackOutput(arnRef);
|
|
355
|
+
|
|
356
|
+
const entities = new Map<string, Declarable>();
|
|
357
|
+
entities.set("MyBucket", bucket);
|
|
358
|
+
entities.set("MyBucketArn", output as unknown as Declarable);
|
|
359
|
+
|
|
360
|
+
const result = awsSerializer.serialize(entities);
|
|
361
|
+
const template = JSON.parse(result as string);
|
|
362
|
+
|
|
363
|
+
expect(template.Outputs.MyBucketArn.Value).toEqual({
|
|
364
|
+
"Fn::GetAtt": ["MyBucket", "Arn"],
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
329
369
|
// ── Nested Stack Serialization ──────────────────────────
|
|
330
370
|
|
|
331
371
|
function mockChildBuildResult(childTemplate: object): BuildResult {
|
package/src/serializer.ts
CHANGED
|
@@ -282,8 +282,13 @@ function serializeToTemplate(
|
|
|
282
282
|
const ref = stackOutput.sourceRef;
|
|
283
283
|
const logicalName = ref.getLogicalName();
|
|
284
284
|
if (logicalName) {
|
|
285
|
+
// Use Ref for primary identifier ("Id") since not all resources
|
|
286
|
+
// support Fn::GetAtt for their primary identifier (e.g. ACM Certificate).
|
|
287
|
+
// Ref always returns the primary identifier for any CF resource.
|
|
285
288
|
const output: CFOutput = {
|
|
286
|
-
Value:
|
|
289
|
+
Value: ref.attribute === "Id"
|
|
290
|
+
? { Ref: logicalName }
|
|
291
|
+
: { "Fn::GetAtt": [logicalName, ref.attribute] },
|
|
287
292
|
};
|
|
288
293
|
if (stackOutput.description) {
|
|
289
294
|
output.Description = stackOutput.description;
|