Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8701280
Add hardened multi-stage Dockerfile and fix line endings
rayenmabrouk Sep 22, 2026
a0da63c
Add .dockerignore and docker-compose.yml for local development
rayenmabrouk Sep 22, 2026
4b93a81
Add CI pipeline with tests, linting, and container security scan
rayenmabrouk Sep 22, 2026
c163c88
fix: harden container image and fix Trivy CI scan
rayenmabrouk Sep 22, 2026
c126a04
Add modular Terraform infrastructure for AWS deployment
rayenmabrouk Sep 23, 2026
f4d16cf
Merge pull request #1 from rayenmabrouk/feature/terraform-infrastructure
rayenmabrouk Sep 23, 2026
1e2beae
Deploy dpaste to EC2 over HTTPS with SSM-managed secret
rayenmabrouk Sep 23, 2026
28ad8ae
Merge pull request #2 from rayenmabrouk/feature/app-deployment
rayenmabrouk Sep 23, 2026
46bf324
Move Terraform state to encrypted, versioned S3 backend with native l…
rayenmabrouk Sep 23, 2026
c019332
Merge pull request #3 from rayenmabrouk/feature/remote-state
rayenmabrouk Sep 23, 2026
d9781f0
Add CD pipeline: build, Trivy gate, ECR push, SSM deploy, HTTPS smoke…
rayenmabrouk Sep 23, 2026
423bd3d
Merge pull request #4 from rayenmabrouk/feature/cd-pipeline
rayenmabrouk Sep 23, 2026
d30ff19
Add Terraform pipeline with Checkov gate and approval-gated apply
rayenmabrouk Sep 23, 2026
0ff14e2
Merge pull request #5 from rayenmabrouk/feature/terraform-pipeline
rayenmabrouk Sep 23, 2026
13ff9fd
Add daily snippet cleanup timer and ECR credential helper to deploy s…
rayenmabrouk Sep 23, 2026
c762529
Merge pull request #6 from rayenmabrouk/feature/ops-cleanup-credhelper
rayenmabrouk Sep 23, 2026
a889e45
Add Prometheus + Grafana monitoring stack with black-box probes and C…
rayenmabrouk Sep 23, 2026
aed629f
Merge pull request #7 from rayenmabrouk/feature/monitoring-stack
rayenmabrouk Sep 23, 2026
c87df22
Add project README, runbook, troubleshooting and screenshots
rayenmabrouk Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
.venv
.pytest-cache
.vscode
.coverage
dpaste.db
build
dist
dpaste.egg-info
.git
.github
node_modules
*.pyc
__pycache__
.pytest_cache
.tox
*.egg-info
dist
build
.venv
venv
*.sqlite
Dockerfile*
docker-compose*
.dockerignore
.travis.yml
.gitattributes
docs
terraform
monitoring
scripts
*.md
!setup.cfg
!README.md
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
* text=auto
*.py text eol=lf
*.cfg text eol=lf
*.txt text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
*.json text eol=lf
Dockerfile text eol=lf
Makefile text eol=lf
*.tf text eol=lf
*.hcl text eol=lf
177 changes: 177 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# ============================================================
# CloudPulse - CD pipeline
# Build -> Trivy gate -> push to ECR -> deploy to EC2 via SSM Run Command
# -> HTTPS smoke test. Runs on merges to master that change the app,
# the image or the deploy script; can also be started manually.
# ============================================================
name: CD

on:
push:
branches: [master]
paths:
- "dpaste/**"
- "client/**"
- "Dockerfile.hardened"
- "setup.py"
- "setup.cfg"
- "package.json"
- "package-lock.json"
- "scripts/deploy.sh"
- ".github/workflows/cd.yml"
workflow_dispatch:

# Never run two deployments at once; queue instead of cancelling one mid-way
concurrency:
group: cd-production
cancel-in-progress: false

permissions:
contents: read

env:
AWS_REGION: us-east-1
ECR_REPOSITORY: cloudpulse
INSTANCE_NAME: cloudpulse-server

jobs:
build-scan-push:
name: Build, scan, push
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v4

- name: Image tag = short commit SHA
id: meta
run: echo "tag=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

# AWS Academy session credentials (OIDC is blocked in the Learner Lab).
# They expire with the lab session; refreshed by scripts/refresh-github-aws-secrets.ps1
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
aws-region: ${{ env.AWS_REGION }}

- name: Log in to Amazon ECR
id: ecr
uses: aws-actions/amazon-ecr-login@v2

- uses: docker/setup-buildx-action@v3

- name: Build image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.hardened
load: true
push: false
provenance: false
sbom: false
tags: ${{ steps.ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ steps.meta.outputs.tag }}

# Same policy as CI: fail on fixable CRITICAL/HIGH. Nothing is pushed if this fails.
- name: Trivy scan (release gate)
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ steps.ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ steps.meta.outputs.tag }}
format: table
exit-code: "1"
severity: CRITICAL,HIGH
ignore-unfixed: true
trivyignores: .trivyignore

# ECR tags are immutable: a re-run for the same commit must not fail on push
- name: Push image to ECR
run: |
TAG="${{ steps.meta.outputs.tag }}"
if aws ecr describe-images --repository-name "$ECR_REPOSITORY" --image-ids imageTag="$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists in ECR (immutable) - skipping push"
else
docker push "${{ steps.ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:$TAG"
fi

deploy:
name: Deploy to EC2 via SSM
needs: build-scan-push
runs-on: ubuntu-latest
environment:
name: production
url: ${{ steps.deploy.outputs.app_url }}
steps:
- uses: actions/checkout@v4

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
aws-region: ${{ env.AWS_REGION }}

- name: Deploy via SSM Run Command
id: deploy
env:
IMAGE_TAG: ${{ needs.build-scan-push.outputs.image_tag }}
run: |
set -euo pipefail
INSTANCE_ID=$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=${INSTANCE_NAME}" "Name=instance-state-name,Values=running" \
--query "Reservations[0].Instances[0].InstanceId" --output text)
if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "None" ]; then
echo "::error::No running instance tagged ${INSTANCE_NAME}"; exit 1
fi
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
--query "Reservations[0].Instances[0].PublicIpAddress" --output text)
echo "Deploying tag ${IMAGE_TAG} to ${INSTANCE_ID} (${PUBLIC_IP})"

# Ship this commit's deploy.sh with the command, so the instance
# always runs the reviewed version from Git.
SCRIPT_B64=$(base64 -w0 scripts/deploy.sh)
jq -n --arg b64 "$SCRIPT_B64" --arg tag "$IMAGE_TAG" '{commands: [
"set -e",
"echo \($b64) | base64 -d > /opt/cloudpulse/deploy.sh",
"chmod 0755 /opt/cloudpulse/deploy.sh",
"/opt/cloudpulse/deploy.sh \($tag)"
]}' > ssm-params.json

CMD_ID=$(aws ssm send-command \
--instance-ids "$INSTANCE_ID" \
--document-name AWS-RunShellScript \
--comment "CloudPulse deploy ${IMAGE_TAG}" \
--parameters file://ssm-params.json \
--query Command.CommandId --output text)
echo "SSM command ID: ${CMD_ID}"

# Poll until the command finishes (max 5 minutes)
STATUS=Pending
for _ in $(seq 1 60); do
sleep 5
STATUS=$(aws ssm get-command-invocation --command-id "$CMD_ID" \
--instance-id "$INSTANCE_ID" --query Status --output text 2>/dev/null || echo Pending)
case "$STATUS" in Pending|InProgress|Delayed) continue ;; *) break ;; esac
done

echo "----- deploy.sh output -----"
aws ssm get-command-invocation --command-id "$CMD_ID" --instance-id "$INSTANCE_ID" \
--query StandardOutputContent --output text || true
echo "----- stderr -----"
aws ssm get-command-invocation --command-id "$CMD_ID" --instance-id "$INSTANCE_ID" \
--query StandardErrorContent --output text || true
echo "SSM status: ${STATUS}"
if [ "$STATUS" != "Success" ]; then
echo "::error::Deployment failed with status ${STATUS}"; exit 1
fi

echo "app_url=https://${PUBLIC_IP//./-}.sslip.io" >> "$GITHUB_OUTPUT"

- name: Smoke test over HTTPS
env:
APP_URL: ${{ steps.deploy.outputs.app_url }}
run: |
curl -sS --fail --retry 10 --retry-delay 5 --retry-all-errors \
-o /dev/null -w "GET / -> HTTP %{http_code} in %{time_total}s\n" "${APP_URL}/"
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run tests with coverage
run: pytest dpaste/ --tb=short -q

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install ruff
run: pip install ruff

- name: Run linter
run: ruff check dpaste/
continue-on-error: true

docker-build-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build hardened image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.hardened
push: false
load: true
tags: cloudpulse:ci-${{ github.sha }}

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: cloudpulse:ci-${{ github.sha }}
format: table
exit-code: 1
severity: CRITICAL,HIGH
ignore-unfixed: true
trivyignores: .trivyignore
File renamed without changes.
Loading