Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 93 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,96 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
target: production

integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull request'
services:
mongodb:
image: mongo:6-jammy
env:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
--health-interval 30s
--health-timeout 10s
--health-retries 5

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Run Integration Tests
env:
MONGODB_URI: mongodb://admin:password@localhost:27017/test?authSource=admin
JWT_SECRET: test-secret-key
working-directory: ./backend
run: |
npm ci
npm run test:integration || echo "Integration Tests not configured"


#Job 6: Deploy to Production
deploy-production:
name: Deploying to Production
runs-on: ubuntu-latest
needs: [quality, build]
if: github.ref == 'refs/heads/main'
environment: production

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Deploy Frontend to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PRODUCT_ID }}
working-directory: ./frontend

- name: Deploy Backend to Railway
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN_PRODUCTION }}
run: npx @railway/cli deploy --service backend

update-k8s:
name: Update K8s Manifests
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'

steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}-gitops
token: ${{ secrets.GITHUB_TOKEN }}
path: gitops

- name: Update image tags
run: |
sed -i "s|image: ${{ env.IMAGE_PREFIX }}-backend:.*|image: ${{ env.IMAGE_PREFIX }}-backend:${{ github.sha }}|g" ./gitops/k8s/backend-deployment.yaml
sed -i "s|image: ${{ env.IMAGE_PREFIX }}-frontend:.*|image: ${{ env.IMAGE_PREFIX }}-frontend:${{ github.sha }}|g" ./gitops/k8s/frontend-deployment.yaml

- name: Commit and Push changes
run: |
cd gitops
git congig user.name "github-actions"
git config user.email "actions@gmail.com"
git add .
git commit -m "Update image tags to ${{ github.sha}}" || exit 0
git push origin main


36 changes: 36 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Security Scanning

on:
schedule:
- cron: '0 0 * * 1' # Weekly on Sundays at midnight
workflow_dispatch:

jobs:
security_scan:
name: Weekly Security Scan
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'Issue Tracker'
path: '.'
format: 'JSON'

- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/oswasp-top-ten
p/javascript

- name: Container Security Scan
run: |
docker run --rm -v "${{ github.workspace }}:/src"
aquasec/trivy fs --security-checks vuln,secret,config /src
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform.tfvars
135 changes: 135 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
terraform {
required_version = ">= 1.0.0"
required_providers {
railway = {
source = "railwayapp/railway"
version = "~> 0.2.0"
}
vercel = {
source = "vercel/vercel"
version = "~> 0.15.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
mongodbatlas = {
source = "mongodb/mongodbatlas"
version = "~> 1.0.0"
}
}

backend "local" {
path = "terraform.tfstate"
}
}

# Configure the Railway provider
provider "railway" {
token = var.railway_token
}

provider "vercel" {
token = var.vercel_token
}

provider "cloudflare" {
api_token = var.cloudflare_token
}

provider "mongodbatlas" {
public_key = var.mongodb_public_key
private_key = var.mongodb_private_key
}

# railway backend Deployment
resource "railway_project" "issue_tracker" {
name = "issue-tracker"
descrption = "Issue Tracker Application"
}

resource "railway_service" "backend" {
project_id = railway_project.issue_tracker.id
name = "backend"

source = {
repo = "var.github_repo"
branch = "main"
root_directory = "backend"
}

variables = {
NODE_ENV = "production"
port = "3000"
MONGODB_URL = var.monogodb_uri
JWT_SECRET = var.jwt_secret
}
}

resource "mongodbatlas_project" "issue_tracker" {
name = "issue-tracker"
org_id = var.mongodb_org_id
}

resource "mongodbatlas_cluster" "free_cluster" {
project_id = mongodbatlas_project.issue_tracker.id
name = "issue-tracker-free"

provider_name = "TENENT"
backing_provider_name = "AWS"
provider_region_name = "AP_SOUTH_1"
provider_instance_size_name = "M0"

# free tier settings
disk_size_gb = 5

lifecycle {
ignore_changes = [disk_size_gb]
}
}

# vercel frontend Deployment
resource "vercel_project" "frontend" {
name = "issue-tracker-frontend"
framework = "create-react-app"

git_repository {
type = "github"
repo = var.github_repo
}

root_directory = "frontend"

environment = [
{
key = "REACT_APP_API_URL"
value = "https://${railway_service.backend.domain}/api"
target = ["production", "preview"]
}
]
}

# cloudflare DNS
resource "cloudflare_zone" "main" {
count = var.domain_name != "" ? 1 : 0
zone = var.domain_name
}

resource "cloudflare_record" "app" {
count = var.domain_name != "" ? 1 : 0
zone_id = cloudflare_zone.main[0].id
name = app
value = vercel_project.frontend.domain
type = "CNAME"
proxied = true
}

resource "cloudflare_record" "api" {
count =var.domain_name != "" 1 : 0
zone_id = cloudflare_zone.main[0].id
name = api
value = railway_service.backend.domain
type = "CNAME"
proxied = true
}

19 changes: 19 additions & 0 deletions terraform/modules/monitoring/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# monitoring module for Grafana Cloud
resource "grafana_data_source" "promethues" {
type = "promethues"
name = "promethues"
url = var.promethues_url

json_data = {
http_method = "GET"
}
}

resource "grafana_dashboard" "issue_tracker" {
config_json = file("${path.module}/dashboards/issue_tracker.json")
}

resource "premethues_url" {
description = "Premethues Server URL"
type = string
}
23 changes: 23 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
output "backend_url" {
description = "Backend service URL"
value = "https://${railway_service.backend.domain}"
}

output "frontend_url" {
description = "Frontend Application URL"
value = "https://${vercel_project.frontend.domain}"
}

output "mongodb_connection_string" {
description = "Mongodb Connection String"
value = mongodbatlas_cluster.free_cluster.connection_string[0].standard_srv
sensitive = true
}

output "custom_domain" {
description = "Custom domain URLs"
value = var.domain_name != "" ? {
frontend = "https://app.${var.domain_name}"
backend = "https://api.${var.domain_name}"
} : null
}
59 changes: 59 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
variable "railway_token" {
description = "Railway API token"
type = string
sensitive = true
}

variable "vercel_token" {
description = "Vercel API token"
type = string
sensitive = true
}

varialbe "cloudflare_api_token" {
description = "Cloudflare API Token"
type = string
sensitive = true
default = ""
}

varialbe "github_repo" {
description = "Github repository in format 'owner/repo'"
type = string
}

variable "mongodb_uri" {
description = "Mongodb connection URI"
type = string
sensitive = true
}

variable "jwt_secret" {
description = "JWT secret key"
type = string
sensitive = true
}

varialbe "mongodb_org_id" {
description = "Mongodb Atlas organization ID"
type = string
default = ""
sensitive = true
}

variable "domain_name" {
description = "Domain name for the application"
type = string
default = ""
}

varialbe "environment" {
description = "Environment name"
type = string
default = "production"

validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "Environment must be one pf 'development', 'staging', 'production'"
}
}
Loading