Skip to content

Commit 7a1ec14

Browse files
committed
feat: implement YAML-based deployment configuration with automatic DNS setup
- Replace GitHub Variables with YAML configuration files - Add sam build step to pipeline (was missing, causing failures) - Add CAPABILITY_AUTO_EXPAND to match local deployment - Create local deployment script (deploy.py) for testing - Add automatic DNS nameserver configuration in pipeline - Configure custom domain (portal.aillc.link) with TLS - Add Makefile commands for local deployment - Remove .claude/settings.local.json from tracking This eliminates the need for GitHub Secrets/Variables by using OIDC authentication and YAML-based configuration. The pipeline now automatically configures Route53 DNS when deploying with custom domains. Closes awslabs#635
1 parent 6fd48ba commit 7a1ec14

File tree

7 files changed

+339
-16
lines changed

7 files changed

+339
-16
lines changed

.claude/settings.local.json

Lines changed: 0 additions & 8 deletions
This file was deleted.

.github/workflows/api-portal-deploy.yaml

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,17 @@ jobs:
172172
fi
173173
done < <(grep -A 50 "^parameters:" $CONFIG_FILE | tail -n +2)
174174
175-
# Package the application
176-
sam package \
175+
# Build the SAM application
176+
echo "🔨 Building SAM application..."
177+
sam build \
177178
--template-file cloudformation/template.yaml \
178-
--s3-bucket ${ARTIFACTS_BUCKET} \
179-
--output-template-file packaged.yaml
179+
--use-container
180180
181181
# Deploy the application
182182
sam deploy \
183-
--template-file packaged.yaml \
183+
--template-file .aws-sam/build/template.yaml \
184184
--stack-name ${STACK_NAME} \
185-
--capabilities CAPABILITY_NAMED_IAM \
185+
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
186186
--region ${AWS_REGION} \
187187
--s3-bucket ${ARTIFACTS_BUCKET} \
188188
--no-fail-on-empty-changeset \
@@ -201,6 +201,116 @@ jobs:
201201
echo "🌐 Developer Portal URL: ${WEBSITE_URL}"
202202
echo "WEBSITE_URL=${WEBSITE_URL}" >> $GITHUB_ENV
203203
204+
- name: Configure DNS nameservers
205+
if: needs.detect-environment.outputs.environment == 'dev' || needs.detect-environment.outputs.environment == 'prod'
206+
run: |
207+
# Extract custom domain from config
208+
CUSTOM_DOMAIN=$(grep "CustomDomainName:" $CONFIG_FILE | awk '{print $2}' | tr -d '"')
209+
210+
if [ -z "$CUSTOM_DOMAIN" ] || [ "$CUSTOM_DOMAIN" == "" ]; then
211+
echo "⏭️ No custom domain configured, skipping DNS setup"
212+
exit 0
213+
fi
214+
215+
echo "🔍 Checking DNS configuration for $CUSTOM_DOMAIN"
216+
217+
# Get the root domain (e.g., aillc.link from portal.aillc.link)
218+
ROOT_DOMAIN=$(echo $CUSTOM_DOMAIN | awk -F. '{print $(NF-1)"."$NF}')
219+
echo "📍 Root domain: $ROOT_DOMAIN"
220+
221+
# Get the hosted zone ID for the custom domain
222+
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones \
223+
--query "HostedZones[?Name=='${CUSTOM_DOMAIN}.'].Id" \
224+
--output text | cut -d'/' -f3)
225+
226+
if [ -z "$HOSTED_ZONE_ID" ]; then
227+
echo "❌ No hosted zone found for $CUSTOM_DOMAIN"
228+
exit 1
229+
fi
230+
231+
echo "📍 Hosted Zone ID: $HOSTED_ZONE_ID"
232+
233+
# Get nameservers from the hosted zone
234+
HOSTED_ZONE_NS=$(aws route53 list-resource-record-sets \
235+
--hosted-zone-id $HOSTED_ZONE_ID \
236+
--query "ResourceRecordSets[?Type=='NS' && Name=='${CUSTOM_DOMAIN}.'].ResourceRecords[*].Value" \
237+
--output text | tr '\t' '\n' | sort)
238+
239+
echo "📋 Hosted zone nameservers:"
240+
echo "$HOSTED_ZONE_NS"
241+
242+
# Check if domain is registered in Route53
243+
DOMAIN_EXISTS=$(aws route53domains get-domain-detail \
244+
--domain-name $ROOT_DOMAIN \
245+
--query "DomainName" \
246+
--output text 2>/dev/null || echo "")
247+
248+
if [ -z "$DOMAIN_EXISTS" ]; then
249+
echo "⚠️ Domain $ROOT_DOMAIN is not registered in Route53 Domains"
250+
echo "📝 Please configure your domain registrar to use these nameservers:"
251+
echo "$HOSTED_ZONE_NS"
252+
exit 0
253+
fi
254+
255+
# Get current nameservers from domain registrar
256+
CURRENT_NS=$(aws route53domains get-domain-detail \
257+
--domain-name $ROOT_DOMAIN \
258+
--query "Nameservers[*].Name" \
259+
--output text | tr '\t' '\n' | sort)
260+
261+
echo "📋 Current domain nameservers:"
262+
echo "$CURRENT_NS"
263+
264+
# Compare nameservers
265+
if [ "$HOSTED_ZONE_NS" == "$CURRENT_NS" ]; then
266+
echo "✅ DNS nameservers are already correctly configured"
267+
else
268+
echo "🔄 Updating nameservers for $ROOT_DOMAIN..."
269+
270+
# Build nameserver parameters
271+
NS_PARAMS=""
272+
for ns in $HOSTED_ZONE_NS; do
273+
NS_PARAMS="$NS_PARAMS Name=${ns%.}"
274+
done
275+
276+
# Update nameservers
277+
OPERATION_ID=$(aws route53domains update-domain-nameservers \
278+
--domain-name $ROOT_DOMAIN \
279+
--nameservers $NS_PARAMS \
280+
--query "OperationId" \
281+
--output text)
282+
283+
echo "📋 Operation ID: $OPERATION_ID"
284+
285+
# Wait for operation to complete (max 5 minutes)
286+
COUNTER=0
287+
while [ $COUNTER -lt 30 ]; do
288+
STATUS=$(aws route53domains get-operation-detail \
289+
--operation-id $OPERATION_ID \
290+
--query "Status" \
291+
--output text)
292+
293+
if [ "$STATUS" == "SUCCESSFUL" ]; then
294+
echo "✅ Nameservers updated successfully!"
295+
break
296+
elif [ "$STATUS" == "FAILED" ]; then
297+
echo "❌ Nameserver update failed"
298+
exit 1
299+
fi
300+
301+
echo "⏳ Waiting for nameserver update... (Status: $STATUS)"
302+
sleep 10
303+
COUNTER=$((COUNTER + 1))
304+
done
305+
306+
if [ $COUNTER -eq 30 ]; then
307+
echo "⚠️ Nameserver update is taking longer than expected"
308+
echo "Check operation status with: aws route53domains get-operation-detail --operation-id $OPERATION_ID"
309+
fi
310+
fi
311+
312+
echo "🌐 DNS configuration complete for $CUSTOM_DOMAIN"
313+
204314
skip-message:
205315
name: Deployment Skipped
206316
needs: detect-environment

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ validate:
112112
@echo "✅ Validating CloudFormation template..."
113113
sam validate --template-file cloudformation/template.yaml --region us-east-1
114114

115+
# Deploy locally (bypassing GitHub Actions)
116+
deploy-local-dev:
117+
@echo "🚀 Deploying to dev environment locally..."
118+
python deploy.py deploy --env dev
119+
120+
deploy-local-prod:
121+
@echo "🏭 Deploying to prod environment locally..."
122+
python deploy.py deploy --env prod
123+
124+
# Delete stacks locally
125+
delete-dev:
126+
@echo "🗑️ Deleting dev stack..."
127+
python deploy.py delete --env dev
128+
129+
delete-prod:
130+
@echo "🗑️ Deleting prod stack..."
131+
python deploy.py delete --env prod
132+
133+
# Just build without deploying
134+
build-sam:
135+
@echo "📦 Building SAM application..."
136+
python deploy.py build
137+
115138
# Quick preview - builds and then previews
116139
preview: build preview-branding
117140

deploy.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Local deployment script for API Portal
4+
Reads configuration from environments/*.yaml and deploys using SAM
5+
"""
6+
7+
import os
8+
import sys
9+
import yaml
10+
import subprocess
11+
import argparse
12+
from datetime import datetime
13+
14+
15+
def load_environment_config(env='dev'):
16+
"""Load configuration from environment YAML file"""
17+
config_file = f'environments/{env}.yaml'
18+
19+
if not os.path.exists(config_file):
20+
print(f"❌ Configuration file not found: {config_file}")
21+
sys.exit(1)
22+
23+
with open(config_file, 'r') as f:
24+
config = yaml.safe_load(f)
25+
26+
return config
27+
28+
29+
def build_sam_application():
30+
"""Build the SAM application"""
31+
print("📦 Building SAM application...")
32+
cmd = [
33+
'sam', 'build',
34+
'--template-file', 'cloudformation/template.yaml',
35+
'--use-container'
36+
]
37+
38+
# Use shell=True on Windows
39+
shell = sys.platform == 'win32'
40+
result = subprocess.run(cmd, capture_output=False, shell=shell)
41+
if result.returncode != 0:
42+
print("❌ SAM build failed")
43+
sys.exit(1)
44+
45+
print("✅ Build complete")
46+
47+
48+
def deploy_stack(config, no_execute_changeset=False):
49+
"""Deploy the CloudFormation stack using SAM"""
50+
env = config['environment']
51+
stack_name = config['stack_name']
52+
aws_config = config['aws']
53+
parameters = config['parameters']
54+
55+
# Check if deployment is enabled
56+
if not config.get('enabled', True):
57+
print(f"⏸️ Deployment is disabled for {env} environment")
58+
print("Set 'enabled: true' in the config file to enable deployment")
59+
sys.exit(0)
60+
61+
print(f"🚀 Deploying {env} environment...")
62+
print(f"📍 Stack: {stack_name}")
63+
print(f"📍 Region: {aws_config['region']}")
64+
print(f"📍 Account: {aws_config['account_id']}")
65+
66+
# Build parameter overrides
67+
param_overrides = []
68+
for key, value in parameters.items():
69+
# Replace ${GITHUB_SHA} with timestamp for local deploys
70+
if isinstance(value, str) and '${GITHUB_SHA}' in value:
71+
value = value.replace('${GITHUB_SHA}', datetime.now().strftime('%Y%m%d%H%M%S'))
72+
param_overrides.append(f"{key}={value}")
73+
74+
# Build SAM deploy command
75+
cmd = [
76+
'sam', 'deploy',
77+
'--template-file', '.aws-sam/build/template.yaml',
78+
'--stack-name', stack_name,
79+
'--s3-bucket', aws_config['artifacts_bucket'],
80+
'--capabilities', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND',
81+
'--region', aws_config['region'],
82+
'--parameter-overrides'
83+
] + param_overrides
84+
85+
if no_execute_changeset:
86+
cmd.append('--no-execute-changeset')
87+
88+
# Execute deployment
89+
print("\n📋 Executing SAM deploy...")
90+
print(f"Command: {' '.join(cmd[:10])}...") # Show first part of command
91+
92+
# Use shell=True on Windows
93+
shell = sys.platform == 'win32'
94+
result = subprocess.run(cmd, shell=shell)
95+
96+
if result.returncode != 0:
97+
print("❌ Deployment failed")
98+
sys.exit(1)
99+
100+
print("✅ Deployment complete!")
101+
102+
# Get stack outputs
103+
get_stack_outputs(stack_name, aws_config['region'])
104+
105+
106+
def get_stack_outputs(stack_name, region):
107+
"""Get and display stack outputs"""
108+
print("\n📋 Stack outputs:")
109+
110+
cmd = [
111+
'aws', 'cloudformation', 'describe-stacks',
112+
'--stack-name', stack_name,
113+
'--region', region,
114+
'--query', 'Stacks[0].Outputs',
115+
'--output', 'json'
116+
]
117+
118+
# Use shell=True on Windows
119+
shell = sys.platform == 'win32'
120+
result = subprocess.run(cmd, capture_output=True, text=True, shell=shell)
121+
122+
if result.returncode == 0:
123+
try:
124+
import json
125+
outputs = json.loads(result.stdout)
126+
if outputs:
127+
for output in outputs:
128+
print(f" {output['OutputKey']}: {output.get('OutputValue', 'N/A')}")
129+
130+
# Look for website URL specifically
131+
website_url = next((o['OutputValue'] for o in outputs if o['OutputKey'] == 'WebsiteURL'), None)
132+
if website_url:
133+
print(f"\n🌐 Portal URL: {website_url}")
134+
except:
135+
print(result.stdout)
136+
137+
138+
def delete_stack(config):
139+
"""Delete the CloudFormation stack"""
140+
env = config['environment']
141+
stack_name = config['stack_name']
142+
aws_config = config['aws']
143+
144+
print(f"🗑️ Deleting {env} stack: {stack_name}")
145+
146+
response = input("⚠️ Are you sure? (yes/no): ")
147+
if response.lower() != 'yes':
148+
print("❌ Deletion cancelled")
149+
return
150+
151+
cmd = [
152+
'aws', 'cloudformation', 'delete-stack',
153+
'--stack-name', stack_name,
154+
'--region', aws_config['region']
155+
]
156+
157+
# Use shell=True on Windows
158+
shell = sys.platform == 'win32'
159+
result = subprocess.run(cmd, shell=shell)
160+
161+
if result.returncode == 0:
162+
print("✅ Stack deletion initiated")
163+
print(f"Monitor progress: aws cloudformation describe-stacks --stack-name {stack_name} --region {aws_config['region']}")
164+
else:
165+
print("❌ Stack deletion failed")
166+
167+
168+
def main():
169+
parser = argparse.ArgumentParser(description='Deploy API Portal locally')
170+
parser.add_argument('action', choices=['deploy', 'delete', 'build'],
171+
help='Action to perform')
172+
parser.add_argument('--env', default='dev', choices=['dev', 'prod'],
173+
help='Environment to deploy (default: dev)')
174+
parser.add_argument('--no-build', action='store_true',
175+
help='Skip SAM build step')
176+
parser.add_argument('--no-execute-changeset', action='store_true',
177+
help='Create changeset but do not execute it')
178+
179+
args = parser.parse_args()
180+
181+
# Load environment configuration
182+
config = load_environment_config(args.env)
183+
184+
if args.action == 'build':
185+
build_sam_application()
186+
elif args.action == 'deploy':
187+
if not args.no_build:
188+
build_sam_application()
189+
deploy_stack(config, args.no_execute_changeset)
190+
elif args.action == 'delete':
191+
delete_stack(config)
192+
193+
194+
if __name__ == '__main__':
195+
main()

environments/dev.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ aws:
1313

1414
# Stack Parameters
1515
parameters:
16-
# S3 Buckets (using existing SAM-created buckets)
17-
ArtifactsS3BucketName: aws-sam-cli-managed-api-portal-dev-artifactsbucket-kcx2dfjqjvxj
16+
# S3 Buckets
17+
ArtifactsS3BucketName: augint-api-portal-dev-artifacts
1818
DevPortalSiteS3BucketName: augint-api-portal-dev-site
1919

2020
# Cognito Configuration

poetry.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[virtualenvs]
2+
in-project = true

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package-mode = false
99
[tool.poetry.dependencies]
1010
python = "^3.12"
1111
python-semantic-release = "^8.0.0"
12+
pyyaml = "^6.0.2"
1213

1314
[tool.semantic_release]
1415
version_toml = ["pyproject.toml:tool.poetry.version"]

0 commit comments

Comments
 (0)