mysystem-cli 1.0.0 → 1.0.1
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/AGENTS.md +66 -0
- package/copy-templates.js +47 -0
- package/package.json +2 -2
- package/templates/docker/fastapi.Dockerfile +32 -0
- package/templates/docker/nextjs.Dockerfile +45 -0
- package/templates/docker/node.Dockerfile +32 -0
- package/templates/docker/react.Dockerfile +38 -0
- package/templates/github/deploy-ec2.yml +163 -0
- package/templates/github/deploy.yml +94 -0
- package/templates/github/destroy.yml +43 -0
- package/templates/terraform/alb.tf +69 -0
- package/templates/terraform/bootstrap-oidc.yaml +69 -0
- package/templates/terraform/budget.tf +40 -0
- package/templates/terraform/db.tf +46 -0
- package/templates/terraform/dns.tf +89 -0
- package/templates/terraform/ecs.tf +110 -0
- package/templates/terraform/outputs.tf +14 -0
- package/templates/terraform/provider.tf +21 -0
- package/templates/terraform/rds_proxy.tf +157 -0
- package/templates/terraform/redis.tf +69 -0
- package/templates/terraform/security.tf +156 -0
- package/templates/terraform/variables.tf +46 -0
- package/templates/terraform/vpc.tf +68 -0
- package/templates/terraform/waf.tf +97 -0
- package/templates/terraform-ec2/budget.tf +40 -0
- package/templates/terraform-ec2/dns.tf +85 -0
- package/templates/terraform-ec2/ec2.tf +124 -0
- package/templates/terraform-ec2/ecr.tf +10 -0
- package/templates/terraform-ec2/outputs.tf +19 -0
- package/templates/terraform-ec2/provider.tf +22 -0
- package/templates/terraform-ec2/variables.tf +58 -0
- package/templates/terraform-ec2/vpc.tf +50 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# MySystem Agent Guidelines (AGENTS.md)
|
|
2
|
+
|
|
3
|
+
This file contains critical production-readiness instructions for AI coding agents (Cursor, Claude Code, Lovable, Bolt, Antigravity, etc.).
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> **To the AI Agent**: You MUST read and follow these rules when editing or creating code in this codebase.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Core Production Architecture Rules
|
|
11
|
+
|
|
12
|
+
### 1.1 Port & Port Binding
|
|
13
|
+
* Always bind the application web server to the port specified in the `PORT` environment variable.
|
|
14
|
+
* If `PORT` is not defined, fall back to a standard default (e.g., `3000` or `8080`), but never hardcode the listen port without checking `process.env.PORT` (Node) or `os.environ.get("PORT")` (Python).
|
|
15
|
+
|
|
16
|
+
### 1.2 Health Check Endpoint
|
|
17
|
+
* The application must expose a GET `/health` or `/healthz` endpoint.
|
|
18
|
+
* It must return a `200 OK` response (preferably JSON, e.g., `{"status": "healthy"}`) with no authentication required.
|
|
19
|
+
* The AWS Load Balancer uses this endpoint to verify if the container is running. If it fails, the container will be terminated and replaced.
|
|
20
|
+
|
|
21
|
+
### 1.3 Statelessness & File Storage
|
|
22
|
+
* Do NOT write persistent files to the local container filesystem. The container instances are ephemeral and can be destroyed at any time.
|
|
23
|
+
* For file uploads, use an external object storage service (e.g., AWS S3).
|
|
24
|
+
* For temporary processing, use the system `/tmp` directory, but expect it to disappear.
|
|
25
|
+
|
|
26
|
+
### 1.4 Environment Variables & Secrets
|
|
27
|
+
* Never hardcode API keys, database credentials, passwords, or secrets.
|
|
28
|
+
* Always read configurations from environment variables.
|
|
29
|
+
* Generate a sample `.env.example` file when adding new environment variables.
|
|
30
|
+
|
|
31
|
+
### 1.5 Database Migrations
|
|
32
|
+
* Ensure database migrations are safe to run concurrently or executed as a single-run step during deployment (never inside the web server's startup command if scaling to multiple container instances).
|
|
33
|
+
* Design database changes to be backward-compatible (e.g., add new columns as nullable, deprecate columns in a two-stage deployment).
|
|
34
|
+
|
|
35
|
+
### 1.6 Logging & Error Tracking (Sentry & Pino)
|
|
36
|
+
* **Structured Logging**: For Node.js/TypeScript applications, format production logs in JSON using `pino` (or standard JSON formatters in Python/Go) for easy querying in CloudWatch. Avoid plain-text print statements in production.
|
|
37
|
+
* **Error Tracking**: If the `SENTRY_DSN` environment variable is present:
|
|
38
|
+
1. Install the appropriate Sentry SDK (e.g., `@sentry/nextjs` for Next.js, `@sentry/node` for Node, or `sentry-sdk` for Python).
|
|
39
|
+
2. Initialize the SDK in the application entrypoint.
|
|
40
|
+
3. Ensure unhandled exceptions and runtime errors are automatically captured and reported to Sentry.
|
|
41
|
+
|
|
42
|
+
### 1.7 EC2 Instance Sizing (Hobbyist Tier)
|
|
43
|
+
If the project is configured to use the **Hobbyist Tier** (single EC2 + Docker Compose), you must select and manage the `instance_type` parameter in `terraform/terraform.tfvars` according to the application footprint:
|
|
44
|
+
* **Micro-services / Static Frontends (Vite/React)**: Use `t4g.nano` (ARM64, 0.5GB RAM, ~$3.20/month) for maximum savings.
|
|
45
|
+
* **Standard Frameworks (Express, Next.js, FastAPI + Postgres)**: Use `t3.micro` (x86, 1GB RAM, **AWS Free Tier eligible**).
|
|
46
|
+
* **Medium Apps (App + Postgres + Redis or memory-intensive runtimes)**: Use `t3.small` (2GB RAM, ~$16/month) to prevent Out-Of-Memory (OOM) crashes.
|
|
47
|
+
* **Large Monoliths**: Scale to `t3.medium` (4GB RAM, ~$32/month) or larger when active traffic warrants it.
|
|
48
|
+
* *Note: When deploying on t4g (ARM) instances, ensure the Dockerfile is compiled for arm64.*
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. Docker & Container Rules
|
|
53
|
+
* Do not modify the `Dockerfile` in a way that breaks multi-stage optimization.
|
|
54
|
+
* Keep container images small by using minimal base images (e.g., alpine or slim variants).
|
|
55
|
+
* Always run the container application under a non-root user (e.g., `node` in Node.js) for security.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. Automated Git Push Workflow (Vibecoder Deployment)
|
|
60
|
+
If the user tells you: **"I have set up the OIDC role / GitHub secrets"** or **"Deploy the app now"**:
|
|
61
|
+
|
|
62
|
+
1. **Verify Git Status**: Run `git status` via terminal to verify the changes.
|
|
63
|
+
2. **Stage files**: Run `git add .` to stage the changes.
|
|
64
|
+
3. **Commit**: Commit with a clean message: `git commit -m "chore: configure MySystem deployment assets"`
|
|
65
|
+
4. **Push**: Identify the active branch (usually `main` or `master`) and push it: `git push origin main`
|
|
66
|
+
5. **Confirm**: Let the user know the push was successful and the GitHub Actions deployment pipeline has been triggered.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const rootDir = path.join(__dirname, '../..');
|
|
5
|
+
const cliDir = __dirname;
|
|
6
|
+
|
|
7
|
+
const srcTemplates = path.join(rootDir, 'templates');
|
|
8
|
+
const destTemplates = path.join(cliDir, 'templates');
|
|
9
|
+
|
|
10
|
+
const srcAgents = path.join(rootDir, 'AGENTS.md');
|
|
11
|
+
const destAgents = path.join(cliDir, 'AGENTS.md');
|
|
12
|
+
|
|
13
|
+
// Helper to recursively copy directories
|
|
14
|
+
function copyDirSync(src, dest) {
|
|
15
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
16
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
17
|
+
|
|
18
|
+
for (let entry of entries) {
|
|
19
|
+
const srcPath = path.join(src, entry.name);
|
|
20
|
+
const destPath = path.join(dest, entry.name);
|
|
21
|
+
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
copyDirSync(srcPath, destPath);
|
|
24
|
+
} else {
|
|
25
|
+
fs.copyFileSync(srcPath, destPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('Copying templates to CLI package...');
|
|
31
|
+
if (fs.existsSync(srcTemplates)) {
|
|
32
|
+
if (fs.existsSync(destTemplates)) {
|
|
33
|
+
fs.rmSync(destTemplates, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
copyDirSync(srcTemplates, destTemplates);
|
|
36
|
+
console.log(' ✅ Templates copied.');
|
|
37
|
+
} else {
|
|
38
|
+
console.error(' ❌ Source templates folder not found at:', srcTemplates);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log('Copying AGENTS.md to CLI package...');
|
|
42
|
+
if (fs.existsSync(srcAgents)) {
|
|
43
|
+
fs.copyFileSync(srcAgents, destAgents);
|
|
44
|
+
console.log(' ✅ AGENTS.md copied.');
|
|
45
|
+
} else {
|
|
46
|
+
console.error(' ❌ Source AGENTS.md not found at:', srcAgents);
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mysystem-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Zero-config deployment standard and CLI for AI-generated applications on AWS Fargate",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"mysystem-cli": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
11
|
+
"build": "tsc && node copy-templates.js",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Multi-stage build for Python FastAPI Applications
|
|
2
|
+
|
|
3
|
+
# --- Build Stage ---
|
|
4
|
+
FROM python:3.11-slim AS builder
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
RUN python -m venv /opt/venv
|
|
7
|
+
ENV PATH="/opt/venv/bin:$PATH"
|
|
8
|
+
COPY requirements.txt .
|
|
9
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
10
|
+
|
|
11
|
+
# --- Production Stage ---
|
|
12
|
+
FROM python:3.11-slim AS runner
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
COPY --from=builder /opt/venv /opt/venv
|
|
15
|
+
ENV PATH="/opt/venv/bin:$PATH"
|
|
16
|
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
17
|
+
ENV PYTHONUNBUFFERED=1
|
|
18
|
+
|
|
19
|
+
COPY . .
|
|
20
|
+
|
|
21
|
+
# Run as non-privileged user
|
|
22
|
+
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser:appuser /app
|
|
23
|
+
USER appuser
|
|
24
|
+
|
|
25
|
+
# Healthcheck
|
|
26
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
27
|
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:' + str(os.environ.get('PORT', 8000)) + '/health', timeout=5)" || exit 1
|
|
28
|
+
|
|
29
|
+
EXPOSE 8000
|
|
30
|
+
ENV PORT=8000
|
|
31
|
+
|
|
32
|
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Multi-stage build for Next.js (using Standalone output)
|
|
2
|
+
|
|
3
|
+
# --- Dependencies Stage ---
|
|
4
|
+
FROM node:20-alpine AS deps
|
|
5
|
+
RUN apk add --no-cache libc6-compat
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
COPY package*.json ./
|
|
8
|
+
RUN npm ci
|
|
9
|
+
|
|
10
|
+
# --- Build Stage ---
|
|
11
|
+
FROM node:20-alpine AS builder
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
14
|
+
COPY . .
|
|
15
|
+
# Set Next.js telemetry disable
|
|
16
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
17
|
+
RUN npm run build
|
|
18
|
+
|
|
19
|
+
# --- Production Stage ---
|
|
20
|
+
FROM node:20-alpine AS runner
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
|
|
23
|
+
ENV NODE_ENV=production
|
|
24
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
25
|
+
|
|
26
|
+
# Create non-root user
|
|
27
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
28
|
+
RUN adduser --system --uid 1001 nextjs
|
|
29
|
+
|
|
30
|
+
# Copy standalone build and static assets
|
|
31
|
+
COPY --from=builder /app/public ./public
|
|
32
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
33
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
34
|
+
|
|
35
|
+
USER nextjs
|
|
36
|
+
|
|
37
|
+
EXPOSE 3000
|
|
38
|
+
ENV PORT=3000
|
|
39
|
+
ENV HOSTNAME="0.0.0.0"
|
|
40
|
+
|
|
41
|
+
# Healthcheck
|
|
42
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
43
|
+
CMD node -e "fetch('http://localhost:3000/api/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
|
|
44
|
+
|
|
45
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Multi-stage build for Node.js Applications
|
|
2
|
+
|
|
3
|
+
# --- Build Stage ---
|
|
4
|
+
FROM node:20-alpine AS builder
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm ci
|
|
8
|
+
COPY . .
|
|
9
|
+
RUN npm run build --if-present
|
|
10
|
+
|
|
11
|
+
# --- Production Stage ---
|
|
12
|
+
FROM node:20-alpine AS runner
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
ENV NODE_ENV=production
|
|
15
|
+
COPY package*.json ./
|
|
16
|
+
RUN npm ci --only=production
|
|
17
|
+
COPY --from=builder /app/dist ./dist --keep-directory-structure --if-present
|
|
18
|
+
COPY --from=builder /app/build ./build --keep-directory-structure --if-present
|
|
19
|
+
COPY --from=builder /app/server.js ./server.js --if-present
|
|
20
|
+
COPY --from=builder /app/index.js ./index.js --if-present
|
|
21
|
+
|
|
22
|
+
# Set safe, non-root user
|
|
23
|
+
USER node
|
|
24
|
+
|
|
25
|
+
# Health check setup
|
|
26
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
27
|
+
CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
|
|
28
|
+
|
|
29
|
+
EXPOSE 3000
|
|
30
|
+
ENV PORT=3000
|
|
31
|
+
|
|
32
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Multi-stage build for React/Vite Single Page Applications (served via Nginx)
|
|
2
|
+
|
|
3
|
+
# --- Build Stage ---
|
|
4
|
+
FROM node:20-alpine AS builder
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm ci
|
|
8
|
+
COPY . .
|
|
9
|
+
RUN npm run build
|
|
10
|
+
|
|
11
|
+
# --- Production Stage ---
|
|
12
|
+
FROM nginx:alpine AS runner
|
|
13
|
+
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
14
|
+
|
|
15
|
+
# Custom Nginx configuration to support SPA routing (redirect all fallback routes to index.html)
|
|
16
|
+
RUN echo $'\n\
|
|
17
|
+
server {\n\
|
|
18
|
+
listen 80;\n\
|
|
19
|
+
location / {\n\
|
|
20
|
+
root /usr/share/nginx/html;\n\
|
|
21
|
+
index index.html index.htm;\n\
|
|
22
|
+
try_files $uri $uri/ /index.html;\n\
|
|
23
|
+
}\n\
|
|
24
|
+
location /health {\n\
|
|
25
|
+
access_log off;\n\
|
|
26
|
+
add_header Content-Type text/plain;\n\
|
|
27
|
+
return 200 "healthy\\n";\n\
|
|
28
|
+
}\n\
|
|
29
|
+
}' > /etc/nginx/conf.d/default.conf
|
|
30
|
+
|
|
31
|
+
# Healthcheck
|
|
32
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
33
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
|
34
|
+
|
|
35
|
+
EXPOSE 80
|
|
36
|
+
ENV PORT=80
|
|
37
|
+
|
|
38
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
name: MySystem EC2 Deployment
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
deploy:
|
|
15
|
+
name: Deploy to AWS EC2 (Hobbyist Tier)
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout Code
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
# --- Step 1: Parse MySystem Config File ---
|
|
23
|
+
- name: Parse MySystem Config
|
|
24
|
+
id: config
|
|
25
|
+
run: |
|
|
26
|
+
echo "APP_NAME=$(jq -r .name mysystem.json)" >> $GITHUB_ENV
|
|
27
|
+
echo "AWS_REGION=$(jq -r .region mysystem.json)" >> $GITHUB_ENV
|
|
28
|
+
echo "CONTAINER_PORT=$(jq -r .port mysystem.json)" >> $GITHUB_ENV
|
|
29
|
+
echo "BILLING_EMAIL=$(jq -r .billingEmail mysystem.json)" >> $GITHUB_ENV
|
|
30
|
+
echo "ENABLE_CUSTOM_DOMAIN=$(jq -r .customDomain mysystem.json)" >> $GITHUB_ENV
|
|
31
|
+
echo "DOMAIN_NAME=$(jq -r .domainName mysystem.json)" >> $GITHUB_ENV
|
|
32
|
+
echo "DNS_PROVIDER=$(jq -r .dnsProvider mysystem.json)" >> $GITHUB_ENV
|
|
33
|
+
|
|
34
|
+
# --- Step 2: Authenticate with AWS via OIDC ---
|
|
35
|
+
- name: Configure AWS Credentials
|
|
36
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
37
|
+
with:
|
|
38
|
+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
39
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
40
|
+
audience: sts.amazonaws.com
|
|
41
|
+
|
|
42
|
+
# --- Step 3: Provision EC2 & ECR via Terraform ---
|
|
43
|
+
- name: Setup Terraform
|
|
44
|
+
uses: hashicorp/setup-terraform@v3
|
|
45
|
+
with:
|
|
46
|
+
terraform_version: 1.6.0
|
|
47
|
+
|
|
48
|
+
- name: Terraform Init & Apply
|
|
49
|
+
working-directory: ./terraform
|
|
50
|
+
env:
|
|
51
|
+
TF_VAR_aws_region: ${{ env.AWS_REGION }}
|
|
52
|
+
TF_VAR_app_name: ${{ env.APP_NAME }}
|
|
53
|
+
TF_VAR_container_port: ${{ env.CONTAINER_PORT }}
|
|
54
|
+
TF_VAR_billing_email: ${{ env.BILLING_EMAIL }}
|
|
55
|
+
TF_VAR_enable_custom_domain: ${{ env.ENABLE_CUSTOM_DOMAIN }}
|
|
56
|
+
TF_VAR_domain_name: ${{ env.DOMAIN_NAME }}
|
|
57
|
+
TF_VAR_dns_provider: ${{ env.DNS_PROVIDER }}
|
|
58
|
+
run: |
|
|
59
|
+
terraform init
|
|
60
|
+
terraform apply -auto-approve
|
|
61
|
+
|
|
62
|
+
# --- Step 4: Extract Terraform Outputs ---
|
|
63
|
+
- name: Get Outputs
|
|
64
|
+
id: tf_outputs
|
|
65
|
+
working-directory: ./terraform
|
|
66
|
+
run: |
|
|
67
|
+
echo "ECR_REPO=$(terraform output -raw ecr_repository_url)" >> $GITHUB_OUTPUT
|
|
68
|
+
echo "INSTANCE_ID=$(terraform output -raw instance_id)" >> $GITHUB_OUTPUT
|
|
69
|
+
echo "APP_URL=$(terraform output -raw application_url)" >> $GITHUB_OUTPUT
|
|
70
|
+
|
|
71
|
+
# --- Step 5: Login to Amazon ECR ---
|
|
72
|
+
- name: Login to Amazon ECR
|
|
73
|
+
id: login-ecr
|
|
74
|
+
uses: aws-actions/amazon-ecr-login@v2
|
|
75
|
+
|
|
76
|
+
# --- Step 6: Build and Push Docker Image ---
|
|
77
|
+
- name: Build, Tag, and Push Image to ECR
|
|
78
|
+
id: build-image
|
|
79
|
+
env:
|
|
80
|
+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
81
|
+
ECR_REPOSITORY: ${{ steps.tf_outputs.outputs.ECR_REPO }}
|
|
82
|
+
IMAGE_TAG: ${{ github.sha }}
|
|
83
|
+
run: |
|
|
84
|
+
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
|
|
85
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
|
86
|
+
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
|
|
87
|
+
|
|
88
|
+
# --- Step 7: Deploy to EC2 via AWS SSM ---
|
|
89
|
+
- name: Deploy to EC2 via AWS SSM
|
|
90
|
+
env:
|
|
91
|
+
INSTANCE_ID: ${{ steps.tf_outputs.outputs.INSTANCE_ID }}
|
|
92
|
+
IMAGE_URL: ${{ steps.build-image.outputs.image }}
|
|
93
|
+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
94
|
+
AWS_DEFAULT_REGION: ${{ env.AWS_REGION }}
|
|
95
|
+
CONTAINER_PORT: ${{ env.CONTAINER_PORT }}
|
|
96
|
+
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
97
|
+
run: |
|
|
98
|
+
# Generate a secure password for Postgres
|
|
99
|
+
DB_PASSWORD=$(openssl rand -base64 12)
|
|
100
|
+
|
|
101
|
+
# Write the deployment script
|
|
102
|
+
cat <<EOF > deploy.sh
|
|
103
|
+
#!/bin/bash
|
|
104
|
+
# 1. Login to ECR
|
|
105
|
+
aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
|
|
106
|
+
|
|
107
|
+
# 2. Create app directory
|
|
108
|
+
mkdir -p /home/ec2-user/app
|
|
109
|
+
|
|
110
|
+
# 3. Create docker-compose.yml
|
|
111
|
+
cat <<'COMPOSE' > /home/ec2-user/app/docker-compose.yml
|
|
112
|
+
version: '3.8'
|
|
113
|
+
services:
|
|
114
|
+
web:
|
|
115
|
+
image: ${IMAGE_URL}
|
|
116
|
+
restart: always
|
|
117
|
+
ports:
|
|
118
|
+
- "80:${CONTAINER_PORT}"
|
|
119
|
+
environment:
|
|
120
|
+
- PORT=${CONTAINER_PORT}
|
|
121
|
+
- NODE_ENV=production
|
|
122
|
+
- DATABASE_URL=postgresql://mysystem_admin:${DB_PASSWORD}@db:5432/mysystem_db
|
|
123
|
+
- SENTRY_DSN=${SENTRY_DSN}
|
|
124
|
+
depends_on:
|
|
125
|
+
- db
|
|
126
|
+
|
|
127
|
+
db:
|
|
128
|
+
image: postgres:15-alpine
|
|
129
|
+
restart: always
|
|
130
|
+
environment:
|
|
131
|
+
- POSTGRES_USER=mysystem_admin
|
|
132
|
+
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
133
|
+
- POSTGRES_DB=mysystem_db
|
|
134
|
+
volumes:
|
|
135
|
+
- pgdata:/var/lib/postgresql/data
|
|
136
|
+
|
|
137
|
+
volumes:
|
|
138
|
+
pgdata:
|
|
139
|
+
COMPOSE
|
|
140
|
+
|
|
141
|
+
# 4. Pull and run containers
|
|
142
|
+
cd /home/ec2-user/app
|
|
143
|
+
docker compose pull
|
|
144
|
+
docker compose up -d
|
|
145
|
+
EOF
|
|
146
|
+
|
|
147
|
+
# Base64 encode the script to avoid all command-line escaping issues
|
|
148
|
+
B64_SCRIPT=\$(base64 -w 0 deploy.sh)
|
|
149
|
+
|
|
150
|
+
# Send run command to AWS SSM
|
|
151
|
+
CMD_ID=\$(aws ssm send-command \
|
|
152
|
+
--instance-ids "${INSTANCE_ID}" \
|
|
153
|
+
--document-name "AWS-RunShellScript" \
|
|
154
|
+
--parameters "commands=[\"echo '\$B64_SCRIPT' | base64 -d > /tmp/deploy.sh\", \"chmod +x /tmp/deploy.sh\", \"/tmp/deploy.sh\"]" \
|
|
155
|
+
--query "Command.CommandId" \
|
|
156
|
+
--region "${AWS_DEFAULT_REGION}" \
|
|
157
|
+
--output text)
|
|
158
|
+
|
|
159
|
+
echo "Deployment initiated via SSM. Command ID: \$CMD_ID"
|
|
160
|
+
|
|
161
|
+
- name: Show App URL
|
|
162
|
+
run: |
|
|
163
|
+
echo "🎉 App successfully deployed to: ${{ steps.tf_outputs.outputs.APP_URL }}"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: MySystem Deployment
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write # Required for AWS OIDC authentication
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
deploy:
|
|
15
|
+
name: Deploy to AWS Fargate
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout Code
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
# --- Step 1: Authenticate with AWS via OIDC ---
|
|
23
|
+
- name: Configure AWS Credentials
|
|
24
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
25
|
+
with:
|
|
26
|
+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
27
|
+
aws-region: us-east-1 # Will be customized by CLI on setup
|
|
28
|
+
audience: sts.amazonaws.com
|
|
29
|
+
|
|
30
|
+
# --- Step 2: Provision Infrastructure using Terraform ---
|
|
31
|
+
- name: Setup Terraform
|
|
32
|
+
uses: hashicorp/setup-terraform@v3
|
|
33
|
+
with:
|
|
34
|
+
terraform_version: 1.6.0
|
|
35
|
+
|
|
36
|
+
- name: Terraform Init & Apply
|
|
37
|
+
working-directory: ./terraform
|
|
38
|
+
run: |
|
|
39
|
+
terraform init
|
|
40
|
+
terraform apply -auto-approve
|
|
41
|
+
|
|
42
|
+
# --- Step 3: Extract Terraform Outputs ---
|
|
43
|
+
- name: Get Outputs
|
|
44
|
+
id: tf_outputs
|
|
45
|
+
working-directory: ./terraform
|
|
46
|
+
run: |
|
|
47
|
+
echo "ECR_REPO=$(terraform output -raw ecr_repository_url)" >> $GITHUB_OUTPUT
|
|
48
|
+
echo "APP_URL=$(terraform output -raw application_url)" >> $GITHUB_OUTPUT
|
|
49
|
+
|
|
50
|
+
# --- Step 4: Login to Amazon ECR ---
|
|
51
|
+
- name: Login to Amazon ECR
|
|
52
|
+
id: login-ecr
|
|
53
|
+
uses: aws-actions/amazon-ecr-login@v2
|
|
54
|
+
|
|
55
|
+
# --- Step 5: Build and Push Docker Image ---
|
|
56
|
+
- name: Build, Tag, and Push Image to ECR
|
|
57
|
+
id: build-image
|
|
58
|
+
env:
|
|
59
|
+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
60
|
+
ECR_REPOSITORY: ${{ steps.tf_outputs.outputs.ECR_REPO }}
|
|
61
|
+
IMAGE_TAG: ${{ github.sha }}
|
|
62
|
+
run: |
|
|
63
|
+
# Build a local Docker image
|
|
64
|
+
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
|
|
65
|
+
# Push it to ECR
|
|
66
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
|
67
|
+
# Save image URL as output
|
|
68
|
+
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
|
|
69
|
+
|
|
70
|
+
# --- Step 6: Deploy to ECS Fargate ---
|
|
71
|
+
# This pulls down the task definition template, updates it with the new image, and deploys it.
|
|
72
|
+
- name: Download Task Definition
|
|
73
|
+
run: |
|
|
74
|
+
aws ecs describe-task-definition --task-definition ${{ github.event.repository.name }} --query taskDefinition > task-definition.json
|
|
75
|
+
|
|
76
|
+
- name: Fill in the new image ID in the Amazon ECS task definition
|
|
77
|
+
id: render-task-def
|
|
78
|
+
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
|
79
|
+
with:
|
|
80
|
+
task-definition: task-definition.json
|
|
81
|
+
container-name: ${{ github.event.repository.name }}
|
|
82
|
+
image: ${{ steps.build-image.outputs.image }}
|
|
83
|
+
|
|
84
|
+
- name: Deploy Amazon ECS task definition
|
|
85
|
+
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
|
86
|
+
with:
|
|
87
|
+
task-definition: ${{ steps.render-task-def.outputs.task-definition }}
|
|
88
|
+
service: ${{ github.event.repository.name }}
|
|
89
|
+
cluster: ${{ github.event.repository.name }}-cluster
|
|
90
|
+
wait-for-service-stability: true
|
|
91
|
+
|
|
92
|
+
- name: Show App URL
|
|
93
|
+
run: |
|
|
94
|
+
echo "🎉 App successfully deployed to: ${{ steps.tf_outputs.outputs.APP_URL }}"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: MySystem Teardown (Destroy)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch: # Allows manual trigger from the GitHub Actions UI
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
id-token: write
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
destroy:
|
|
12
|
+
name: Teardown AWS Infrastructure
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout Code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
# --- Step 1: Authenticate with AWS via OIDC ---
|
|
20
|
+
- name: Configure AWS Credentials
|
|
21
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
22
|
+
with:
|
|
23
|
+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
24
|
+
aws-region: us-east-1 # Will be customized by CLI on setup
|
|
25
|
+
audience: sts.amazonaws.com
|
|
26
|
+
|
|
27
|
+
# --- Step 2: Set up Terraform ---
|
|
28
|
+
- name: Setup Terraform
|
|
29
|
+
uses: hashicorp/setup-terraform@v3
|
|
30
|
+
with:
|
|
31
|
+
terraform_version: 1.6.0
|
|
32
|
+
|
|
33
|
+
# --- Step 3: Run Terraform Destroy ---
|
|
34
|
+
- name: Terraform Init & Destroy
|
|
35
|
+
working-directory: ./terraform
|
|
36
|
+
run: |
|
|
37
|
+
terraform init
|
|
38
|
+
terraform destroy -auto-approve
|
|
39
|
+
|
|
40
|
+
- name: Success Message
|
|
41
|
+
run: |
|
|
42
|
+
echo "🔥 All AWS resources for this application have been successfully destroyed."
|
|
43
|
+
echo "Your AWS billing for this project has been stopped."
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
resource "aws_lb" "main" {
|
|
2
|
+
name = "${var.app_name}-alb"
|
|
3
|
+
internal = false
|
|
4
|
+
load_balancer_type = "application"
|
|
5
|
+
security_groups = [aws_security_group.alb.id]
|
|
6
|
+
subnets = aws_subnet.public[*].id
|
|
7
|
+
|
|
8
|
+
tags = {
|
|
9
|
+
Name = "${var.app_name}-alb"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
resource "aws_lb_target_group" "app" {
|
|
14
|
+
name = "${var.app_name}-tg"
|
|
15
|
+
port = var.container_port
|
|
16
|
+
protocol = "HTTP"
|
|
17
|
+
vpc_id = aws_vpc.main.id
|
|
18
|
+
target_type = "ip"
|
|
19
|
+
|
|
20
|
+
health_check {
|
|
21
|
+
healthy_threshold = 3
|
|
22
|
+
unhealthy_threshold = 3
|
|
23
|
+
timeout = 5
|
|
24
|
+
interval = 30
|
|
25
|
+
path = "/health" # Compliant with AGENTS.md rule
|
|
26
|
+
protocol = "HTTP"
|
|
27
|
+
matcher = "200"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
tags = {
|
|
31
|
+
Name = "${var.app_name}-tg"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# HTTP Listener (Port 80)
|
|
36
|
+
resource "aws_lb_listener" "http" {
|
|
37
|
+
load_balancer_arn = aws_lb.main.arn
|
|
38
|
+
port = "80"
|
|
39
|
+
protocol = "HTTP"
|
|
40
|
+
|
|
41
|
+
default_action {
|
|
42
|
+
type = var.enable_custom_domain ? "redirect" : "forward"
|
|
43
|
+
target_group_arn = var.enable_custom_domain ? null : aws_lb_target_group.app.arn
|
|
44
|
+
|
|
45
|
+
dynamic "redirect" {
|
|
46
|
+
for_each = var.enable_custom_domain ? [1] : []
|
|
47
|
+
content {
|
|
48
|
+
port = "443"
|
|
49
|
+
protocol = "HTTPS"
|
|
50
|
+
status_code = "HTTP_301"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# HTTPS Listener (Port 443) - Created only if custom domain is enabled
|
|
57
|
+
resource "aws_lb_listener" "https" {
|
|
58
|
+
count = var.enable_custom_domain ? 1 : 0
|
|
59
|
+
load_balancer_arn = aws_lb.main.arn
|
|
60
|
+
port = "443"
|
|
61
|
+
protocol = "HTTPS"
|
|
62
|
+
ssl_policy = "ELBSecurityPolicy-2016-08"
|
|
63
|
+
certificate_arn = aws_acm_certificate.cert[0].arn
|
|
64
|
+
|
|
65
|
+
default_action {
|
|
66
|
+
type = "forward"
|
|
67
|
+
target_group_arn = aws_lb_target_group.app.arn
|
|
68
|
+
}
|
|
69
|
+
}
|