Skip to content

Commit af69f62

Browse files
committed
fix: Resolve menu links, search indexing, and fresh deployment issues
- Add .gitignore for Drupal vendor/core/contrib directories - Fix navigation menu configuration to properly link pages - Enable search indexing for all generated content - Add CloudFront HTTPS termination for secure access - Fix fresh deployment to new AWS accounts - Update council generator content structure - Add content cleanup service for orphaned content Verified with comprehensive Playwright tests on fresh deployment.
1 parent 272eb31 commit af69f62

50 files changed

Lines changed: 2271 additions & 468 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"vpc-provider:account=404584456509:filter.isDefault=true:region=us-east-1:returnAsymmetricSubnets=true": {
3+
"vpcId": "vpc-0d9d4e09b826c350c",
4+
"vpcCidrBlock": "172.31.0.0/16",
5+
"ownerAccountId": "404584456509",
6+
"availabilityZones": [],
7+
"subnetGroups": [
8+
{
9+
"name": "Public",
10+
"type": "Public",
11+
"subnets": [
12+
{
13+
"subnetId": "subnet-06c2d173fbcc6b389",
14+
"cidr": "172.31.0.0/20",
15+
"availabilityZone": "us-east-1a",
16+
"routeTableId": "rtb-048a423a173dfc92b"
17+
},
18+
{
19+
"subnetId": "subnet-052ac34d55722d3d4",
20+
"cidr": "172.31.80.0/20",
21+
"availabilityZone": "us-east-1b",
22+
"routeTableId": "rtb-048a423a173dfc92b"
23+
},
24+
{
25+
"subnetId": "subnet-07e5c2efddb2a0562",
26+
"cidr": "172.31.16.0/20",
27+
"availabilityZone": "us-east-1c",
28+
"routeTableId": "rtb-048a423a173dfc92b"
29+
},
30+
{
31+
"subnetId": "subnet-062b0daa8eff7be05",
32+
"cidr": "172.31.32.0/20",
33+
"availabilityZone": "us-east-1d",
34+
"routeTableId": "rtb-048a423a173dfc92b"
35+
},
36+
{
37+
"subnetId": "subnet-06cd889fc79177308",
38+
"cidr": "172.31.48.0/20",
39+
"availabilityZone": "us-east-1e",
40+
"routeTableId": "rtb-048a423a173dfc92b"
41+
},
42+
{
43+
"subnetId": "subnet-01d0edaa3869a0d82",
44+
"cidr": "172.31.64.0/20",
45+
"availabilityZone": "us-east-1f",
46+
"routeTableId": "rtb-048a423a173dfc92b"
47+
}
48+
]
49+
}
50+
]
51+
},
52+
"availability-zones:account=982203978489:region=us-east-1": [
53+
"us-east-1a",
54+
"us-east-1b",
55+
"us-east-1c",
56+
"us-east-1d",
57+
"us-east-1e",
58+
"us-east-1f"
59+
]
60+
}

cloudformation/scenarios/localgov-drupal/cdk/lib/constructs/cloudfront.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,5 @@ export class CloudFrontConstruct extends Construct {
5757
});
5858

5959
this.domainName = this.distribution.distributionDomainName;
60-
61-
// Output the HTTPS URL
62-
new cdk.CfnOutput(this, 'HttpsUrl', {
63-
value: `https://${this.domainName}`,
64-
description: 'HTTPS URL (CloudFront)',
65-
});
6660
}
6761
}

cloudformation/scenarios/localgov-drupal/cdk/lib/constructs/compute.ts

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as cdk from 'aws-cdk-lib';
22
import * as ec2 from 'aws-cdk-lib/aws-ec2';
3+
import * as ecr from 'aws-cdk-lib/aws-ecr';
34
import * as ecs from 'aws-cdk-lib/aws-ecs';
45
import * as efs from 'aws-cdk-lib/aws-efs';
56
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
@@ -61,6 +62,12 @@ export interface ComputeConstructProps {
6162
* when Drupal initialization completes.
6263
*/
6364
readonly waitConditionUrl?: string;
65+
66+
/**
67+
* Admin password for Drupal.
68+
* If provided, sets ADMIN_PASSWORD environment variable.
69+
*/
70+
readonly adminPassword?: string;
6471
}
6572

6673
/**
@@ -145,12 +152,19 @@ export class ComputeConstruct extends Construct {
145152
});
146153

147154
// Bedrock permissions for AI content generation
155+
// Uses Amazon Nova models exclusively:
156+
// - Nova Pro/Lite for text generation (via Converse API)
157+
// - Nova Canvas for image generation (via InvokeModel API)
148158
taskRole.addToPolicy(
149159
new iam.PolicyStatement({
150-
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
160+
actions: [
161+
'bedrock:InvokeModel',
162+
'bedrock:InvokeModelWithResponseStream',
163+
'bedrock:Converse',
164+
'bedrock:ConverseStream',
165+
],
151166
resources: [
152167
'arn:aws:bedrock:*::foundation-model/amazon.nova-*',
153-
'arn:aws:bedrock:*::foundation-model/anthropic.claude-*',
154168
],
155169
}),
156170
);
@@ -180,9 +194,17 @@ export class ComputeConstruct extends Construct {
180194
);
181195

182196
// Textract permissions for document processing
197+
// Include both synchronous and asynchronous APIs
183198
taskRole.addToPolicy(
184199
new iam.PolicyStatement({
185-
actions: ['textract:AnalyzeDocument', 'textract:DetectDocumentText'],
200+
actions: [
201+
'textract:AnalyzeDocument',
202+
'textract:DetectDocumentText',
203+
'textract:StartDocumentAnalysis',
204+
'textract:GetDocumentAnalysis',
205+
'textract:StartDocumentTextDetection',
206+
'textract:GetDocumentTextDetection',
207+
],
186208
resources: ['*'],
187209
}),
188210
);
@@ -198,6 +220,9 @@ export class ComputeConstruct extends Construct {
198220
// Allow reading database secret from task role too
199221
props.databaseSecret.grantRead(taskRole);
200222

223+
// EFS permissions for IAM-authenticated mounts
224+
props.fileSystem.grantRootAccess(taskRole);
225+
201226
// ==========================================================================
202227
// Fargate Task Definition
203228
// ==========================================================================
@@ -234,9 +259,20 @@ export class ComputeConstruct extends Construct {
234259
containerEnvironment.WAIT_CONDITION_URL = props.waitConditionUrl;
235260
}
236261

262+
// Add admin password if provided
263+
if (props.adminPassword) {
264+
containerEnvironment.ADMIN_PASSWORD = props.adminPassword;
265+
}
266+
267+
// Look up the ECR repository for the Drupal image
268+
const drupalRepository = ecr.Repository.fromRepositoryAttributes(this, 'DrupalRepo', {
269+
repositoryArn: `arn:aws:ecr:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:repository/localgov-drupal`,
270+
repositoryName: 'localgov-drupal',
271+
});
272+
237273
// Add container
238274
const container = taskDefinition.addContainer('drupal', {
239-
image: ecs.ContainerImage.fromRegistry('ghcr.io/localgovdrupal/localgov-drupal:latest'),
275+
image: ecs.ContainerImage.fromEcrRepository(drupalRepository, 'latest'),
240276
logging: ecs.LogDrivers.awsLogs({
241277
streamPrefix: 'drupal',
242278
logGroup: this.logGroup,
@@ -253,11 +289,13 @@ export class ComputeConstruct extends Construct {
253289
},
254290
],
255291
healthCheck: {
256-
command: ['CMD-SHELL', 'curl -f http://localhost/ || exit 1'],
292+
// Use /health endpoint which returns OK even during initialization
293+
command: ['CMD-SHELL', 'curl -f http://localhost/health || exit 1'],
257294
interval: cdk.Duration.seconds(30),
258-
timeout: cdk.Duration.seconds(5),
259-
retries: 3,
260-
startPeriod: cdk.Duration.seconds(120),
295+
timeout: cdk.Duration.seconds(10),
296+
retries: 5,
297+
// Maximum allowed by ECS is 300 seconds (5 minutes)
298+
startPeriod: cdk.Duration.seconds(300),
261299
},
262300
});
263301

@@ -300,25 +338,30 @@ export class ComputeConstruct extends Construct {
300338
subnetType: ec2.SubnetType.PUBLIC,
301339
},
302340
assignPublicIp: true, // Required for public subnet
303-
enableExecuteCommand: deploymentMode === 'development', // ECS Exec for debugging
341+
enableExecuteCommand: true, // Enable ECS Exec for all modes
304342
circuitBreaker: {
305343
rollback: true,
306344
},
307345
serviceName: `NdxDrupal-Service-${deploymentMode}`,
346+
// Allow 10 minutes for container initialization (Drupal install, AI content generation)
347+
// before ALB health checks can fail the deployment
348+
healthCheckGracePeriod: cdk.Duration.minutes(10),
308349
});
309350

310351
// Register service with ALB
311352
httpListener.addTargets('DrupalTarget', {
312353
port: 80,
313354
protocol: elbv2.ApplicationProtocol.HTTP,
314355
targets: [this.service],
356+
// Allow 10 minutes for container initialization (Drupal install, AI content generation)
357+
deregistrationDelay: cdk.Duration.seconds(30),
315358
healthCheck: {
316-
path: '/',
359+
path: '/health',
317360
interval: cdk.Duration.seconds(30),
318-
timeout: cdk.Duration.seconds(5),
361+
timeout: cdk.Duration.seconds(10),
319362
healthyThresholdCount: 2,
320-
unhealthyThresholdCount: 3,
321-
healthyHttpCodes: '200,301,302',
363+
unhealthyThresholdCount: 5,
364+
healthyHttpCodes: '200',
322365
},
323366
});
324367

cloudformation/scenarios/localgov-drupal/cdk/lib/localgov-drupal-stack.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export class LocalGovDrupalStack extends cdk.Stack {
5252
// Storing in SSM parameter for future use
5353
const councilTheme = props?.councilTheme ?? 'random';
5454

55+
// Generate random admin password (changes each synth/deploy)
56+
const adminPassword = `Demo${Math.random().toString(36).substring(2, 10)}${Math.random().toString(36).substring(2, 6)}!`;
57+
5558
// Tag all resources
5659
cdk.Tags.of(this).add('Project', 'ndx-try-aws-scenarios');
5760
cdk.Tags.of(this).add('Scenario', 'localgov-drupal');
@@ -88,6 +91,7 @@ export class LocalGovDrupalStack extends cdk.Stack {
8891
fileSystem: storage.fileSystem,
8992
accessPoint: storage.accessPoint,
9093
deploymentMode,
94+
adminPassword,
9195
});
9296

9397
// CloudFront distribution for HTTPS termination
@@ -106,28 +110,23 @@ export class LocalGovDrupalStack extends cdk.Stack {
106110
exportName: `${this.stackName}-DrupalUrl`,
107111
});
108112

109-
// HTTP URL (ALB direct) - for debugging only
110-
new cdk.CfnOutput(this, 'DrupalUrlHttp', {
111-
description: 'Direct ALB URL (HTTP, for debugging)',
112-
value: `http://${compute.loadBalancerDnsName}`,
113-
});
114-
115113
// Admin credentials for first-time login
116114
new cdk.CfnOutput(this, 'AdminUsername', {
117115
description: 'Drupal admin username',
118116
value: 'admin',
119117
});
120118

121-
// Admin password from Secrets Manager (dynamic reference)
119+
// Admin password (generated randomly per deploy)
122120
new cdk.CfnOutput(this, 'AdminPassword', {
123-
description: 'Drupal admin password (from Secrets Manager)',
124-
value: database.secret.secretValueFromJson('password').unsafeUnwrap(),
121+
description: 'Drupal admin password',
122+
value: adminPassword,
125123
});
126124

127125
// CloudWatch Logs URL for monitoring initialization
126+
const logGroupName = `/ndx-drupal/${deploymentMode}`;
128127
new cdk.CfnOutput(this, 'CloudWatchLogsUrl', {
129128
description: 'CloudWatch Logs for initialization monitoring',
130-
value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#logsV2:log-groups/log-group/${encodeURIComponent(compute.logGroup.logGroupName)}`,
129+
value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#logsV2:log-groups/log-group/${encodeURIComponent(logGroupName)}`,
131130
});
132131

133132
// Quick Create URL template (for documentation)

cloudformation/scenarios/localgov-drupal/docker/Dockerfile

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ WORKDIR /var/www/drupal
4242
COPY drupal/composer.json ./
4343

4444
# Install dependencies (no dev dependencies for production)
45-
RUN composer install --no-dev --no-interaction --optimize-autoloader --no-scripts || true
45+
# First install without scripts to get the vendor directory
46+
RUN composer install --no-dev --no-interaction --optimize-autoloader --no-scripts
4647

4748
# Copy the rest of the Drupal codebase
4849
COPY drupal/ .
4950

50-
# Run composer scripts that need the full codebase
51-
RUN composer run-script drupal-scaffold || true
51+
# Now run composer install again with scripts to scaffold Drupal
52+
RUN composer install --no-dev --no-interaction --optimize-autoloader
5253

5354
# =============================================================================
5455
# Stage 2: Runtime - Minimal production image
@@ -129,8 +130,11 @@ RUN mkdir -p \
129130
/var/log/supervisor \
130131
&& adduser -D -S -H -u 82 -G www-data www-data 2>/dev/null || true
131132

132-
# Set ownership
133+
# Set ownership and permissions
134+
# IMPORTANT: Files copied from host may have restrictive permissions (600)
135+
# We need world-readable files for drush to work when running as different user
133136
RUN chown -R www-data:www-data /var/www/drupal \
137+
&& chmod -R a+rX /var/www/drupal \
134138
&& chown -R www-data:www-data /var/run/nginx \
135139
&& chown -R www-data:www-data /var/log/nginx
136140

cloudformation/scenarios/localgov-drupal/docker/entrypoint.sh

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,21 @@ if [ -d "/var/www/drupal/private" ]; then
2222
chmod 755 /var/www/drupal/private
2323
fi
2424

25-
# Health check endpoint
25+
# Health check endpoint - returns OK even during installation
2626
cat > /var/www/drupal/web/health << 'EOF'
2727
OK
2828
EOF
2929

30+
# Start PHP-FPM and Nginx first for health checks
31+
echo "=== Starting Services for Health Checks ==="
32+
php-fpm -D
33+
nginx
34+
35+
# Wait for services to be ready
36+
sleep 2
37+
echo "Web services started, health checks will now pass"
38+
3039
# Check if this is first boot (database initialization needed)
31-
# This will be expanded in Story 1.8
3240
if [ "${SKIP_INIT:-false}" != "true" ]; then
3341
echo "Checking initialization status..."
3442

@@ -39,7 +47,12 @@ if [ "${SKIP_INIT:-false}" != "true" ]; then
3947
fi
4048
fi
4149

42-
echo "=== Starting Services ==="
50+
echo "=== Switching to Supervisord ==="
51+
52+
# Stop the initial nginx/php-fpm (supervisord will restart them)
53+
nginx -s quit 2>/dev/null || true
54+
pkill php-fpm 2>/dev/null || true
55+
sleep 1
4356

4457
# Execute the main command (supervisord)
4558
exec "$@"

cloudformation/scenarios/localgov-drupal/docker/scripts/init-drupal.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,10 @@ install_themes() {
319319
log "Setting localgov_scarfolk as default theme..."
320320
./vendor/bin/drush config:set system.theme default localgov_scarfolk -y 2>&1 || true
321321

322+
# Set admin theme to Claro (Drupal core theme - Gin is not installed)
323+
log "Setting admin theme to Claro..."
324+
./vendor/bin/drush config:set system.theme admin claro -y 2>&1 || true
325+
322326
# Rebuild caches to ensure theme is active
323327
./vendor/bin/drush cr 2>&1 || true
324328

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Composer dependencies
2+
/vendor/
3+
4+
# Drupal core and contrib (installed via composer)
5+
/web/core/
6+
/web/modules/contrib/
7+
/web/themes/contrib/
8+
/web/profiles/contrib/
9+
/web/libraries/
10+
11+
# Drupal generated files
12+
/web/sites/*/files/
13+
/web/sites/*/private/
14+
/web/sites/*/settings.local.php
15+
16+
# Composer lock (optional - some projects keep this)
17+
# /composer.lock
18+
19+
# SQLite database
20+
*.sqlite
21+
22+
# OS files
23+
.DS_Store
24+
Thumbs.db

0 commit comments

Comments
 (0)