deploy-stack 0.4.0 ā 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/cli.js +4 -1
- package/package.json +1 -1
- package/src/commands/destroy.js +77 -0
- package/src/commands/init.js +6 -0
- package/src/utils/aws.js +43 -1
- package/templates/README.md +18 -9
- package/templates/docker/nextjs.Dockerfile +11 -4
- package/templates/docker/node.Dockerfile +6 -8
- package/templates/docker/python.Dockerfile +2 -4
- package/templates/docker/static.Dockerfile +1 -14
- package/templates/github/deploy.yml +56 -1
- package/templates/terraform/cloudfront.tf +1 -0
- package/templates/terraform/main.tf +8 -0
- package/templates/terraform/network.tf +3 -0
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ You retain complete ownership of your infrastructure code without relying on bla
|
|
|
33
33
|
* **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern native S3 concurrency locking.
|
|
34
34
|
* **Built-in Secrets Sync:** Provides a dedicated CLI workflow to securely push local `.env` variables into AWS Secrets Manager and map them directly into containers at runtime.
|
|
35
35
|
* **Non-Destructive:** Safely analyzes existing directories and prompts for confirmation before updating any files.
|
|
36
|
+
* **Zero-Config Detection:** Automatically resolves output directories for Vite, Astro, SvelteKit, CRA, and more.
|
|
37
|
+
* **Safe Teardown:** Completely remove all generated AWS resources and empty S3 state buckets with a single `destroy` command.
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -81,6 +83,12 @@ npx deploy-stack secrets push .env.production
|
|
|
81
83
|
|
|
82
84
|
---
|
|
83
85
|
|
|
86
|
+
## šļø Infrastructure Teardown
|
|
87
|
+
To safely completely remove your ECS cluster, load balancers, and empty the remote S3 state bucket, run:
|
|
88
|
+
`npx deploy-stack destroy`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
84
92
|
## š Generated File Structure
|
|
85
93
|
|
|
86
94
|
Running the CLI generates a modular architecture tailored to your service:
|
package/bin/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { mainStack } from '../src/commands/init.js';
|
|
4
|
+
import { destroyStack } from '../src/commands/destroy.js';
|
|
3
5
|
import { runDoctor } from '../src/commands/doctor.js';
|
|
4
6
|
import { pushSecrets } from '../src/commands/secrets.js';
|
|
5
|
-
import { mainStack } from '../src/commands/init.js';
|
|
6
7
|
|
|
7
8
|
// 1. Extract the telemetry flag and set the environment variable
|
|
8
9
|
const rawArgs = process.argv.slice(2);
|
|
@@ -22,6 +23,8 @@ if (args[0] === 'secrets' && args[1] === 'push') {
|
|
|
22
23
|
pushSecrets(envFile, projectName).catch(console.error);
|
|
23
24
|
} else if (args[0] === 'doctor') {
|
|
24
25
|
runDoctor().catch(console.error);
|
|
26
|
+
} else if (args[0] === 'destroy') {
|
|
27
|
+
destroyStack().catch(console.error);
|
|
25
28
|
} else {
|
|
26
29
|
mainStack().catch(console.error);
|
|
27
30
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { teardownStateBucket } from '../utils/aws.js';
|
|
7
|
+
import { checkDependency } from '../utils/system.js';
|
|
8
|
+
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
9
|
+
|
|
10
|
+
export async function destroyStack() {
|
|
11
|
+
intro(color.bgRed(color.white(' deploy-stack destroy šļø ')));
|
|
12
|
+
|
|
13
|
+
const tfDirPath = path.join(process.cwd(), 'terraform');
|
|
14
|
+
const backendFilePath = path.join(tfDirPath, 'backend.tf');
|
|
15
|
+
|
|
16
|
+
if (!fsSync.existsSync(backendFilePath)) {
|
|
17
|
+
console.error(color.red('ā No terraform/backend.tf found in the current directory.'));
|
|
18
|
+
console.log(color.yellow('Are you in the root of a deploy-stack project?'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasTerraform = await checkDependency('terraform');
|
|
23
|
+
if (!hasTerraform) {
|
|
24
|
+
console.error(color.red('ā Terraform is not installed.'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const proceed = await confirm({
|
|
29
|
+
message: color.red('ā ļø WARNING: This will permanently destroy all AWS resources associated with this project. Are you absolutely sure?'),
|
|
30
|
+
initialValue: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!proceed) {
|
|
34
|
+
cancel('Destruction cancelled. Your infrastructure is safe.');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const s = spinner();
|
|
39
|
+
|
|
40
|
+
// 1. Extract Bucket and Region from backend.tf
|
|
41
|
+
const backendContent = fsSync.readFileSync(backendFilePath, 'utf-8');
|
|
42
|
+
const bucketMatch = backendContent.match(/bucket\s*=\s*"([^"]+)"/);
|
|
43
|
+
const regionMatch = backendContent.match(/region\s*=\s*"([^"]+)"/);
|
|
44
|
+
|
|
45
|
+
const bucketName = bucketMatch ? bucketMatch[1] : null;
|
|
46
|
+
const region = regionMatch ? regionMatch[1] : 'us-east-1';
|
|
47
|
+
|
|
48
|
+
// 2. Execute Terraform Destroy
|
|
49
|
+
console.log(color.cyan('\nInitiating Terraform destroy (this may take a few minutes)...\n'));
|
|
50
|
+
try {
|
|
51
|
+
execSync('terraform destroy -auto-approve', { cwd: tfDirPath, stdio: 'inherit' });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(color.red('\nā Terraform destroy failed. Please check the output above.'));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. Clean up the S3 State Bucket
|
|
58
|
+
if (bucketName) {
|
|
59
|
+
s.start(`Emptying and deleting S3 state bucket: ${bucketName}...`);
|
|
60
|
+
try {
|
|
61
|
+
await teardownStateBucket(region, bucketName);
|
|
62
|
+
s.stop(`S3 bucket ${bucketName} successfully deleted.`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
s.stop(`ā Failed to delete S3 bucket. You may need to delete it manually in the AWS Console.`);
|
|
65
|
+
console.error(color.red(`AWS Error: ${error.message}`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
trackEvent('project_destroyed', {
|
|
70
|
+
region,
|
|
71
|
+
bucket: bucketName,
|
|
72
|
+
success: true
|
|
73
|
+
});
|
|
74
|
+
await flushTelemetry();
|
|
75
|
+
|
|
76
|
+
outro(color.green('ā
Infrastructure successfully destroyed. Your AWS bill is safe.'));
|
|
77
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -232,6 +232,7 @@ export async function mainStack() {
|
|
|
232
232
|
trackEvent('project_provisioned', {
|
|
233
233
|
projectName: actualProjectName,
|
|
234
234
|
framework: finalFramework,
|
|
235
|
+
specific_framework: detectedFramework?.name || finalFramework,
|
|
235
236
|
region: project.region,
|
|
236
237
|
size: project.size,
|
|
237
238
|
setup_mode: setupType,
|
|
@@ -260,6 +261,11 @@ export async function mainStack() {
|
|
|
260
261
|
outro(`
|
|
261
262
|
${color.green('ā
Project provisioned successfully!')}
|
|
262
263
|
|
|
264
|
+
${color.blue('š”ļø DevSecOps Enabled:')}
|
|
265
|
+
Automated Trivy vulnerability scanning for your Docker container
|
|
266
|
+
and Terraform has been added to your CI/CD pipeline.
|
|
267
|
+
Check the 'Summary' page of your GitHub Actions runs for reports.
|
|
268
|
+
|
|
263
269
|
${frameworkWarnings}
|
|
264
270
|
|
|
265
271
|
Next steps:
|
package/src/utils/aws.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
2
|
-
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand, PutBucketTaggingCommand } from '@aws-sdk/client-s3';
|
|
3
|
+
import { DeleteBucketCommand, ListObjectVersionsCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
3
4
|
|
|
4
5
|
export async function provisionStateBucket(region, projectName) {
|
|
5
6
|
const stsClient = new STSClient({ region });
|
|
@@ -23,6 +24,15 @@ export async function provisionStateBucket(region, projectName) {
|
|
|
23
24
|
CreateBucketConfiguration: region === 'us-east-1' ? undefined : { LocationConstraint: region }
|
|
24
25
|
}));
|
|
25
26
|
|
|
27
|
+
await s3Client.send(new PutBucketTaggingCommand({
|
|
28
|
+
Bucket: stateBucketName,
|
|
29
|
+
Tagging: {
|
|
30
|
+
TagSet: [
|
|
31
|
+
{ Key: "ManagedBy", Value: "deploy-stack" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
|
|
26
36
|
await s3Client.send(new PutBucketVersioningCommand({
|
|
27
37
|
Bucket: stateBucketName,
|
|
28
38
|
VersioningConfiguration: { Status: 'Enabled' }
|
|
@@ -35,4 +45,36 @@ export async function provisionStateBucket(region, projectName) {
|
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
return { awsAccountId, stateBucketName };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function teardownStateBucket(region, bucketName) {
|
|
51
|
+
const client = new S3Client({ region });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// 1. Fetch all object versions and delete markers
|
|
55
|
+
const listCommand = new ListObjectVersionsCommand({ Bucket: bucketName });
|
|
56
|
+
const { Versions, DeleteMarkers } = await client.send(listCommand);
|
|
57
|
+
|
|
58
|
+
const objectsToDelete = [];
|
|
59
|
+
if (Versions) objectsToDelete.push(...Versions.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
|
|
60
|
+
if (DeleteMarkers) objectsToDelete.push(...DeleteMarkers.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
|
|
61
|
+
|
|
62
|
+
// 2. Delete all contents if any exist
|
|
63
|
+
if (objectsToDelete.length > 0) {
|
|
64
|
+
const deleteCommand = new DeleteObjectsCommand({
|
|
65
|
+
Bucket: bucketName,
|
|
66
|
+
Delete: { Objects: objectsToDelete }
|
|
67
|
+
});
|
|
68
|
+
await client.send(deleteCommand);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Delete the now-empty bucket
|
|
72
|
+
const deleteBucketCommand = new DeleteBucketCommand({ Bucket: bucketName });
|
|
73
|
+
await client.send(deleteBucketCommand);
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.name === 'NoSuchBucket') return true; // Already deleted
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
38
80
|
}
|
package/templates/README.md
CHANGED
|
@@ -29,7 +29,7 @@ This infrastructure provisions a highly available Application Load Balancer (ALB
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
3. **Automated CI/CD (Keyless via OIDC):**
|
|
32
|
-
Push this repository to GitHub. Your deployment pipeline uses AWS IAM OpenID Connect (OIDC) to authenticate securely with temporary credentialsā**no long-lived AWS secret keys are required in GitHub Secrets**. Every push to `
|
|
32
|
+
Push this repository to GitHub. Your deployment pipeline uses AWS IAM OpenID Connect (OIDC) to authenticate securely with temporary credentialsā**no long-lived AWS secret keys are required in GitHub Secrets**. Every push to `{{DEPLOY_BRANCH}}` will automatically build, package, and deploy your application.
|
|
33
33
|
|
|
34
34
|
### ā ļø Troubleshooting: OIDC Provider Already Exists
|
|
35
35
|
AWS only permits one GitHub Actions OIDC provider per AWS account. If `terraform apply` fails with an `EntityAlreadyExists` error regarding the OIDC provider, it indicates GitHub Actions was previously configured in this account.
|
|
@@ -48,13 +48,11 @@ Re-run `terraform apply` to link directly to your existing provider.
|
|
|
48
48
|
|
|
49
49
|
If you are done testing and want to stop all AWS billing, you must destroy the infrastructure.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
Run the automated teardown command from the root of your project:
|
|
53
52
|
```bash
|
|
54
|
-
|
|
55
|
-
terraform destroy
|
|
53
|
+
npx deploy-stack destroy
|
|
56
54
|
```
|
|
57
|
-
*Type `yes` when prompted. This will
|
|
55
|
+
*Type `yes` when prompted. This will execute a safe Terraform teardown of your Load Balancer, ECS cluster, and networking components, followed by automatically emptying and deleting your remote S3 state bucket.*
|
|
58
56
|
|
|
59
57
|
## ā ļø Critical Application Prerequisites
|
|
60
58
|
|
|
@@ -94,8 +92,19 @@ Make sure your app is configured correctly:
|
|
|
94
92
|
* **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
|
|
95
93
|
* **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
|
|
96
94
|
|
|
97
|
-
### 4. Static Sites (Vite, Astro, React, Vue)
|
|
95
|
+
### 4. Static Sites (Vite, Astro, React, Vue, SvelteKit)
|
|
98
96
|
|
|
99
97
|
If you are deploying a static site, your application is served via a highly optimized, unprivileged Nginx container.
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
* **Zero-Config Build:** The CLI automatically detected your framework's output folder (`dist`, `build`, etc.) and pre-configured your Dockerfile.
|
|
99
|
+
* **Health Checks:** You do not need to configure a custom `/health` route. Nginx will automatically return a `200 OK` when AWS pings the root `/` index page.
|
|
100
|
+
|
|
101
|
+
## š”ļø Security Scanning
|
|
102
|
+
|
|
103
|
+
This pipeline includes automated DevSecOps guardrails using [Trivy](https://trivy.dev/).
|
|
104
|
+
Every time you push code, the pipeline will scan both your Docker container and your
|
|
105
|
+
Terraform configurations for vulnerabilities and misconfigurations.
|
|
106
|
+
|
|
107
|
+
To view the security reports:
|
|
108
|
+
1. Navigate to the **Actions** tab in GitHub.
|
|
109
|
+
2. Click on the latest deployment run.
|
|
110
|
+
3. Scroll down the **Summary** page to view the generated vulnerability tables.
|
|
@@ -21,10 +21,17 @@ ENV NODE_ENV=production
|
|
|
21
21
|
ENV PORT={{PORT}}
|
|
22
22
|
ENV HOSTNAME="0.0.0.0"
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
# Create an unprivileged user and group
|
|
25
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
26
|
+
adduser -S nextjs -u 1001 -G nodejs
|
|
27
|
+
|
|
28
|
+
# Copy the standalone output and assign ownership to the unprivileged user
|
|
29
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
30
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
31
|
+
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
32
|
+
|
|
33
|
+
# Switch to the unprivileged user before executing
|
|
34
|
+
USER nextjs
|
|
28
35
|
|
|
29
36
|
EXPOSE {{PORT}}
|
|
30
37
|
|
|
@@ -5,16 +5,14 @@ ENV NODE_ENV=production
|
|
|
5
5
|
|
|
6
6
|
WORKDIR /app
|
|
7
7
|
|
|
8
|
-
# 2. Copy
|
|
9
|
-
COPY package*.json ./
|
|
8
|
+
# 2. Copy dependency manifests with non-root ownership
|
|
9
|
+
COPY --chown=node:node package*.json ./
|
|
10
|
+
RUN npm ci --omit=dev
|
|
10
11
|
|
|
11
|
-
# 3.
|
|
12
|
-
|
|
12
|
+
# 3. Copy application code with non-root ownership
|
|
13
|
+
COPY --chown=node:node . .
|
|
13
14
|
|
|
14
|
-
# 4.
|
|
15
|
-
COPY . .
|
|
16
|
-
|
|
17
|
-
# 5. DevSecOps best practice: do not run the container as root
|
|
15
|
+
# 4. DevSecOps best practice: do not run the container as root
|
|
18
16
|
USER node
|
|
19
17
|
|
|
20
18
|
EXPOSE {{PORT}}
|
|
@@ -13,10 +13,8 @@ RUN adduser --disabled-password --gecos '' appuser
|
|
|
13
13
|
COPY requirements.txt .
|
|
14
14
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Secure file permissions
|
|
19
|
-
RUN chown -R appuser:appuser /app
|
|
16
|
+
# Copy with ownership
|
|
17
|
+
COPY --chown=appuser:appuser . .
|
|
20
18
|
|
|
21
19
|
# Drop root privileges
|
|
22
20
|
USER appuser
|
|
@@ -11,7 +11,7 @@ COPY . .
|
|
|
11
11
|
RUN npm run build
|
|
12
12
|
|
|
13
13
|
# STAGE 2: Serve with Hardened Nginx
|
|
14
|
-
FROM nginx:alpine
|
|
14
|
+
FROM nginxinc/nginx-unprivileged:alpine
|
|
15
15
|
|
|
16
16
|
# Adjusts BUILD_DIR to match your framework's output folder
|
|
17
17
|
COPY --from=builder /app/{{BUILD_DIR}} /usr/share/nginx/html
|
|
@@ -28,18 +28,5 @@ RUN echo "server {" > /etc/nginx/conf.d/default.conf && \
|
|
|
28
28
|
echo " }" >> /etc/nginx/conf.d/default.conf && \
|
|
29
29
|
echo "}" >> /etc/nginx/conf.d/default.conf
|
|
30
30
|
|
|
31
|
-
# Silence unprivileged user directive warning in main config
|
|
32
|
-
RUN sed -i 's/^user\s\+nginx;/# user nginx;/' /etc/nginx/nginx.conf
|
|
33
|
-
|
|
34
|
-
# DevSecOps Hardening: Drop root privileges for the Nginx process
|
|
35
|
-
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
|
36
|
-
chown -R nginx:nginx /var/cache/nginx && \
|
|
37
|
-
chown -R nginx:nginx /var/log/nginx && \
|
|
38
|
-
chown -R nginx:nginx /etc/nginx/conf.d && \
|
|
39
|
-
touch /var/run/nginx.pid && \
|
|
40
|
-
chown -R nginx:nginx /var/run/nginx.pid
|
|
41
|
-
|
|
42
|
-
USER nginx
|
|
43
|
-
|
|
44
31
|
EXPOSE {{PORT}}
|
|
45
32
|
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -4,6 +4,8 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
6
|
- {{DEPLOY_BRANCH}}
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: '0 0 * * 0'
|
|
7
9
|
|
|
8
10
|
env:
|
|
9
11
|
AWS_REGION: {{REGION}}
|
|
@@ -24,6 +26,29 @@ jobs:
|
|
|
24
26
|
- name: Checkout Code
|
|
25
27
|
uses: actions/checkout@v4
|
|
26
28
|
|
|
29
|
+
# --- 1. IaC SECURITY SCAN ---
|
|
30
|
+
- name: Scan Terraform for Misconfigurations
|
|
31
|
+
uses: aquasecurity/trivy-action@master
|
|
32
|
+
with:
|
|
33
|
+
scan-type: 'fs'
|
|
34
|
+
scan-ref: 'terraform/'
|
|
35
|
+
scanners: 'vuln,secret,misconfig'
|
|
36
|
+
format: 'table'
|
|
37
|
+
exit-code: '0' # Informational only, will not break the build
|
|
38
|
+
severity: 'CRITICAL,HIGH'
|
|
39
|
+
output: 'trivy-iac-summary.txt'
|
|
40
|
+
|
|
41
|
+
- name: Publish IaC Summary
|
|
42
|
+
if: always()
|
|
43
|
+
run: |
|
|
44
|
+
if [ -f trivy-iac-summary.txt ]; then
|
|
45
|
+
echo "### šļø Terraform Misconfiguration Scan" >> $GITHUB_STEP_SUMMARY
|
|
46
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
47
|
+
cat trivy-iac-summary.txt >> $GITHUB_STEP_SUMMARY
|
|
48
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# --- 2. DEPLOYMENT PREP ---
|
|
27
52
|
- name: Configure AWS Credentials
|
|
28
53
|
uses: aws-actions/configure-aws-credentials@v4
|
|
29
54
|
with:
|
|
@@ -34,13 +59,43 @@ jobs:
|
|
|
34
59
|
id: login-ecr
|
|
35
60
|
uses: aws-actions/amazon-ecr-login@v2
|
|
36
61
|
|
|
37
|
-
|
|
62
|
+
# --- 3. BUILD ---
|
|
63
|
+
- name: Build Docker image
|
|
38
64
|
id: build-image
|
|
39
65
|
env:
|
|
40
66
|
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
41
67
|
IMAGE_TAG: latest
|
|
42
68
|
run: |
|
|
43
69
|
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
|
|
70
|
+
|
|
71
|
+
# --- 4. CONTAINER SECURITY SCAN ---
|
|
72
|
+
- name: Scan Container for Vulnerabilities (SARIF Output)
|
|
73
|
+
uses: aquasecurity/trivy-action@master
|
|
74
|
+
with:
|
|
75
|
+
image-ref: '${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest'
|
|
76
|
+
format: 'table'
|
|
77
|
+
output: 'trivy-container-summary.txt'
|
|
78
|
+
exit-code: '0' # Informational only, will not break the build
|
|
79
|
+
ignore-unfixed: true
|
|
80
|
+
vuln-type: 'os,library'
|
|
81
|
+
severity: 'CRITICAL,HIGH'
|
|
82
|
+
|
|
83
|
+
- name: Publish Container Summary
|
|
84
|
+
if: always()
|
|
85
|
+
run: |
|
|
86
|
+
if [ -f trivy-container-summary.txt ]; then
|
|
87
|
+
echo "### š”ļø Container Security Scan Results" >> $GITHUB_STEP_SUMMARY
|
|
88
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
89
|
+
cat trivy-container-summary.txt >> $GITHUB_STEP_SUMMARY
|
|
90
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# --- 5. DEPLOY ---
|
|
94
|
+
- name: Push image to Amazon ECR
|
|
95
|
+
env:
|
|
96
|
+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
97
|
+
IMAGE_TAG: latest
|
|
98
|
+
run: |
|
|
44
99
|
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
|
45
100
|
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
|
|
46
101
|
|
|
@@ -8,6 +8,7 @@ data "aws_cloudfront_origin_request_policy" "all_viewer" {
|
|
|
8
8
|
name = "Managed-AllViewerExceptHostHeader"
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
# trivy:ignore:AVD-AWS-0011 - WAF is omitted by default to prevent unexpected monthly costs for users
|
|
11
12
|
resource "aws_cloudfront_distribution" "cdn" {
|
|
12
13
|
enabled = true
|
|
13
14
|
is_ipv6_enabled = true
|
|
@@ -17,8 +17,13 @@ resource "aws_cloudwatch_log_group" "app_logs" {
|
|
|
17
17
|
# --- ECR Repository ---
|
|
18
18
|
resource "aws_ecr_repository" "app" {
|
|
19
19
|
name = "{{PROJECT_NAME}}-repo"
|
|
20
|
+
# trivy:ignore:AVD-AWS-0031 - Mutable tags allow the CI/CD pipeline to reuse the 'latest' tag for simplified deployments
|
|
20
21
|
image_tag_mutability = "MUTABLE"
|
|
21
22
|
force_delete = true
|
|
23
|
+
|
|
24
|
+
image_scanning_configuration {
|
|
25
|
+
scan_on_push = true
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
# --- IAM: Execution Role ---
|
|
@@ -100,11 +105,13 @@ resource "aws_ecs_task_definition" "app" {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
# --- Application Load Balancer ---
|
|
108
|
+
# trivy:ignore:AVD-AWS-0053 - This ALB is intended to be publicly facing behind CloudFront
|
|
103
109
|
resource "aws_lb" "main" {
|
|
104
110
|
name = "{{PROJECT_NAME}}-alb"
|
|
105
111
|
load_balancer_type = "application"
|
|
106
112
|
security_groups = [aws_security_group.alb.id]
|
|
107
113
|
subnets = aws_subnet.public[*].id
|
|
114
|
+
drop_invalid_header_fields = true
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
resource "aws_lb_target_group" "app" {
|
|
@@ -124,6 +131,7 @@ resource "aws_lb_target_group" "app" {
|
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
# trivy:ignore:AVD-AWS-0054 - CloudFront handles HTTPS edge termination; ALB uses HTTP to avoid complex ACM DNS validation for users
|
|
127
135
|
resource "aws_lb_listener" "http" {
|
|
128
136
|
load_balancer_arn = aws_lb.main.arn
|
|
129
137
|
port = "80"
|
|
@@ -19,6 +19,7 @@ resource "aws_internet_gateway" "main" {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
# Create 2 Public Subnets
|
|
22
|
+
# trivy:ignore:AVD-AWS-0164 - Public subnets are used to avoid the $30/mo cost of a NAT Gateway for outbound container traffic
|
|
22
23
|
resource "aws_subnet" "public" {
|
|
23
24
|
count = 2
|
|
24
25
|
vpc_id = aws_vpc.main.id
|
|
@@ -64,6 +65,7 @@ resource "aws_security_group" "alb" {
|
|
|
64
65
|
from_port = 0
|
|
65
66
|
to_port = 0
|
|
66
67
|
protocol = "-1"
|
|
68
|
+
# trivy:ignore:AVD-AWS-0104 - Allow ALB to route out to standard AWS services
|
|
67
69
|
cidr_blocks = ["0.0.0.0/0"]
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -85,6 +87,7 @@ resource "aws_security_group" "ecs_tasks" {
|
|
|
85
87
|
from_port = 0
|
|
86
88
|
to_port = 0
|
|
87
89
|
protocol = "-1"
|
|
90
|
+
# trivy:ignore:AVD-AWS-0104 - Allow containers to pull images and hit external APIs
|
|
88
91
|
cidr_blocks = ["0.0.0.0/0"]
|
|
89
92
|
}
|
|
90
93
|
}
|