Skip to content

Commit 293a8de

Browse files
committed
feat: add script for adding cloudfront for s3 buckets
1 parent 67e8aba commit 293a8de

2 files changed

Lines changed: 309 additions & 1 deletion

File tree

docs/usage/connecting-to-cluster.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ eksctl get iamidentitymapping --cluster hotosm-production-cluster \
8484
### Using the role
8585

8686
This role should have access to view pods / deployment progress,
87-
but not modify things.
87+
but not modify things / exec / view secrets.
8888

8989
`~/.aws/config`
9090
```toml

scripts/add-s3-cloudfront.sh

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# -------------------------------------------------------------
5+
# Add CloudFront in front of an existing S3 bucket
6+
#
7+
# Usage:
8+
# bash add-s3-cloudfront.sh <bucket-name> [profile-name]
9+
# [--public-path <path>]...
10+
#
11+
# Example:
12+
# bash add-s3-cloudfront.sh my-bucket admin \
13+
# --public-path tutorials \
14+
# --public-path publicuploads
15+
# -------------------------------------------------------------
16+
17+
# Ensure jq available
18+
JQ_CMD=$(command -v jq 2>/dev/null || echo "/usr/bin/jq" || echo "/usr/local/bin/jq")
19+
if [[ ! -x "$JQ_CMD" ]]; then
20+
echo "Error: jq command not found. Please install jq."
21+
exit 1
22+
fi
23+
24+
if [[ $# -lt 1 ]]; then
25+
echo "Usage: $0 <bucket-name> [profile-name] [--public-path <path>]"
26+
exit 1
27+
fi
28+
29+
BUCKET="$1"
30+
PROFILE="${2:-admin}"
31+
shift || true
32+
shift || true
33+
34+
PUBLIC_PATHS=()
35+
36+
while [[ $# -gt 0 ]]; do
37+
case "$1" in
38+
--public-path)
39+
PUBLIC_PATHS+=("$2")
40+
shift 2
41+
;;
42+
*)
43+
echo "Unknown argument: $1"
44+
exit 1
45+
;;
46+
esac
47+
done
48+
49+
ACCOUNT_ID=$(aws sts get-caller-identity \
50+
--profile "$PROFILE" \
51+
--query Account \
52+
--output text)
53+
54+
echo "========================================="
55+
echo "🪣 Bucket: $BUCKET"
56+
echo "👔 Profile: $PROFILE"
57+
echo "🌐 Public CloudFront paths: ${PUBLIC_PATHS[*]:-(none)}"
58+
echo "========================================="
59+
60+
# -------------------------------------------------------------
61+
# 1. Create or retrieve Origin Access Control
62+
# -------------------------------------------------------------
63+
OAC_NAME="${BUCKET}-oac"
64+
echo "Checking for existing Origin Access Control: ${OAC_NAME}..."
65+
66+
# Try to find existing OAC
67+
EXISTING_OAC=$(aws cloudfront list-origin-access-controls \
68+
--profile "$PROFILE" \
69+
--query "OriginAccessControlList.Items[?Name=='${OAC_NAME}'].Id | [0]" \
70+
--output text 2>/dev/null || echo "None")
71+
72+
if [[ "$EXISTING_OAC" != "None" && -n "$EXISTING_OAC" ]]; then
73+
echo "✓ Found existing OAC: ${EXISTING_OAC}"
74+
OAC_ID="$EXISTING_OAC"
75+
else
76+
echo "Creating new Origin Access Control..."
77+
OAC_ID=$(aws cloudfront create-origin-access-control \
78+
--origin-access-control-config "{
79+
\"Name\": \"${OAC_NAME}\",
80+
\"Description\": \"OAC for ${BUCKET}\",
81+
\"SigningProtocol\": \"sigv4\",
82+
\"SigningBehavior\": \"always\",
83+
\"OriginAccessControlOriginType\": \"s3\"
84+
}" \
85+
--profile "$PROFILE" \
86+
--query "OriginAccessControl.Id" \
87+
--output text)
88+
echo "✓ Created OAC: ${OAC_ID}"
89+
fi
90+
91+
# -------------------------------------------------------------
92+
# 2. Create or retrieve cache policy for presigned URLs
93+
# -------------------------------------------------------------
94+
CACHE_POLICY_NAME="${BUCKET}-presigned-cache"
95+
echo "Checking for existing cache policy: ${CACHE_POLICY_NAME}..."
96+
97+
EXISTING_POLICY=$(aws cloudfront list-cache-policies \
98+
--profile "$PROFILE" \
99+
--query "CachePolicyList.Items[?CachePolicy.CachePolicyConfig.Name=='${CACHE_POLICY_NAME}'].CachePolicy.Id | [0]" \
100+
--output text 2>/dev/null || echo "None")
101+
102+
if [[ "$EXISTING_POLICY" != "None" && -n "$EXISTING_POLICY" ]]; then
103+
echo "✓ Found existing cache policy: ${EXISTING_POLICY}"
104+
CACHE_POLICY_ID="$EXISTING_POLICY"
105+
else
106+
echo "Creating cache policy for presigned URLs..."
107+
CACHE_POLICY_ID=$(aws cloudfront create-cache-policy \
108+
--cache-policy-config "{
109+
\"Name\": \"${CACHE_POLICY_NAME}\",
110+
\"Comment\": \"Low-TTL cache for presigned URLs and COG imagery\",
111+
\"MinTTL\": 0,
112+
\"DefaultTTL\": 300,
113+
\"MaxTTL\": 3600,
114+
\"ParametersInCacheKeyAndForwardedToOrigin\": {
115+
\"EnableAcceptEncodingGzip\": false,
116+
\"EnableAcceptEncodingBrotli\": false,
117+
\"QueryStringsConfig\": {
118+
\"QueryStringBehavior\": \"all\"
119+
},
120+
\"HeadersConfig\": {
121+
\"HeaderBehavior\": \"whitelist\",
122+
\"Headers\": {
123+
\"Quantity\": 3,
124+
\"Items\": [\"Origin\", \"Access-Control-Request-Method\", \"Access-Control-Request-Headers\"]
125+
}
126+
},
127+
\"CookiesConfig\": {
128+
\"CookieBehavior\": \"none\"
129+
}
130+
}
131+
}" \
132+
--profile "$PROFILE" \
133+
--query "CachePolicy.Id" \
134+
--output text)
135+
echo "✓ Created cache policy: ${CACHE_POLICY_ID}"
136+
fi
137+
138+
# -------------------------------------------------------------
139+
# 3. Build CloudFront behaviors
140+
# -------------------------------------------------------------
141+
142+
DEFAULT_BEHAVIOR=$(cat <<EOF
143+
{
144+
"TargetOriginId": "S3-${BUCKET}",
145+
"ViewerProtocolPolicy": "redirect-to-https",
146+
"AllowedMethods": {
147+
"Quantity": 3,
148+
"Items": ["GET", "HEAD", "OPTIONS"],
149+
"CachedMethods": {
150+
"Quantity": 2,
151+
"Items": ["GET", "HEAD"]
152+
}
153+
},
154+
"CachePolicyId": "${CACHE_POLICY_ID}",
155+
"OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf",
156+
"Compress": false
157+
}
158+
EOF
159+
)
160+
161+
if [[ ${#PUBLIC_PATHS[@]} -gt 0 ]]; then
162+
PATHS_JSON=$(printf '%s\n' "${PUBLIC_PATHS[@]}" | "$JQ_CMD" -R . | "$JQ_CMD" -s .)
163+
164+
ORDERED_BEHAVIORS=$(
165+
"$JQ_CMD" -n \
166+
--arg bucket "$BUCKET" \
167+
--argjson paths "$PATHS_JSON" \
168+
'{
169+
Quantity: ($paths | length),
170+
Items: [
171+
$paths[] | {
172+
PathPattern: ("/" + . + "/*"),
173+
TargetOriginId: ("S3-" + $bucket),
174+
ViewerProtocolPolicy: "redirect-to-https",
175+
AllowedMethods: {
176+
Quantity: 3,
177+
Items: ["GET", "HEAD", "OPTIONS"],
178+
CachedMethods: {
179+
Quantity: 2,
180+
Items: ["GET", "HEAD"]
181+
}
182+
},
183+
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
184+
OriginRequestPolicyId: "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf",
185+
Compress: false
186+
}
187+
]
188+
}'
189+
)
190+
else
191+
ORDERED_BEHAVIORS='{"Quantity":0}'
192+
fi
193+
194+
# -------------------------------------------------------------
195+
# 4. Create or retrieve CloudFront distribution
196+
# -------------------------------------------------------------
197+
echo "Checking for existing CloudFront distribution for bucket: ${BUCKET}..."
198+
199+
# Find distribution with matching origin
200+
EXISTING_DIST=$(aws cloudfront list-distributions \
201+
--profile "$PROFILE" \
202+
--query "DistributionList.Items[?Origins.Items[?DomainName=='${BUCKET}.s3.amazonaws.com']].Id | [0]" \
203+
--output text 2>/dev/null || echo "None")
204+
205+
if [[ "$EXISTING_DIST" != "None" && -n "$EXISTING_DIST" ]]; then
206+
echo "✓ Found existing distribution: ${EXISTING_DIST}"
207+
DIST_ID="$EXISTING_DIST"
208+
209+
# Get existing distribution details
210+
DIST_JSON=$(aws cloudfront get-distribution \
211+
--id "$DIST_ID" \
212+
--profile "$PROFILE")
213+
214+
CLOUDFRONT_DOMAIN=$(echo "$DIST_JSON" | "$JQ_CMD" -r .Distribution.DomainName)
215+
ETAG=$(echo "$DIST_JSON" | "$JQ_CMD" -r .ETag)
216+
217+
echo "⚠️ Distribution already exists. To update it, you would need to modify the config."
218+
echo " For now, using existing distribution as-is."
219+
else
220+
echo "Creating new CloudFront distribution..."
221+
DIST_JSON=$(aws cloudfront create-distribution \
222+
--profile "$PROFILE" \
223+
--distribution-config "{
224+
\"CallerReference\": \"$(date +%s)\",
225+
\"Comment\": \"CDN for ${BUCKET} bucket\",
226+
\"Origins\": {
227+
\"Quantity\": 1,
228+
\"Items\": [{
229+
\"Id\": \"S3-${BUCKET}\",
230+
\"DomainName\": \"${BUCKET}.s3.amazonaws.com\",
231+
\"S3OriginConfig\": {
232+
\"OriginAccessIdentity\": \"\"
233+
},
234+
\"OriginAccessControlId\": \"${OAC_ID}\"
235+
}]
236+
},
237+
\"DefaultCacheBehavior\": ${DEFAULT_BEHAVIOR},
238+
\"CacheBehaviors\": ${ORDERED_BEHAVIORS},
239+
\"Enabled\": true
240+
}")
241+
242+
DIST_ID=$(echo "$DIST_JSON" | "$JQ_CMD" -r .Distribution.Id)
243+
CLOUDFRONT_DOMAIN=$(echo "$DIST_JSON" | "$JQ_CMD" -r .Distribution.DomainName)
244+
echo "✓ Created distribution: ${DIST_ID}"
245+
fi
246+
247+
# -------------------------------------------------------------
248+
# 5. Apply S3 bucket policy (CloudFront-only access)
249+
# -------------------------------------------------------------
250+
echo "Applying bucket policy..."
251+
aws s3api put-bucket-policy \
252+
--bucket "$BUCKET" \
253+
--profile "$PROFILE" \
254+
--policy "{
255+
\"Version\": \"2012-10-17\",
256+
\"Statement\": [{
257+
\"Effect\": \"Allow\",
258+
\"Principal\": {
259+
\"Service\": \"cloudfront.amazonaws.com\"
260+
},
261+
\"Action\": \"s3:GetObject\",
262+
\"Resource\": \"arn:aws:s3:::${BUCKET}/*\",
263+
\"Condition\": {
264+
\"StringEquals\": {
265+
\"AWS:SourceArn\": \"arn:aws:cloudfront::${ACCOUNT_ID}:distribution/${DIST_ID}\"
266+
}
267+
}
268+
}]
269+
}"
270+
271+
echo "✓ Bucket policy applied"
272+
273+
# -------------------------------------------------------------
274+
# 6. Output
275+
# -------------------------------------------------------------
276+
echo
277+
echo "✅ CloudFront setup complete!"
278+
echo "-----------------------------------------"
279+
echo "Distribution ID: ${DIST_ID}"
280+
echo "CloudFront URL:"
281+
echo "https://${CLOUDFRONT_DOMAIN}/"
282+
echo
283+
284+
if [[ "$EXISTING_DIST" == "None" ]]; then
285+
echo "⏳ Distribution is now deploying (15-30 minutes)"
286+
echo " Check status in AWS Console or run:"
287+
echo " aws cloudfront get-distribution --id ${DIST_ID} --profile ${PROFILE}"
288+
else
289+
echo "ℹ️ Using existing distribution (already deployed)"
290+
fi
291+
echo
292+
293+
if [[ ${#PUBLIC_PATHS[@]} -gt 0 ]]; then
294+
echo "🌍 Public paths (aggressive caching):"
295+
for PATH in "${PUBLIC_PATHS[@]}"; do
296+
echo " https://${CLOUDFRONT_DOMAIN}/${PATH}/"
297+
done
298+
echo
299+
fi
300+
301+
echo "-----------------------------------------"
302+
echo "📝 Configuration:"
303+
echo " • Default paths: 5-min cache (presigned URLs, COG)"
304+
echo " • Public paths: 24-hour cache (static content)"
305+
echo " • Query strings: Forwarded (presigned URLs work)"
306+
echo " • Range requests: ✓ Enabled (COG tiles work)"
307+
echo " • Methods: GET, HEAD, OPTIONS (CORS enabled)"
308+
echo " • Compression: Disabled (preserves imagery)"

0 commit comments

Comments
 (0)