@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/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
 
@@ -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: { "Fn::GetAtt": [logicalName, ref.attribute] },
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;