aws2-7 Exercise 1: An eksctl Config Checked Offline, and the Version Calendar ======================================================================== Run with Windows 11, Python 3.14 (py launcher) and a venv with jsonschema 4.26.0, PyYAML, cfn-lint 1.57.2; eksctl 0.231.0 and kind 0.31.0 downloaded as binaries into C:/lwaws/c7/bin (SHA-256 checked against the published checksums; not installed); Node 24.18.0 with aws-cdk 2.1145.0, aws-cdk-lib 2.273.0, constructs 10.8.1, typescript 7.0.2, jest 30.5.2 (the chapter 5 install, reached from a junction); kubectl client 1.32.2 (Docker Desktop's). Scratch folder C:/lwaws/c7, nothing installed globally. VERIFICATION LEVEL: 1 for the offline checks below (no AWS account, no credentials, no cluster created). A kind cluster (Kubernetes v1.35.0) was created but Docker Desktop's engine stopped before any experiment ran, so no Kubernetes API server result is reported. Outputs below are saved from the real runs of 2026-10-13. --- cluster.yaml --- apiVersion: eksctl.io/v1alpha5 kind: ClusterConfig metadata: name: demo region: eu-west-1 version: "1.35" availabilityZones: ["eu-west-1a", "eu-west-1b"] iam: withOIDC: true managedNodeGroups: - name: ng-1 instanceType: t3.medium desiredCapacity: 2 minSize: 1 maxSize: 3 privateNetworking: true --- eksctl create cluster -f cluster.yaml --dry-run (saved error; exit status 1) --- Error: checking AWS STS access – cannot get role ARN for current session: operation error STS: GetCallerIdentity, get identity: get credentials: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, access disabled to EC2 IMDS via client option, or "AWS_EC2_METADATA_DISABLED" environment variable --- how the schema was obtained: eksctl utils schema > out/eksctl_schema.json (153,659 bytes, JSON Schema draft 7) --- --- validate_eksctl.py --- """Validate an eksctl ClusterConfig against the JSON schema that eksctl itself prints (eksctl utils schema, v0.231.0), no AWS access needed. Then change one thing at a time. Uses jsonschema (Draft 7).""" import copy, json, yaml, jsonschema schema = json.load(open("out/eksctl_schema.json", encoding="utf-8")) base = yaml.safe_load(open("cluster.yaml", encoding="utf-8")) val = jsonschema.Draft7Validator(schema) def check(doc): errs = sorted(val.iter_errors(doc), key=lambda e: list(e.path)) return errs def mut(f): d = copy.deepcopy(base); f(d); return d cases = [ ("the file as written", base), ("version written as a YAML number, 1.35", mut(lambda d: d["metadata"].update(version=1.35))), ("version 1.30 written unquoted (YAML reads it as 1.3)", mut(lambda d: d["metadata"].update(version=yaml.safe_load("1.30")))), ("misspelt key managedNodeGroup", mut(lambda d: d.update(managedNodeGroup=d.pop("managedNodeGroups")))), ("desiredCapacity 'two'", mut(lambda d: d["managedNodeGroups"][0].update(desiredCapacity="two"))), ("an unknown field on the node group", mut(lambda d: d["managedNodeGroups"][0].update(colour="blue"))), ("minSize 5 above maxSize 3", mut(lambda d: d["managedNodeGroups"][0].update(minSize=5))), ("instanceType 't3.huge'", mut(lambda d: d["managedNodeGroups"][0].update(instanceType="t3.huge"))), ("region 'eu-west-9'", mut(lambda d: d["metadata"].update(region="eu-west-9"))), ("withOIDC 'yes'", mut(lambda d: d["iam"].update(withOIDC="yes"))), ("kind 'ClusterConfigs'", mut(lambda d: d.update(kind="ClusterConfigs"))), ] print("yaml.safe_load('1.30') ->", repr(yaml.safe_load("1.30")), "| yaml.safe_load('\"1.30\"') ->", repr(yaml.safe_load('"1.30"'))) print("%-56s %s" % ("change", "schema validation")) for label, d in cases: e = check(d) print("%-56s %s" % (label, ("%d error(s): %s" % (len(e), e[0].message[:90].replace("\n", " "))) if e else "valid")) --- python validate_eksctl.py (saved output of the real run) --- yaml.safe_load('1.30') -> 1.3 | yaml.safe_load('"1.30"') -> '1.30' change schema validation the file as written valid version written as a YAML number, 1.35 1 error(s): 1.35 is not of type 'string' version 1.30 written unquoted (YAML reads it as 1.3) 1 error(s): 1.3 is not of type 'string' misspelt key managedNodeGroup 1 error(s): Additional properties are not allowed ('managedNodeGroup' was unexpected) desiredCapacity 'two' 1 error(s): 'two' is not of type 'integer' an unknown field on the node group 1 error(s): Additional properties are not allowed ('colour' was unexpected) minSize 5 above maxSize 3 valid instanceType 't3.huge' valid region 'eu-west-9' valid withOIDC 'yes' 1 error(s): 'yes' is not of type 'boolean' kind 'ClusterConfigs' 1 error(s): 'ClusterConfigs' is not one of ['ClusterConfig'] --- versions.py --- """EKS version lifecycle from the 'Kubernetes version lifecycle on EKS' page (read 2026-10-13). Rule stated on the page: standard support = 14 months from the EKS release date, then extended support for 12 more months (26 in all). The page's release calendar is used as the test data: the rule is applied to each row's EKS release date and compared with the printed end dates.""" import datetime as dt def add_months(d, n): y, m = divmod(d.month - 1 + n, 12) return d.replace(year=d.year + y, month=m + 1) D = lambda s: dt.datetime.strptime(s, "%B %d, %Y").date() CAL = [ # version, EKS release, end of standard, end of extended (as printed) ("1.37", "October 1, 2026", "December 1, 2027", "December 1, 2028"), ("1.36", "June 2, 2026", "August 2, 2027", "August 2, 2028"), ("1.35", "January 27, 2026", "March 27, 2027", "March 27, 2028"), ("1.34", "October 2, 2025", "December 2, 2026", "December 2, 2027"), ("1.33", "May 29, 2025", "July 29, 2026", "July 29, 2027"), ("1.32", "January 23, 2025", "March 23, 2026", "March 23, 2027"), ("1.31", "September 26, 2024", "November 26, 2025", "November 26, 2026"), ] STD_H, EXT_H = 0.10, 0.60 # $ per cluster-hour, standard / extended (pricing page read 2026-10-13, no Region stated) def status(version, today): for v, rel, es, ee in CAL: if v == version: if today < D(rel): return "not released" if today < D(es): return "standard" if today < D(ee): return "extended" return "ended (auto-upgraded)" return "not in calendar" if __name__ == "__main__": bad = 0 for v, rel, es, ee in CAL: s = add_months(D(rel), 14); e = add_months(s, 12) ok = (s == D(es)) and (e == D(ee)) bad += not ok print("%-4s release %-18s -> standard ends %s (printed %s) extended ends %s (printed %s) %s" % (v, rel, s, D(es), e, D(ee), "ok" if ok else "MISMATCH")) print("rows where the 14 + 12 month rule disagrees with the page:", bad) today = dt.date(2026, 10, 13) print("\nstatus on", today) for v, *_ in CAL: st = status(v, today) days = (D([r for r in CAL if r[0] == v][0][2]) - today).days print(" %s %-10s %s" % (v, st, ("(standard ends in %d days)" % days) if st == "standard" else "")) H = 730 print("\ncontrol plane fee for one cluster for one month (730 h): standard $%.2f, extended $%.2f" % (H * STD_H, H * EXT_H)) print("the cluster.yaml first drafted for this chapter used version 1.33, which is in:", status("1.33", today)) --- py versions.py (saved output of the real run) --- 1.37 release October 1, 2026 -> standard ends 2027-12-01 (printed 2027-12-01) extended ends 2028-12-01 (printed 2028-12-01) ok 1.36 release June 2, 2026 -> standard ends 2027-08-02 (printed 2027-08-02) extended ends 2028-08-02 (printed 2028-08-02) ok 1.35 release January 27, 2026 -> standard ends 2027-03-27 (printed 2027-03-27) extended ends 2028-03-27 (printed 2028-03-27) ok 1.34 release October 2, 2025 -> standard ends 2026-12-02 (printed 2026-12-02) extended ends 2027-12-02 (printed 2027-12-02) ok 1.33 release May 29, 2025 -> standard ends 2026-07-29 (printed 2026-07-29) extended ends 2027-07-29 (printed 2027-07-29) ok 1.32 release January 23, 2025 -> standard ends 2026-03-23 (printed 2026-03-23) extended ends 2027-03-23 (printed 2027-03-23) ok 1.31 release September 26, 2024 -> standard ends 2025-11-26 (printed 2025-11-26) extended ends 2026-11-26 (printed 2026-11-26) ok rows where the 14 + 12 month rule disagrees with the page: 0 status on 2026-10-13 1.37 standard (standard ends in 414 days) 1.36 standard (standard ends in 293 days) 1.35 standard (standard ends in 165 days) 1.34 standard (standard ends in 50 days) 1.33 extended 1.32 extended 1.31 extended control plane fee for one cluster for one month (730 h): standard $73.00, extended $438.00 the cluster.yaml first drafted for this chapter used version 1.33, which is in: extended WHY THIS WORKS AS AN ANSWER --------------------------- eksctl's own schema is a source of truth that needs no AWS access, and the experiment measures what it covers instead of assuming: 7 of 10 changes were caught (wrong types, unknown fields, a number-typed version, the unquoted 1.30 that YAML reads as 1.3, a bad kind), 3 were not (minSize above maxSize, an instance type that does not exist, a Region that does not exist). The dry run needs STS access, so even that cannot be used offline. The 14 + 12 month rule from the lifecycle page reproduced all 7 printed pairs of dates, and applying it to 2026-10-13 showed that the first draft's version (1.33) was already in extended support at 6 times the standard control plane fee. Limits of the answer: the calendar and prices were read on 2026-10-13 through a page-fetch tool that summarises pages; the control plane fee page states no Region; the schema is for eksctl 0.231.0; nothing was created in AWS.