Repository navigation
Expand file tree
/
Copy pathbuildspec.yml
More file actions
259 lines (241 loc) · 14.3 KB
/
Copy pathbuildspec.yml
File metadata and controls
259 lines (241 loc) · 14.3 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
# Copyright (c) 2026 GitOps Manager, S.L. All rights reserved.
version: 0.2
# -----------------------------------------------------------------------------
# EKS Manager Bootstrap — AWS CodeBuild buildspec
# -----------------------------------------------------------------------------
# Sourced from S3 (see iam/codebuild-pipeline-tf/main.tf) — setup-pipeline.sh/
# .ps1 clones your private copy with a GitHub App token, zips it, and uploads it here.
# CodeBuild itself never touches GitHub. The server supplies presigned S3
# URLs, SSO ARN and a bearer token via POST /bootstrap/aws.
#
# Entra SAML SSO is set up separately, outside this pipeline — see
# the gitopsmanager-identity-bootstrap repository.
#
# Environment variables (set on the CodeBuild project):
# EKSMANAGER_CLIENT_ID ← M2M client ID (from Settings → Terraform tile)
# EKSMANAGER_COGNITO_URL ← Cognito token endpoint
# EKSMANAGER_API_URL ← EKS Manager API URL
#
# Management/shared-services account+region and the agent's name/subnet/
# security group/AMI are NOT CodeBuild env vars and are not in topology.json --
# they're pinned once via setup-pipeline.sh (Settings → Terraform tile's
# Generate button), written to pinned.auto.tfvars.json, and committed straight
# into this repo. Terraform loads that file automatically. To change any of
# them, re-run setup-pipeline.sh -- not this buildspec, not a GUI edit.
#
# Secrets Manager (set once):
# /EKSManagerBootstrap/client-m2m-cognito-secret ← M2M client secret
# -----------------------------------------------------------------------------
env:
secrets-manager:
EKSMANAGER_CLIENT_SECRET: "/EKSManagerBootstrap/client-m2m-cognito-secret"
phases:
install:
commands:
# Egress check, first thing. This project is VPC-attached, so it has
# whatever internet access its subnet's route table gives it and no more
# -- with no NAT gateway everything outbound fails, and the first symptom
# used to be a bare "wget: unable to resolve host" on the Terraform
# download, which reads like a mirror problem rather than a network one.
#
# checkip.amazonaws.com is AWS-run and returns the caller's public IP as
# plain text, so this doubles as the whitelisting check: the address it
# prints is what the EKS Manager API sees.
- |
EGRESS_ERR=$(mktemp)
EGRESS_IP=$(curl -sS --connect-timeout 10 --max-time 20 https://checkip.amazonaws.com 2>"${EGRESS_ERR}") || EGRESS_IP=""
EGRESS_IP=$(printf '%s' "${EGRESS_IP}" | tr -d ' \t\r\n')
case "${EGRESS_IP}" in
""|*[!0-9.]*)
echo "=============================================================="
echo "ERROR: no internet egress from this CodeBuild container."
echo " curl said: $(cat "${EGRESS_ERR}" 2>/dev/null || echo '<no output>')"
echo ""
echo " This project is attached to a VPC, so it can only reach what"
echo " its subnet routes to. A private subnet with no NAT gateway"
echo " has no outbound path at all."
echo ""
echo " Check, for the subnet in the project's VPC config:"
echo " - a route for 0.0.0.0/0 to a NAT gateway"
echo " - the NAT gateway is in a PUBLIC subnet with an IGW route"
echo " - the security group allows outbound 443"
echo " - NACLs allow return traffic on ephemeral ports"
echo "=============================================================="
rm -f "${EGRESS_ERR}"
exit 1
;;
*)
rm -f "${EGRESS_ERR}"
echo "OK: internet egress confirmed, public IP ${EGRESS_IP}"
;;
esac
- TERRAFORM_VERSION="1.11.0"
- |
wget -q -O terraform.zip \
"https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip"
- unzip -q terraform.zip -d /tmp/terraform-install && mv /tmp/terraform-install/terraform /usr/local/bin/ && rm terraform.zip
- terraform version
pre_build:
commands:
# Confirm AWS runner identity
- aws sts get-caller-identity
- "echo 'OK: AWS identity confirmed'"
# ── Get M2M bearer token and validate topology ────────────────────────
# Combined into one script so TOKEN is set and used in the same shell
# process -- CODEBUILD_ENV_FILE isn't populated in every CodeBuild
# environment, so it can't be relied on to pass it to a separate
# commands: list item.
# Comments live out here, at YAML level, NOT inside the `- |` block:
# CodeBuild echoes the whole block verbatim as the "command" it is
# running, so anything in there is reprinted into the log ahead of the
# actual output and buries it.
#
# Status and body are captured rather than discarded. This was
# `curl -fsSL`, where -f fails silently on an HTTP error and -s hides the
# body, so invalid credentials, a wrong Cognito URL and an unroutable
# subnet all produced the same bare "Failed to obtain M2M bearer token".
#
# --retry covers genuine transients only: curl does not retry a 4xx, so
# bad credentials still fail at once rather than three times slower.
#
# Cognito's error body names the cause (invalid_client,
# unauthorized_client, invalid_grant) and carries no secret -- the
# request holds the secret, the reply does not.
- |
TOKEN_BODY=$(mktemp)
TOKEN_STATUS=$(curl -sS -o "${TOKEN_BODY}" -w '%{http_code}' \
--connect-timeout 10 --max-time 30 --retry 3 --retry-delay 2 \
-X POST "${EKSMANAGER_COGNITO_URL}/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=${EKSMANAGER_CLIENT_ID}&client_secret=${EKSMANAGER_CLIENT_SECRET}") \
|| TOKEN_STATUS="000"
CURL_RC=$?
TOKEN=$(grep -o '"access_token":"[^"]*"' "${TOKEN_BODY}" | cut -d'"' -f4)
if [ -z "${TOKEN}" ] || [ "${TOKEN}" = "null" ]; then
echo "=============================================================="
echo "ERROR: could not get the M2M bearer token (HTTP ${TOKEN_STATUS})"
echo " endpoint : ${EKSMANAGER_COGNITO_URL}/oauth2/token"
echo " client id: ${EKSMANAGER_CLIENT_ID}"
echo " response : $(cat "${TOKEN_BODY}")"
case "${TOKEN_STATUS}" in
000) echo " CAUSE: no HTTP response -- CodeBuild never reached Cognito."
echo " This project is VPC-attached; its subnet needs egress." ;;
400|401) echo " CAUSE: Cognito rejected the credentials. Check EKSMANAGER_CLIENT_ID,"
echo " the Secrets Manager secret /EKSManagerBootstrap/client-m2m-cognito-secret,"
echo " and that client_credentials is enabled on the app client." ;;
404) echo " CAUSE: endpoint not found -- EKSMANAGER_COGNITO_URL may have a trailing slash." ;;
*) echo " CAUSE: unexpected status -- see the response above." ;;
esac
echo "=============================================================="
rm -f "${TOKEN_BODY}"
exit 1
fi
rm -f "${TOKEN_BODY}"
echo "OK: M2M bearer token obtained"
API_ERR=$(mktemp)
HTTP_STATUS=$(curl -sSL -X POST "${EKSMANAGER_API_URL}/bootstrap/aws" \
--connect-timeout 10 --max-time 60 \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d @topology.json \
-o terraform.tfvars.json \
-w "%{http_code}" 2>"${API_ERR}") || HTTP_STATUS="000"
API_RC=$?
if [ "${HTTP_STATUS}" != "200" ]; then
echo "=============================================================="
echo "ERROR: topology.json validation failed (HTTP ${HTTP_STATUS})"
echo " endpoint : ${EKSMANAGER_API_URL}/bootstrap/aws"
echo " curl rc : ${API_RC}"
echo " curl says: $(cat "${API_ERR}" 2>/dev/null || echo '<none>')"
echo " response : $(cat terraform.tfvars.json 2>/dev/null)"
# curl's exit code splits the 000 case in two, and they are not the
# same problem: 7/28 means the packets never got anywhere, 52 means
# the server answered the TCP handshake and then closed without
# sending a response -- which is the server being unhealthy, not the
# network refusing to carry it.
case "${HTTP_STATUS}" in
000)
case "${API_RC}" in
52) echo " CAUSE: connected to the API and got an EMPTY REPLY."
echo " TLS succeeded, so egress and the allowlist are fine --"
echo " the server accepted the connection and closed it without"
echo " answering. That is the API host, not the network:"
echo " - is the app running, and has it restarted recently?"
echo " - are the load balancer's targets healthy?"
echo " - check the app log for the moment of this build." ;;
7|28) echo " CAUSE: never reached the API (curl ${API_RC})."
echo " Cognito was reachable a moment ago, so general egress works;"
echo " this is specific to the API host."
echo " Most likely its allowlist does not include this build's public"
echo " IP (printed as 'internet egress confirmed' in the INSTALL phase)."
echo " A network-level block drops packets, which shows as a timeout"
echo " rather than 403. Also check EKSMANAGER_API_URL host and port." ;;
35|60) echo " CAUSE: TLS failed (curl ${API_RC}) -- certificate or protocol." ;;
*) echo " CAUSE: no HTTP response, curl exit ${API_RC}. See curl says above." ;;
esac ;;
401|403) echo " CAUSE: the API rejected the token or this caller. Check the client's"
echo " entitlement and that this build's public IP is allowlisted." ;;
400|422) echo " CAUSE: topology.json was rejected. See the response above." ;;
502|503|504) echo " CAUSE: the load balancer answered but no healthy target did."
echo " Check target group health for the API." ;;
*) echo " CAUSE: unexpected status ${HTTP_STATUS} -- see the response above." ;;
esac
echo "=============================================================="
rm -f "${API_ERR}"
exit 1
fi
rm -f "${API_ERR}"
echo "OK: topology.json validated (HTTP 200) — terraform.tfvars.json received"
- cp terraform.tfvars.json pinned.auto.tfvars.json aws/
- |
SHARED_SERVICES_ACCOUNT_ID=$(python3 -c "import json; print(json.load(open('pinned.auto.tfvars.json'))['shared_services_account_id'])")
SHARED_SERVICES_REGION=$(python3 -c "import json; print(json.load(open('pinned.auto.tfvars.json'))['shared_services_region'])")
STATE_BUCKET="eksmanager-bootstrap-${SHARED_SERVICES_ACCOUNT_ID}"
terraform -chdir=aws init -input=false \
-backend-config="bucket=${STATE_BUCKET}" \
-backend-config="region=${SHARED_SERVICES_REGION}"
- "echo 'OK: terraform init complete'"
build:
commands:
- |
set -eu
SHARED_SERVICES_ACCOUNT_ID=$(python3 -c "import json; print(json.load(open('aws/pinned.auto.tfvars.json'))['shared_services_account_id'])")
SHARED_SERVICES_REGION=$(python3 -c "import json; print(json.load(open('aws/pinned.auto.tfvars.json'))['shared_services_region'])")
if [ "${DESTROY_MODE:-}" = "true" ]; then
echo "DESTROY_MODE=true — tearing down the aws/ module."
CONFIG_BUCKET="eks-manager-config-store-${SHARED_SERVICES_ACCOUNT_ID}"
VERSIONS_JSON=$(aws s3api list-object-versions --bucket "${CONFIG_BUCKET}" \
--output json --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' 2>/dev/null || echo '{}')
if [ "$(echo "$VERSIONS_JSON" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d.get("Objects") or []))')" != "0" ]; then
aws s3api delete-objects --bucket "${CONFIG_BUCKET}" --delete "$VERSIONS_JSON" >/dev/null
fi
MARKERS_JSON=$(aws s3api list-object-versions --bucket "${CONFIG_BUCKET}" \
--output json --query '{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}' 2>/dev/null || echo '{}')
if [ "$(echo "$MARKERS_JSON" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d.get("Objects") or []))')" != "0" ]; then
aws s3api delete-objects --bucket "${CONFIG_BUCKET}" --delete "$MARKERS_JSON" >/dev/null
fi
IMAGE_IDS=$(aws ecr list-images --repository-name eksmanager --region "${SHARED_SERVICES_REGION}" \
--output json --query 'imageIds' 2>/dev/null || echo '[]')
if [ "$(echo "$IMAGE_IDS" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))')" != "0" ]; then
aws ecr batch-delete-image --repository-name eksmanager --region "${SHARED_SERVICES_REGION}" \
--image-ids "$IMAGE_IDS" >/dev/null
fi
terraform -chdir=aws destroy -input=false -auto-approve
echo "OK: terraform destroy complete"
else
terraform -chdir=aws plan -input=false -out=tfplan
echo "OK: terraform plan complete"
terraform -chdir=aws apply -input=false tfplan
echo "OK: terraform apply complete"
# Connect the agent to the server over PrivateLink. Skips itself when
# EKSMANAGER_ENDPOINT_SERVICE_NAME is unset, so installations not
# using it are unaffected.
#
# After the apply on purpose: the agent's VPC and subnet must exist,
# and the script proves the endpoint answers before creating the
# private hosted zone that shadows the server's hostname.
bash scripts/setup_agent_privatelink.sh aws/pinned.auto.tfvars.json
fi
post_build:
commands:
- echo "Bootstrap complete."