forked from cilium/cilium
-
Notifications
You must be signed in to change notification settings - Fork 7
697 lines (583 loc) · 29.6 KB
/
Copy patheks-cluster-pool-manager.yaml
File metadata and controls
697 lines (583 loc) · 29.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
name: EKS Cluster Pool Manager
# Run on schedule every 2 hours and allow manual triggering
on:
schedule:
- cron: '0 */2 * * *'
workflow_dispatch:
inputs:
min_cluster_per_region:
description: 'Number of clusters that should exist on each region'
required: false
default: '2'
type: string
# By specifying the access of one of the scopes, all of those that are not
# specified are set to 'none'.
permissions:
# To be able to access the repository with actions/checkout
contents: read
# To be able to request the JWT from GitHub's OIDC provider
id-token: write
env:
# renovate: datasource=github-releases depName=eksctl-io/eksctl
eksctl_version: v0.230.0
jobs:
generate-cleanup-matrix:
name: Generate Cleanup Matrix
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'schedule' }}
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
empty: ${{ steps.set-matrix.outputs.empty }}
steps:
- name: Checkout context ref (trusted)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Convert YAML to JSON
run: |
destination_directory="/tmp/generated/eks"
mkdir -p "${destination_directory}"
# Convert both version files to JSON
yq -o=json ".github/actions/eks/k8s-versions.yaml" | jq . > "${destination_directory}/eks.json"
yq -o=json ".github/actions/aws-cni/k8s-versions.yaml" | jq . > "${destination_directory}/aws-cni.json"
- name: Generate Matrix
id: set-matrix
run: |
cd /tmp/generated/eks
# Extract unique regions from both version files
jq -s '{ "region": ([.[].include[].region] | unique) }' eks.json aws-cni.json > /tmp/matrix.json
echo "Generated matrix:"
cat /tmp/matrix.json
echo "matrix=$(jq -c . < /tmp/matrix.json)" >> $GITHUB_OUTPUT
echo "empty=false" >> $GITHUB_OUTPUT
cleanup-old-clusters:
permissions:
# To be able to trigger eks-cluster-delete.yaml workflow
actions: write
# To be able to access the repository with actions/checkout
contents: read
# To be able to request the JWT from GitHub's OIDC provider
id-token: write
name: Cleanup clusters older than 6 hours
runs-on: ubuntu-24.04
needs: generate-cleanup-matrix
if: ${{ needs.generate-cleanup-matrix.outputs.empty == 'false' }}
timeout-minutes: 45
strategy:
matrix: ${{ fromJson(needs.generate-cleanup-matrix.outputs.matrix) }}
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_PR_ASSUME_ROLE }}
aws-region: ${{ matrix.region }}
- name: List and delete old clusters
env:
REGION: ${{ matrix.region }}
MAX_AGE_HOURS: 6
GH_TOKEN: ${{ github.token }}
run: |
set -e
echo "Checking for clusters in region: ${REGION}"
# Get current timestamp
CURRENT_TIME=$(date +%s)
HOURS_AGO=$((CURRENT_TIME - (MAX_AGE_HOURS * 3600)))
S3_BUCKET="${{ format('{0}-{1}-eks-kubeconfig-pool', github.repository_owner, github.event.repository.name) }}"
# Clean up stale lock files (older than 1 hour)
echo "Cleaning up stale lock files..."
LOCK_FILES=$(aws s3 ls "s3://${S3_BUCKET}/locks/" --recursive --region ${REGION} | awk '{print $4}' || echo "")
for lock_file in ${LOCK_FILES}; do
if [ -n "${lock_file}" ]; then
LOCK_TIMESTAMP=$(echo "${lock_file}" | grep -oP 'locks/.*-\K[0-9]+(?=\.lock)' || echo "0")
if [ "${LOCK_TIMESTAMP}" != "0" ]; then
LOCK_AGE_SECONDS=$((CURRENT_TIME - LOCK_TIMESTAMP))
if [ ${LOCK_AGE_SECONDS} -gt 3600 ]; then
echo "Removing stale lock file: ${lock_file} (age: $((LOCK_AGE_SECONDS / 60)) minutes)"
aws s3 rm "s3://${S3_BUCKET}/${lock_file}" --region ${REGION} || true
fi
fi
fi
done
# List all EKS clusters and fetch all needed information at once
CLUSTERS_JSON=$(aws eks list-clusters --region ${REGION} --output json | jq -r '.clusters[]' | \
xargs -I {} aws eks describe-cluster --region ${REGION} --name {} \
--query 'cluster.{name:name, tags:tags, createdAt:createdAt}' --output json | \
jq -s '.')
if [ -z "${CLUSTERS_JSON}" ] || [ "${CLUSTERS_JSON}" = "[]" ]; then
echo "No EKS clusters found in region ${REGION}"
exit 0
fi
# Filter clusters by usage tag and extract needed information
FILTERED_CLUSTERS=$(echo "${CLUSTERS_JSON}" | jq -r --arg tag_value "${{ github.repository_owner }}-${{ github.event.repository.name }}" \
'[.[] | select(.tags.usage == $tag_value)] | if length == 0 then [] else . end')
if [ "$(echo "${FILTERED_CLUSTERS}" | jq 'length')" -eq 0 ]; then
echo "No clusters found in region ${REGION} with our usage tag"
exit 0
fi
echo "Found $(echo "${FILTERED_CLUSTERS}" | jq 'length') clusters with our usage tag"
# Process each cluster
echo "${FILTERED_CLUSTERS}" | jq -c '.[]' | while read -r cluster_info; do
cluster=$(echo "${cluster_info}" | jq -r '.name')
CREATION_TIME=$(echo "${cluster_info}" | jq -r '.createdAt')
echo "Checking cluster: ${cluster}"
# Get cluster creation time
CREATION_TIMESTAMP=$(date -d "${CREATION_TIME}" +%s)
CLUSTER_AGE_SECONDS=$((CURRENT_TIME - CREATION_TIMESTAMP))
CLUSTER_AGE_HOURS=$((CLUSTER_AGE_SECONDS / 3600))
echo "Cluster ${cluster} age: ${CLUSTER_AGE_HOURS} hours"
# Delete clusters older than 3 hours
if [ ${CREATION_TIMESTAMP} -lt ${HOURS_AGO} ]; then
# Check if kubeconfig exists in S3 pool before deleting
# If it doesn't exist, the cluster is being used and should not be deleted
S3_BUCKET="${{ format('{0}-{1}-eks-kubeconfig-pool', github.repository_owner, github.event.repository.name) }}"
if [ -n "${S3_BUCKET}" ]; then
echo "Checking if kubeconfig for cluster ${cluster} exists in S3 bucket ${S3_BUCKET}"
KUBECONFIG_EXISTS=$(aws s3 ls "s3://${S3_BUCKET}/kubeconfig-pool/" --recursive --region ${REGION} | grep -c "${cluster}-" || true)
if [ "${KUBECONFIG_EXISTS}" -eq "0" ]; then
echo "Kubeconfig for cluster ${cluster} not found in S3 pool - cluster is being used, skipping deletion"
continue
fi
echo "Kubeconfig found in pool for cluster ${cluster} (age: ${CLUSTER_AGE_HOURS} hours), proceeding with deletion"
# Remove kubeconfig from S3 pool
echo "Removing kubeconfigs for cluster ${cluster} from S3 bucket ${S3_BUCKET}"
aws s3 rm "s3://${S3_BUCKET}/kubeconfig-pool/" --recursive --exclude "*" --include "${cluster}-*" --region ${REGION} || true
fi
# Trigger the eks-cluster-delete workflow to delete the cluster
echo "Triggering eks-cluster-delete workflow for cluster ${cluster} in region ${REGION}"
gh workflow run eks-cluster-delete.yaml \
-f cluster_name="${cluster}" \
-f region="${REGION}" || {
echo "Failed to trigger delete workflow for cluster ${cluster}, continuing..."
}
else
echo "Cluster ${cluster} is not old enough to delete (age: ${CLUSTER_AGE_HOURS} hours, threshold: ${MAX_AGE_HOURS} hours)"
fi
done
cleanup-unassociated-eips:
name: Cleanup unassociated EIPs
runs-on: ubuntu-24.04
needs: [generate-cleanup-matrix, cleanup-old-clusters]
if: ${{ always() && needs.generate-cleanup-matrix.outputs.empty == 'false' }}
timeout-minutes: 20
strategy:
matrix: ${{ fromJson(needs.generate-cleanup-matrix.outputs.matrix) }}
fail-fast: false
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_PR_ASSUME_ROLE }}
aws-region: ${{ matrix.region }}
- name: Find and release unassociated EIPs
env:
REGION: ${{ matrix.region }}
run: |
set -e
echo "Checking for unassociated EIPs in region: ${REGION}"
# Find all unassociated EIPs with names starting with "eksctl-cilium-pool"
EIPS_JSON=$(aws ec2 describe-addresses \
--region "${REGION}" \
--query "Addresses[?AssociationId==\`null\`].[AllocationId,Tags[?Key==\`Name\`].Value|[0]]" \
--output json | jq -r '.[] | select(.[1] != null and (.[1] | startswith("eksctl-cilium-pool"))) | @json')
if [ -z "${EIPS_JSON}" ]; then
echo "No unassociated EIPs found with 'eksctl-cilium-pool' prefix in region ${REGION}"
exit 0
fi
echo "${EIPS_JSON}" | while read -r eip_info; do
if [ -n "${eip_info}" ]; then
ALLOCATION_ID=$(echo "${eip_info}" | jq -r '.[0]')
aws ec2 release-address \
--region "${REGION}" \
--allocation-id "${ALLOCATION_ID}" || {
echo "Failed to release EIP ${ALLOCATION_ID}, continuing..."
}
echo "Successfully released EIP: ${ALLOCATION_ID}"
fi
done
generate-create-matrix:
name: Generate Create Matrix
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
empty: ${{ steps.set-matrix.outputs.empty }}
steps:
- name: Checkout context ref (trusted)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Convert YAML to JSON
run: |
destination_directory="/tmp/generated/eks"
mkdir -p "${destination_directory}"
# Convert both version files to JSON
yq -o=json ".github/actions/eks/k8s-versions.yaml" | jq . > "${destination_directory}/eks.json"
yq -o=json ".github/actions/aws-cni/k8s-versions.yaml" | jq . > "${destination_directory}/aws-cni.json"
- name: Generate Matrix
id: set-matrix
run: |
cd /tmp/generated/eks
# Use only default versions for pool from both sources
jq '{ "include": [ .include[] | select(.default) ] }' eks.json > /tmp/eks-defaults.json
jq '{ "include": [ .include[] | select(.default) ] }' aws-cni.json > /tmp/aws-cni-defaults.json
# Expand matrix with different addon configurations.
#
# The "zones" field MUST match what the conformance-eks.yaml /
# conformance-aws-cni.yaml consumers compute from the region, because
# setup-eks-cluster hashes "<addons> <zones>" into the pool key. If
# the pool clusters are created with a different zone set than the PR
# jobs search for, the pool key never matches, every PR leg creates a
# brand-new (NAT-gateway-consuming) cluster instead of claiming a
# pooled one, and the region eventually hits its NAT gateway quota.
# Consumer derivation (keep in sync): ZONE_1 = <region>a for us-west-1
# (us-west-1b rejects subnets), else <region>b; ZONE_2 = <region>c.
cat > /tmp/expand-addons.jq << 'EOF'
def zones: (if .region == "us-west-1" then .region + "a" else .region + "b" end) + " " + .region + "c";
{
"include": [
# all-addons configuration from aws-cni versions
(.aws_cni.include[] | . + {
"addons": "coredns kube-proxy vpc-cni",
"addons_name": "all-addons",
"zones": (. | zones),
"az_cluster": (if .region == "us-west-1" then .region + "a" else .region + "b" end),
"az_external": (.region + "c")
}),
# coredns-kubeproxy configuration from eks versions
(.eks.include[] | . + {
"addons": "coredns kube-proxy",
"addons_name": "coredns-kubeproxy",
"zones": (. | zones),
"az_cluster": (if .region == "us-west-1" then .region + "a" else .region + "b" end),
"az_external": (.region + "c")
})
]
}
EOF
jq -s '{eks: .[0], aws_cni: .[1]}' /tmp/eks-defaults.json /tmp/aws-cni-defaults.json | \
jq -f /tmp/expand-addons.jq > /tmp/matrix.json
echo "Generated matrix:"
cat /tmp/matrix.json
echo "matrix=$(jq -c . < /tmp/matrix.json)" >> $GITHUB_OUTPUT
echo "empty=$(jq '(.include | length) == 0' /tmp/matrix.json)" >> $GITHUB_OUTPUT
ensure-s3-bucket:
name: Ensure S3 bucket exists
runs-on: ubuntu-24.04
needs: generate-create-matrix
if: ${{ always() && needs.generate-create-matrix.outputs.empty == 'false' }}
timeout-minutes: 10
strategy:
matrix: ${{ fromJson(needs.generate-create-matrix.outputs.matrix) }}
fail-fast: false
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_PR_ASSUME_ROLE }}
aws-region: ${{ matrix.region }}
- name: Create S3 bucket if it doesn't exist
env:
REGION: ${{ matrix.region }}
run: |
set -e
S3_BUCKET="${{ format('{0}-{1}-eks-kubeconfig-pool', github.repository_owner, github.event.repository.name) }}"
echo "Checking if S3 bucket ${S3_BUCKET} exists in region ${REGION}"
# Check if bucket exists
if aws s3api head-bucket --bucket "${S3_BUCKET}" --region "${REGION}" 2>/dev/null; then
echo "Bucket ${S3_BUCKET} already exists"
else
echo "Bucket ${S3_BUCKET} does not exist, creating..."
# Create bucket
if [ "${REGION}" = "us-east-1" ]; then
# us-east-1 doesn't need LocationConstraint
aws s3api create-bucket \
--bucket "${S3_BUCKET}" \
--region "${REGION}"
else
aws s3api create-bucket \
--bucket "${S3_BUCKET}" \
--region "${REGION}" \
--create-bucket-configuration LocationConstraint="${REGION}"
fi
# Enable versioning
aws s3api put-bucket-versioning \
--bucket "${S3_BUCKET}" \
--region "${REGION}" \
--versioning-configuration Status=Enabled
# Set bucket tags
aws s3api put-bucket-tagging \
--bucket "${S3_BUCKET}" \
--region "${REGION}" \
--tagging "TagSet=[{Key=usage,Value=${{ github.repository_owner }}-${{ github.event.repository.name }}},{Key=managed-by,Value=github-actions}]"
echo "Bucket ${S3_BUCKET} created successfully"
fi
create-clusters:
permissions:
# To be able to trigger eks-cluster-pool-manager.yaml workflow
actions: write
# To be able to access the repository with actions/checkout
contents: read
# To be able to request the JWT from GitHub's OIDC provider
id-token: write
name: Create EKS clusters for pool
runs-on: ubuntu-24.04
needs: [cleanup-old-clusters, cleanup-unassociated-eips, generate-create-matrix, ensure-s3-bucket]
if: ${{ always() && needs.generate-create-matrix.outputs.empty == 'false' }}
timeout-minutes: 30
strategy:
matrix: ${{ fromJson(needs.generate-create-matrix.outputs.matrix) }}
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install eksctl CLI
run: |
curl -fL --retry 5 --retry-all-errors --retry-delay 3 -O "https://github.com/eksctl-io/eksctl/releases/download/${{ env.eksctl_version }}/eksctl_$(uname -s)_amd64.tar.gz"
sudo tar -xzvf "eksctl_$(uname -s)_amd64.tar.gz" -C /usr/bin
rm "eksctl_$(uname -s)_amd64.tar.gz"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_PR_ASSUME_ROLE }}
aws-region: ${{ matrix.region }}
- name: Check pool status and determine if cluster creation needed
id: check-pool
env:
REGION: ${{ matrix.region }}
VERSION: ${{ matrix.version }}
ADDONS: ${{ matrix.addons }}
ZONES: ${{ matrix.zones }}
run: |
set -e
# Set desired count based on addon configuration
# Buffer of 3x the number of clusters needed per PR:
# - coredns kube-proxy: 4 clusters/PR × 3 = 12
# - coredns kube-proxy vpc-cni: 1 cluster/PR × 3 = 3
if [[ "${ADDONS}" == "coredns kube-proxy vpc-cni" ]]; then
DESIRED_COUNT=3
else
DESIRED_COUNT=12
fi
# Compute SHA256 hash of addons and zones to differentiate pools by configuration
ADDONS_HASH=$(echo -n "${ADDONS} ${ZONES}" | sha256sum | cut -c1-8)
KUBECONFIG_PATH="${REGION}-${VERSION}-${ADDONS_HASH}"
KUBECONFIG_PATH_SAFE="${KUBECONFIG_PATH//[.\/]/-}"
S3_PREFIX="kubeconfig-pool/${KUBECONFIG_PATH_SAFE}/"
S3_BUCKET="${{ format('{0}-{1}-eks-kubeconfig-pool', github.repository_owner, github.event.repository.name) }}"
echo "Checking pool in s3://${S3_BUCKET}/${S3_PREFIX}"
# Count existing kubeconfigs in the pool
EXISTING_COUNT=$(aws s3 ls "s3://${S3_BUCKET}/${S3_PREFIX}" --region ${REGION} | wc -l || echo "0")
# Count clusters being created (lock files)
LOCK_COUNT=$(aws s3 ls "s3://${S3_BUCKET}/locks/${KUBECONFIG_PATH_SAFE}/" --region ${REGION} | wc -l || echo "0")
TOTAL_COUNT=$((EXISTING_COUNT + LOCK_COUNT))
echo "Existing clusters in pool: ${EXISTING_COUNT}"
echo "Clusters being created: ${LOCK_COUNT}"
echo "Total count: ${TOTAL_COUNT}"
echo "Desired count: ${DESIRED_COUNT}"
if [ ${TOTAL_COUNT} -ge ${DESIRED_COUNT} ]; then
echo "Pool has enough clusters or clusters being created (${TOTAL_COUNT} >= ${DESIRED_COUNT})"
echo "should_create=false" >> $GITHUB_OUTPUT
else
echo "Need to create cluster(s) for pool"
echo "should_create=true" >> $GITHUB_OUTPUT
fi
# Export variables for next steps
echo "kubeconfig_path_safe=${KUBECONFIG_PATH_SAFE}" >> $GITHUB_OUTPUT
echo "s3_bucket=${S3_BUCKET}" >> $GITHUB_OUTPUT
- name: Generate cluster name
id: cluster-name
if: ${{ steps.check-pool.outputs.should_create == 'true' }}
run: |
TIMESTAMP=$(date +%s)
# Add random suffix to prevent race conditions when multiple jobs run in parallel
RANDOM_SUFFIX=$(head -c 4 /dev/urandom | xxd -p)
CLUSTER_NAME="cilium-pool-${{ matrix.version }}-${{ matrix.addons_name }}-${TIMESTAMP}-${RANDOM_SUFFIX}"
# Sanitize cluster name (replace dots with dashes)
CLUSTER_NAME="${CLUSTER_NAME//./-}"
echo "cluster_name=${CLUSTER_NAME}" >> $GITHUB_OUTPUT
echo "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT
echo "Generated cluster name: ${CLUSTER_NAME}"
- name: Create lock file in S3
if: ${{ steps.check-pool.outputs.should_create == 'true' }}
env:
S3_BUCKET: ${{ steps.check-pool.outputs.s3_bucket }}
KUBECONFIG_PATH_SAFE: ${{ steps.check-pool.outputs.kubeconfig_path_safe }}
CLUSTER_NAME: ${{ steps.cluster-name.outputs.cluster_name }}
TIMESTAMP: ${{ steps.cluster-name.outputs.timestamp }}
REGION: ${{ matrix.region }}
run: |
set -e
LOCK_KEY="locks/${KUBECONFIG_PATH_SAFE}/${CLUSTER_NAME}-${TIMESTAMP}.lock"
echo "Creating lock file: s3://${S3_BUCKET}/${LOCK_KEY}"
# Create lock file with metadata
echo "cluster_name=${CLUSTER_NAME}" > /tmp/lock-${CLUSTER_NAME}.txt
echo "timestamp=${TIMESTAMP}" >> /tmp/lock-${CLUSTER_NAME}.txt
echo "workflow_run_id=${{ github.run_id }}" >> /tmp/lock-${CLUSTER_NAME}.txt
aws s3 cp /tmp/lock-${CLUSTER_NAME}.txt "s3://${S3_BUCKET}/${LOCK_KEY}" --region ${REGION}
echo "Lock file created successfully"
- name: Setup EKS cluster
id: setup-cluster
if: ${{ steps.check-pool.outputs.should_create == 'true' }}
uses: ./.github/actions/setup-eks-cluster
with:
create-cluster: 'true'
cluster_name: ${{ steps.cluster-name.outputs.cluster_name }}
region: ${{ matrix.region }}
zones: ${{ matrix.zones }}
owner: "eks-cluster-pool-manager"
version: ${{ matrix.version }}
addons: ${{ matrix.addons }}
- name: Create EKS nodegroups
# We can only create node groups with vpc-cni addon or if Cilium was
# already installed in the cluster.
if: ${{ steps.check-pool.outputs.should_create == 'true' && contains(matrix.addons, 'vpc-cni') }}
uses: ./.github/actions/setup-eks-nodegroup
with:
cluster_name: ${{ steps.setup-cluster.outputs.cluster_name }}
region: ${{ matrix.region }}
owner: "eks-cluster-pool-manager"
version: ${{ matrix.version }}
spot: false
nodes_without_cilium: 2
az_cluster: ${{ matrix.az_cluster }}
az_external: ${{ matrix.az_external }}
- name: Upload kubeconfig to S3 pool
if: ${{ steps.check-pool.outputs.should_create == 'true' }}
env:
REGION: ${{ matrix.region }}
VERSION: ${{ matrix.version }}
ADDONS: ${{ matrix.addons }}
ZONES: ${{ matrix.zones }}
CLUSTER_NAME: ${{ steps.setup-cluster.outputs.cluster_name }}
TIMESTAMP: ${{ steps.cluster-name.outputs.timestamp }}
S3_BUCKET: ${{ steps.check-pool.outputs.s3_bucket }}
KUBECONFIG_PATH_SAFE: ${{ steps.check-pool.outputs.kubeconfig_path_safe }}
run: |
set -e
# Compute SHA256 hash of addons and zones
ADDONS_HASH=$(echo -n "${ADDONS} ${ZONES}" | sha256sum | cut -c1-8)
KUBECONFIG_PATH="${REGION}-${VERSION}-${ADDONS_HASH}"
KUBECONFIG_PATH_SAFE="${KUBECONFIG_PATH//[.\/]/-}"
S3_PREFIX="kubeconfig-pool/${KUBECONFIG_PATH_SAFE}/"
S3_BUCKET="${{ format('{0}-{1}-eks-kubeconfig-pool', github.repository_owner, github.event.repository.name) }}"
TIMESTAMP=$(date +%s)
KUBECONFIG_KEY="${CLUSTER_NAME}-${TIMESTAMP}.yaml"
echo "Uploading kubeconfig to s3://${S3_BUCKET}/${S3_PREFIX}${KUBECONFIG_KEY}"
# Get kubeconfig
eksctl utils write-kubeconfig --cluster=${CLUSTER_NAME} --region=${REGION} --kubeconfig=/tmp/kubeconfig-${CLUSTER_NAME}.yaml
# Upload to S3
aws s3 cp /tmp/kubeconfig-${CLUSTER_NAME}.yaml "s3://${S3_BUCKET}/${S3_PREFIX}${KUBECONFIG_KEY}" --region ${REGION}
echo "Kubeconfig uploaded successfully to pool"
# Remove lock file now that cluster is ready
LOCK_KEY="locks/${KUBECONFIG_PATH_SAFE}/${CLUSTER_NAME}-${TIMESTAMP}.lock"
echo "Removing lock file: s3://${S3_BUCKET}/${LOCK_KEY}"
aws s3 rm "s3://${S3_BUCKET}/${LOCK_KEY}" --region ${REGION} || true
- name: Cleanup on failure
if: ${{ failure() && steps.check-pool.outputs.should_create == 'true' }}
env:
S3_BUCKET: ${{ steps.check-pool.outputs.s3_bucket }}
KUBECONFIG_PATH_SAFE: ${{ steps.check-pool.outputs.kubeconfig_path_safe }}
CLUSTER_NAME: ${{ steps.cluster-name.outputs.cluster_name }}
TIMESTAMP: ${{ steps.cluster-name.outputs.timestamp }}
REGION: ${{ matrix.region }}
run: |
echo "Cleaning up after failure for cluster: ${CLUSTER_NAME}"
# Remove lock file
LOCK_KEY="locks/${KUBECONFIG_PATH_SAFE}/${CLUSTER_NAME}-${TIMESTAMP}.lock"
echo "Removing lock file: s3://${S3_BUCKET}/${LOCK_KEY}"
aws s3 rm "s3://${S3_BUCKET}/${LOCK_KEY}" --region ${REGION} || true
# Delete any CloudFormation stacks created for this cluster
echo "Checking for CloudFormation stacks to clean up..."
for stack in $(aws cloudformation list-stacks --region ${REGION} \
--stack-status-filter CREATE_COMPLETE CREATE_IN_PROGRESS CREATE_FAILED ROLLBACK_COMPLETE ROLLBACK_IN_PROGRESS \
--query "StackSummaries[?starts_with(StackName, 'eksctl-${CLUSTER_NAME}')].StackName" \
--output text 2>/dev/null || echo ""); do
if [ -n "${stack}" ]; then
echo "Deleting CloudFormation stack: ${stack}"
aws cloudformation delete-stack --stack-name "${stack}" --region ${REGION} || true
fi
done
# Clean up orphaned IAM roles for this cluster
echo "Checking for orphaned IAM roles..."
for role in $(aws iam list-roles --query "Roles[?starts_with(RoleName, 'eksctl-${CLUSTER_NAME}')].RoleName" --output text 2>/dev/null || echo ""); do
if [ -n "${role}" ]; then
echo "Cleaning up IAM role: ${role}"
# Detach managed policies
for policy in $(aws iam list-attached-role-policies --role-name "${role}" --query "AttachedPolicies[].PolicyArn" --output text 2>/dev/null || echo ""); do
if [ -n "${policy}" ]; then
echo " Detaching policy: ${policy}"
aws iam detach-role-policy --role-name "${role}" --policy-arn "${policy}" || true
fi
done
# Delete inline policies
for policy in $(aws iam list-role-policies --role-name "${role}" --query "PolicyNames[]" --output text 2>/dev/null || echo ""); do
if [ -n "${policy}" ]; then
echo " Deleting inline policy: ${policy}"
aws iam delete-role-policy --role-name "${role}" --policy-name "${policy}" || true
fi
done
# Remove from instance profiles
for profile in $(aws iam list-instance-profiles-for-role --role-name "${role}" --query "InstanceProfiles[].InstanceProfileName" --output text 2>/dev/null || echo ""); do
if [ -n "${profile}" ]; then
echo " Removing from instance profile: ${profile}"
aws iam remove-role-from-instance-profile --role-name "${role}" --instance-profile-name "${profile}" || true
aws iam delete-instance-profile --instance-profile-name "${profile}" || true
fi
done
# Delete the role
echo " Deleting role: ${role}"
aws iam delete-role --role-name "${role}" || true
fi
done
echo "Cleanup complete"
report-status:
name: Report pool status
runs-on: ubuntu-24.04
needs: [generate-cleanup-matrix, cleanup-old-clusters, cleanup-unassociated-eips, create-clusters]
if: ${{ always() && needs.generate-cleanup-matrix.outputs.empty == 'false' }}
timeout-minutes: 10
strategy:
matrix: ${{ fromJson(needs.generate-cleanup-matrix.outputs.matrix) }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_PR_ASSUME_ROLE }}
aws-region: ${{ matrix.region }}
- name: Report pool status
env:
REGION: ${{ matrix.region }}
run: |
set -e
echo "=== Pool Status for ${REGION} ==="
S3_BUCKET="${{ format('{0}-{1}-eks-kubeconfig-pool', github.repository_owner, github.event.repository.name) }}"
if [ -z "${S3_BUCKET}" ]; then
echo "S3 bucket variable not configured"
exit 0
fi
echo "S3 Bucket: ${S3_BUCKET}"
echo ""
# List all kubeconfigs in pool
echo "Kubeconfigs in pool:"
aws s3 ls "s3://${S3_BUCKET}/kubeconfig-pool/" --recursive --region ${REGION} | awk '{print $4}' || echo "None"
echo ""
# List all running clusters with our usage tag
echo "Running EKS clusters:"
CLUSTERS_JSON=$(aws eks list-clusters --region ${REGION} --output json | jq -r '.clusters[]' | \
xargs -I {} aws eks describe-cluster --region ${REGION} --name {} \
--query 'cluster.{name:name, tags:tags, createdAt:createdAt}' --output json | \
jq -s '.')
# Filter and display clusters by usage tag
FILTERED_CLUSTERS=$(echo "${CLUSTERS_JSON}" | jq -r --arg tag_value "${{ github.repository_owner }}-${{ github.event.repository.name }}" \
'[.[] | select(.tags.usage == $tag_value)]')
if [ "$(echo "${FILTERED_CLUSTERS}" | jq 'length')" -eq 0 ]; then
echo "No clusters found"
else
echo "${FILTERED_CLUSTERS}" | jq -r '.[] | " - \(.name) (created: \(.createdAt))"'
fi