@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/dist/rules/waw030.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - API Gateway V2 Deployment with no DependsOn on any Route
|
|
11
11
|
* - DynamoDB ScalableTarget with no DependsOn on the Table
|
|
12
12
|
* - ECS ScalableTarget with no DependsOn on the ECS Service
|
|
13
|
+
* - EKS Addon with hardcoded ClusterName but no DependsOn on the Cluster or Nodegroup
|
|
14
|
+
* - EKS Nodegroup with hardcoded ClusterName but no DependsOn on the Cluster
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
@@ -34,6 +36,9 @@ export function checkMissingDependsOn(ctx: PostSynthContext): PostSynthDiagnosti
|
|
|
34
36
|
const dynamoTableIds: string[] = [];
|
|
35
37
|
const ecsServiceIds: string[] = [];
|
|
36
38
|
const scalableTargetEntries: { logicalId: string; namespace: string }[] = [];
|
|
39
|
+
const eksClusterIds: string[] = [];
|
|
40
|
+
const eksNodegroupIds: string[] = [];
|
|
41
|
+
const eksAddonIds: string[] = [];
|
|
37
42
|
|
|
38
43
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
39
44
|
if (resource.Type === "AWS::ElasticLoadBalancingV2::Listener") {
|
|
@@ -60,6 +65,15 @@ export function checkMissingDependsOn(ctx: PostSynthContext): PostSynthDiagnosti
|
|
|
60
65
|
if (resource.Type === "AWS::ECS::Service") {
|
|
61
66
|
ecsServiceIds.push(logicalId);
|
|
62
67
|
}
|
|
68
|
+
if (resource.Type === "AWS::EKS::Cluster") {
|
|
69
|
+
eksClusterIds.push(logicalId);
|
|
70
|
+
}
|
|
71
|
+
if (resource.Type === "AWS::EKS::Nodegroup") {
|
|
72
|
+
eksNodegroupIds.push(logicalId);
|
|
73
|
+
}
|
|
74
|
+
if (resource.Type === "AWS::EKS::Addon") {
|
|
75
|
+
eksAddonIds.push(logicalId);
|
|
76
|
+
}
|
|
63
77
|
if (resource.Type === "AWS::ApplicationAutoScaling::ScalableTarget") {
|
|
64
78
|
const props = resource.Properties ?? {};
|
|
65
79
|
const ns = inferScalingNamespace(props);
|
|
@@ -144,6 +158,47 @@ export function checkMissingDependsOn(ctx: PostSynthContext): PostSynthDiagnosti
|
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
|
|
161
|
+
// Pattern 7: EKS Addon with hardcoded ClusterName but no dependency on Cluster or Nodegroup
|
|
162
|
+
for (const addonId of eksAddonIds) {
|
|
163
|
+
const resource = resources[addonId];
|
|
164
|
+
const deps = getDependsOnSet(resource);
|
|
165
|
+
const propRefs = collectPropertyRefs(resource);
|
|
166
|
+
const allClusterAndNodeDeps = [...eksClusterIds, ...eksNodegroupIds];
|
|
167
|
+
|
|
168
|
+
const hasClusterDep = allClusterAndNodeDeps.some((id) => deps.has(id));
|
|
169
|
+
const hasClusterRef = allClusterAndNodeDeps.some((id) => propRefs.has(id));
|
|
170
|
+
|
|
171
|
+
if (!hasClusterDep && !hasClusterRef && eksClusterIds.length > 0) {
|
|
172
|
+
diagnostics.push({
|
|
173
|
+
checkId: "WAW030",
|
|
174
|
+
severity: "warning",
|
|
175
|
+
message: `EKS Addon "${addonId}" has no dependency on the Cluster or Nodegroup — the addon may fail if the cluster or nodes aren't ready yet. Add DependsOn.`,
|
|
176
|
+
entity: addonId,
|
|
177
|
+
lexicon: "aws",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Pattern 8: EKS Nodegroup with hardcoded ClusterName but no dependency on Cluster
|
|
183
|
+
for (const ngId of eksNodegroupIds) {
|
|
184
|
+
const resource = resources[ngId];
|
|
185
|
+
const deps = getDependsOnSet(resource);
|
|
186
|
+
const propRefs = collectPropertyRefs(resource);
|
|
187
|
+
|
|
188
|
+
const hasClusterDep = eksClusterIds.some((id) => deps.has(id));
|
|
189
|
+
const hasClusterRef = eksClusterIds.some((id) => propRefs.has(id));
|
|
190
|
+
|
|
191
|
+
if (!hasClusterDep && !hasClusterRef && eksClusterIds.length > 0) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
checkId: "WAW030",
|
|
194
|
+
severity: "warning",
|
|
195
|
+
message: `EKS Nodegroup "${ngId}" has no dependency on the Cluster — the node group may fail if the cluster isn't ready yet. Add DependsOn.`,
|
|
196
|
+
entity: ngId,
|
|
197
|
+
lexicon: "aws",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
147
202
|
// Pattern 5 & 6: ScalableTarget with no DependsOn on the target resource
|
|
148
203
|
for (const entry of scalableTargetEntries) {
|
|
149
204
|
const resource = resources[entry.logicalId];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW031: EKS Addon Missing ServiceAccountRoleArn
|
|
3
|
+
*
|
|
4
|
+
* Certain EKS addons require a ServiceAccountRoleArn (IRSA role) to function.
|
|
5
|
+
* Without one, the addon pods can't authenticate to AWS APIs and the addon
|
|
6
|
+
* hangs in CREATING status indefinitely.
|
|
7
|
+
*
|
|
8
|
+
* Known addons that require IRSA:
|
|
9
|
+
* - aws-ebs-csi-driver (needs EBS API access)
|
|
10
|
+
* - aws-efs-csi-driver (needs EFS API access)
|
|
11
|
+
* - adot (needs CloudWatch/X-Ray access)
|
|
12
|
+
* - amazon-cloudwatch-observability (needs CloudWatch access)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
17
|
+
|
|
18
|
+
/** Addons that are known to require a ServiceAccountRoleArn to function. */
|
|
19
|
+
const ADDONS_REQUIRING_IRSA: Record<string, string> = {
|
|
20
|
+
"aws-ebs-csi-driver": "EBS API access to manage volumes",
|
|
21
|
+
"aws-efs-csi-driver": "EFS API access to manage file systems",
|
|
22
|
+
"adot": "CloudWatch/X-Ray access for metrics and traces",
|
|
23
|
+
"amazon-cloudwatch-observability": "CloudWatch access for logs and metrics",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function checkAddonMissingRole(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
30
|
+
const template = parseCFTemplate(output);
|
|
31
|
+
if (!template?.Resources) continue;
|
|
32
|
+
|
|
33
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
34
|
+
if (resource.Type !== "AWS::EKS::Addon") continue;
|
|
35
|
+
|
|
36
|
+
const props = resource.Properties ?? {};
|
|
37
|
+
const addonName = typeof props.AddonName === "string" ? props.AddonName : null;
|
|
38
|
+
if (!addonName) continue;
|
|
39
|
+
|
|
40
|
+
const reason = ADDONS_REQUIRING_IRSA[addonName];
|
|
41
|
+
if (!reason) continue;
|
|
42
|
+
|
|
43
|
+
// Check if ServiceAccountRoleArn is set (could be a string, Ref, or GetAtt)
|
|
44
|
+
if (!props.ServiceAccountRoleArn) {
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
checkId: "WAW031",
|
|
47
|
+
severity: "warning",
|
|
48
|
+
message: `EKS Addon "${logicalId}" (${addonName}) has no ServiceAccountRoleArn — it needs an IRSA role for ${reason}. Without it, the addon will hang in CREATING status.`,
|
|
49
|
+
entity: logicalId,
|
|
50
|
+
lexicon: "aws",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return diagnostics;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const waw031: PostSynthCheck = {
|
|
60
|
+
id: "WAW031",
|
|
61
|
+
description: "EKS Addon missing ServiceAccountRoleArn for addons that require IRSA",
|
|
62
|
+
|
|
63
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
64
|
+
return checkAddonMissingRole(ctx);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-eks
|
|
3
|
+
description: End-to-end EKS workflow bridging AWS infrastructure and Kubernetes workloads
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# EKS End-to-End Workflow
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This skill bridges two lexicons:
|
|
12
|
+
- **`@intentius/chant-lexicon-aws`** — EKS cluster, node groups, IAM roles, OIDC provider (CloudFormation)
|
|
13
|
+
- **`@intentius/chant-lexicon-k8s`** — Kubernetes workloads, IRSA, ALB Ingress, storage, observability (K8s YAML)
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
AWS Lexicon (CloudFormation) K8s Lexicon (kubectl apply)
|
|
19
|
+
┌────────────────────────┐ ┌────────────────────────────┐
|
|
20
|
+
│ VPC + Subnets │ │ NamespaceEnv (quotas) │
|
|
21
|
+
│ EKS Cluster │ │ AutoscaledService (app) │
|
|
22
|
+
│ Managed Node Group │──ARNs──→ │ IrsaServiceAccount (IRSA) │
|
|
23
|
+
│ OIDC Provider │ │ AlbIngress (ALB) │
|
|
24
|
+
│ IAM Roles (IRSA) │ │ EbsStorageClass (gp3) │
|
|
25
|
+
│ EKS Add-ons │ │ FluentBitAgent (logs) │
|
|
26
|
+
└────────────────────────┘ │ ExternalDnsAgent (DNS) │
|
|
27
|
+
└────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Step 1: Provision AWS Infrastructure
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Build CloudFormation template
|
|
34
|
+
chant build src/infra/ --output infra.json
|
|
35
|
+
|
|
36
|
+
# Deploy
|
|
37
|
+
aws cloudformation deploy \
|
|
38
|
+
--template-file infra.json \
|
|
39
|
+
--stack-name my-eks-cluster \
|
|
40
|
+
--capabilities CAPABILITY_NAMED_IAM
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Key AWS resources:
|
|
44
|
+
- **EKS Cluster** — control plane
|
|
45
|
+
- **Managed Node Group** — EC2 worker nodes
|
|
46
|
+
- **OIDC Provider** — enables IRSA (IAM Roles for Service Accounts)
|
|
47
|
+
- **IAM Roles** — node role, app IRSA roles, ALB controller role
|
|
48
|
+
|
|
49
|
+
## Step 2: Configure kubectl
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
aws eks update-kubeconfig --name my-cluster --region us-east-1
|
|
53
|
+
kubectl get nodes # verify connectivity
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 3: Deploy K8s Workloads
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Build K8s manifests
|
|
60
|
+
chant build src/k8s/ --output manifests.yaml
|
|
61
|
+
|
|
62
|
+
# Apply
|
|
63
|
+
kubectl apply -f manifests.yaml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Key K8s composites for EKS
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
NamespaceEnv,
|
|
71
|
+
AutoscaledService,
|
|
72
|
+
IrsaServiceAccount,
|
|
73
|
+
AlbIngress,
|
|
74
|
+
EbsStorageClass,
|
|
75
|
+
FluentBitAgent,
|
|
76
|
+
ExternalDnsAgent,
|
|
77
|
+
} from "@intentius/chant-lexicon-k8s";
|
|
78
|
+
|
|
79
|
+
// 1. Namespace with quotas and network isolation
|
|
80
|
+
const ns = NamespaceEnv({
|
|
81
|
+
name: "prod",
|
|
82
|
+
cpuQuota: "16",
|
|
83
|
+
memoryQuota: "32Gi",
|
|
84
|
+
defaultCpuRequest: "100m",
|
|
85
|
+
defaultMemoryRequest: "128Mi",
|
|
86
|
+
defaultDenyIngress: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 2. IRSA ServiceAccount (use IAM Role ARN from CloudFormation outputs)
|
|
90
|
+
const irsa = IrsaServiceAccount({
|
|
91
|
+
name: "app-sa",
|
|
92
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/app-role", // from CF output
|
|
93
|
+
namespace: "prod",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 3. Application with autoscaling
|
|
97
|
+
const app = AutoscaledService({
|
|
98
|
+
name: "api",
|
|
99
|
+
image: "api:1.0",
|
|
100
|
+
port: 8080,
|
|
101
|
+
maxReplicas: 10,
|
|
102
|
+
cpuRequest: "200m",
|
|
103
|
+
memoryRequest: "256Mi",
|
|
104
|
+
namespace: "prod",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 4. ALB Ingress (use ACM cert ARN from CloudFormation outputs)
|
|
108
|
+
const ingress = AlbIngress({
|
|
109
|
+
name: "api-ingress",
|
|
110
|
+
hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
|
|
111
|
+
certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/abc", // from CF output
|
|
112
|
+
namespace: "prod",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 5. Storage
|
|
116
|
+
const storage = EbsStorageClass({ name: "gp3-encrypted", type: "gp3", encrypted: true });
|
|
117
|
+
|
|
118
|
+
// 6. Observability
|
|
119
|
+
const logging = FluentBitAgent({
|
|
120
|
+
logGroup: "/aws/eks/my-cluster/containers",
|
|
121
|
+
region: "us-east-1",
|
|
122
|
+
clusterName: "my-cluster",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 7. DNS
|
|
126
|
+
const dns = ExternalDnsAgent({
|
|
127
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/external-dns-role",
|
|
128
|
+
domainFilters: ["example.com"],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Step 4: Verify
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
kubectl get pods -n prod
|
|
136
|
+
kubectl get ingress -n prod
|
|
137
|
+
kubectl logs -n amazon-cloudwatch -l app.kubernetes.io/name=fluent-bit
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Cleanup
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Delete K8s workloads first
|
|
144
|
+
kubectl delete -f manifests.yaml
|
|
145
|
+
|
|
146
|
+
# Then delete AWS infrastructure
|
|
147
|
+
aws cloudformation delete-stack --stack-name my-eks-cluster
|
|
148
|
+
aws cloudformation wait stack-delete-complete --stack-name my-eks-cluster
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Cross-Lexicon Value Flow
|
|
152
|
+
|
|
153
|
+
CloudFormation outputs flow into K8s composite props:
|
|
154
|
+
|
|
155
|
+
| CloudFormation Output | K8s Composite Prop |
|
|
156
|
+
|----------------------|-------------------|
|
|
157
|
+
| App IAM Role ARN | `IrsaServiceAccount.iamRoleArn` |
|
|
158
|
+
| ALB Controller Role ARN | `IrsaServiceAccount.iamRoleArn` (for ALB controller SA) |
|
|
159
|
+
| ACM Certificate ARN | `AlbIngress.certificateArn` |
|
|
160
|
+
| ExternalDNS Role ARN | `ExternalDnsAgent.iamRoleArn` |
|
|
161
|
+
| EKS Cluster Name | `FluentBitAgent.clusterName`, `AdotCollector.clusterName` |
|
|
162
|
+
| EFS Filesystem ID | `EfsStorageClass.fileSystemId` |
|
|
163
|
+
|
|
164
|
+
## EKS Init Template
|
|
165
|
+
|
|
166
|
+
Scaffold a dual-lexicon EKS project:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
chant init --lexicon aws --template eks
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
This creates:
|
|
173
|
+
- `src/infra/` — EKS cluster, node group, IAM (AWS lexicon)
|
|
174
|
+
- `src/k8s/` — namespace, app, ingress, storage (K8s lexicon)
|
|
175
|
+
- `package.json` with both `@intentius/chant-lexicon-aws` and `@intentius/chant-lexicon-k8s`
|