chore: drop staging and improve to adopt doppler in CI
Backend CD / build (push) Successful in 7m59s

This commit is contained in:
2026-07-22 11:02:14 -03:00
parent 8901084fa9
commit 488586be48
27 changed files with 299 additions and 999 deletions
+84
View File
@@ -0,0 +1,84 @@
name: Backend CD
on:
workflow_dispatch:
push:
branches: [ main ]
paths:
- 'server/**'
- '.github/workflows/cd-backend.yml'
jobs:
build:
runs-on: graalvm-25
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/server/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Generate version tag
id: version
run: |
DATE=$(date +'%Y.%m.%d')
TAG="api-v${DATE}.${{ github.run_number }}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Generated tag: ${TAG}"
- name: Build Docker image with Spring Boot
working-directory: ./server
run: |
./mvnw -Pnative -DskipTests spring-boot:build-image \
-Dspring-boot.build-image.imageName=rmcampos/tasknote-api:latest \
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:0.0.505
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: rmcampos/tasknote-api
tags: |
type=raw,value=${{ steps.version.outputs.tag }}
type=raw,value=latest,enable={{ is_default_branch }}
- name: Tag and push Docker image
run: |
docker tag docker.io/rmcampos/tasknote-api:latest docker.io/rmcampos/tasknote-api:${{ steps.version.outputs.tag }}
docker push docker.io/rmcampos/tasknote-api:latest
docker push docker.io/rmcampos/tasknote-api:${{ steps.version.outputs.tag }}
- name: Output Docker image URLs
run: |
echo ""
echo "=========================================="
echo "🚀 Docker Images Published Successfully!"
echo "=========================================="
echo ""
echo "API Image:"
echo " rmcampos/tasknote-api:${{ steps.version.outputs.tag }}"
echo " rmcampos/tasknote-api:latest"
echo ""
echo "=========================================="
- name: Create and push Git tag
run: |
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@noreply.lightroasted.vps-kinghost.net"
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
git push origin ${{ steps.version.outputs.tag }}
+96
View File
@@ -0,0 +1,96 @@
name: Frontend CD
on:
workflow_dispatch:
push:
branches: [ main ]
paths:
- 'client/**'
- '.github/workflows/frontend-cd.yml'
jobs:
build:
runs-on: easynode-debian
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/client/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
run: npm install
working-directory: ./client
- name: Run build
run: npm run build
working-directory: ./client
- name: Generate version tag
id: version
run: |
DATE=$(date +'%Y.%m.%d')
TAG="app-v${DATE}.${{ github.run_number }}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Generated tag: ${TAG}"
- name: Set up Docker Buildx
run: docker buildx inspect --bootstrap
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: rmcampos/tasknote-app
tags: |
type=raw,value=${{ steps.version.outputs.tag }}
type=raw,value=latest,enable={{ is_default_branch }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./client
push: true
tags: |
${{ steps.meta.outputs.tags }}
rmcampos/tasknote-app:${{ steps.version.outputs.tag }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=rmcampos/tasknote-app:buildcache
cache-to: type=registry,ref=rmcampos/tasknote-app:buildcache,mode=max
build-args: |
VITE_BUILD=${{ steps.version.outputs.tag }}
- name: Output Docker image URLs
run: |
echo ""
echo "=========================================="
echo "🚀 Docker Images Published Successfully!"
echo "=========================================="
echo ""
echo "App Image:"
echo " rmcampos/tasknote-app:${{ steps.version.outputs.tag }}"
echo " rmcampos/tasknote-app:latest"
echo ""
echo "=========================================="
- name: Create and push Git tag
run: |
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@noreply.lightroasted.vps-kinghost.net"
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
git push origin ${{ steps.version.outputs.tag }}
+74
View File
@@ -0,0 +1,74 @@
name: CI
on:
pull_request:
branches: ['**/*']
jobs:
build-backend:
name: Build Backend
runs-on: graalvm-25
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/server/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Run Check Style
working-directory: ./server
run: ./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
- name: Run build
working-directory: ./server
run: ./mvnw --no-transfer-progress clean compile -DskipTests
- name: Run tests
working-directory: ./server
run: ./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
build-frontend:
name: Build Frontend
runs-on: easynode-debian
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/client/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
run: npm install
working-directory: ./client
- name: Run lint
run: npm run lint
working-directory: ./client
- name: Run build
run: npm run build
working-directory: ./client
- name: Run tests
run: npm run test:no-watch
working-directory: ./client
@@ -1,4 +1,8 @@
name: Main CD-Deploy to Prod
name: Deploy to Prod
concurrency:
group: deploy-production
cancel-in-progress: true
on:
workflow_dispatch:
@@ -14,7 +18,7 @@ on:
required: false
default: "true"
workflow_run:
workflows: [ "Main CI-Backend", "Main CI-Frontend" ]
workflows: [ "Backend CD", "Frontend CD" ]
types: [ completed ]
jobs:
@@ -37,10 +41,15 @@ jobs:
- name: Setup kubectl
uses: azure/setup-kubectl@v4
- name: Setup Doppler CLI
uses: dopplerhq/cli-action@v4
- name: Setup Kubeconfig
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
doppler run --config prd -- bash -c 'echo "$KUBECONFIG_DATA" | base64 -d > ~/.kube/config'
chmod 600 ~/.kube/config
- name: Validate cluster access
@@ -54,25 +63,17 @@ jobs:
backend_image="${{ github.event.inputs.backend_image }}"
frontend_image="${{ github.event.inputs.frontend_image }}"
latest_backend_tag_tmp="$(git tag --list 'api-v*' | sort -V | tail -n1)"
latest_backend_tag="${latest_backend_tag_tmp#api-v}"
latest_backend_tag="$(git tag --list 'api-v*' | sort -V | tail -n1)"
echo "latest backend tag=$latest_backend_tag"
latest_frontend_tag="$(git tag --list 'app-v*' | sort -V | tail -n1)"
if [ -z "$latest_backend_tag" ]; then
echo "No backend tag found matching [0-9]*" >&2
exit 1
fi
if [ -z "$latest_frontend_tag" ]; then
echo "No frontend tag found matching app-v*" >&2
exit 1
fi
echo "latest frontend tag=$latest_frontend_tag"
if [ -z "$backend_image" ]; then
backend_image="rmcampos/tasknote-api:$latest_backend_tag"
backend_image="docker.io/rmcampos/polpa-gestao-api:$latest_backend_tag"
fi
if [ -z "$frontend_image" ]; then
frontend_image="rmcampos/tasknote-app:$latest_frontend_tag"
frontend_image="docker.io/rmcampos/polpa-gestao-app:$latest_frontend_tag"
fi
echo "Resolved backend_image=$backend_image"
@@ -88,9 +89,8 @@ jobs:
- name: Terraform Init
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: terraform init -input=false
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
run: doppler run --config prd -- terraform init -input=false
- name: Terraform Validate
working-directory: terraform
@@ -100,19 +100,22 @@ jobs:
id: check-changes
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
BACKEND_IMAGE: ${{ steps.deploy-vars.outputs.backend_image }}
FRONTEND_IMAGE: ${{ steps.deploy-vars.outputs.frontend_image }}
run: |
timeout 1m terraform plan -input=false -out=tfplan \
-var="db_user=${{ secrets.DB_USER }}" \
-var="db_password=${{ secrets.DB_PASSWORD }}" \
-var="db_name=${{ secrets.DB_NAME }}" \
-var="security_key=${{ secrets.JWT_SECURITY_KEY }}" \
-var="mailgun_apikey=${{ secrets.MAILGUN_API_KEY }}" \
-var="r2_access_key=${{ secrets.R2_ACCESS_KEY_ID }}" \
-var="r2_secret_key=${{ secrets.R2_SECRET_ACCESS_KEY }}" \
-var="backend_image=${{ steps.deploy-vars.outputs.backend_image }}" \
-var="frontend_image=${{ steps.deploy-vars.outputs.frontend_image }}"
doppler run --config prd -- bash -c '
TF_VAR_db_user="$DB_USER" \
TF_VAR_db_password="$DB_PASSWORD" \
TF_VAR_db_name="$DB_NAME" \
TF_VAR_security_key="$SECURITY_KEY" \
TF_VAR_mailgun_apikey="$MAILGUN_APIKEY" \
TF_VAR_r2_access_key="$AWS_ACCESS_KEY_ID" \
TF_VAR_r2_secret_key="$AWS_SECRET_ACCESS_KEY" \
TF_VAR_backend_image="$BACKEND_IMAGE" \
TF_VAR_frontend_image="$FRONTEND_IMAGE" \
timeout 1m terraform plan -input=false -out=tfplan
'
terraform show -json tfplan > tfplan.json
if jq -e '.resource_changes | length == 0' tfplan.json >/dev/null; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
@@ -127,6 +130,5 @@ jobs:
working-directory: terraform
if: steps.check-changes.outputs.has_changes == 'true'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: timeout 1m terraform apply tfplan
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
run: doppler run --config prd -- timeout 2m terraform apply tfplan
@@ -2,9 +2,6 @@ name: Pull Request CD-Deploy to Staging
on:
workflow_dispatch:
workflow_run:
workflows: [ "Pull Request CI-Backend", "Pull Request CI-Frontend" ]
types: [ completed ]
jobs:
terraform-plan-stg:
@@ -2,14 +2,6 @@ name: Main CI-Backend
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'server/**/*.java'
- 'server/**/*.xml'
- 'server/pom.xml'
- '.github/workflows/ci-main-backend.yml'
jobs:
build-and-push:
@@ -2,20 +2,6 @@ name: Main CI-Frontend
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'client/**/*.html'
- 'client/**/*.png'
- 'client/**/*.json'
- 'client/**/*.txt'
- 'client/**/*.ts'
- 'client/**/*.tsx'
- 'client/**/*.js'
- 'client/Dockerfile'
- 'client/Caddyfile'
- '.github/workflows/ci-main-frontend.yml'
jobs:
build-and-push:
@@ -2,16 +2,6 @@ name: Pull Request CI-Backend
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
branches:
- 'main'
paths:
- 'server/**/*.java'
- 'server/**/*.xml'
- 'server/pom.xml'
- 'server/**/*.yml'
- '.github/workflows/ci-pr-backend.yml'
jobs:
run-checks:
@@ -2,21 +2,6 @@ name: Pull Request CI-Frontend
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
branches:
- 'main'
paths:
- 'client/**/*.html'
- 'client/**/*.png'
- 'client/**/*.json'
- 'client/**/*.txt'
- 'client/**/*.ts'
- 'client/**/*.tsx'
- 'client/**/*.js'
- 'client/Dockerfile'
- 'client/Caddyfile'
- '.github/workflows/ci-pr-frontend.yml'
jobs:
run-checks:
+4 -4
View File
@@ -25,10 +25,10 @@
- Frontend local dev: run from `client/` with `npm start`; backend local dev: run from `server/` with `./mvnw spring-boot:run`.
## CI/CD and release behavior
- PR workflows (`.github/workflows/ci-pr-frontend.yml`, `.github/workflows/ci-pr-backend.yml`) run checks then push `:candidate` and `:pr-<N>` images to GHCR.
- Main workflows (`.github/workflows/ci-main-frontend.yml`, `.github/workflows/ci-main-backend.yml`) push versioned tags (`app-v<date>.<run>` / `api-v<pom-version>`) + `latest`; backend workflow also increments `server/pom.xml` version.
- Staging deploy workflow (`.github/workflows/cd-pr.yml`) triggers on completion of either PR CI workflow and applies Terraform in `terraform-stg/` using a plan→apply split.
- Production deploy workflow (`.github/workflows/cd-main.yml`) triggers on completion of either Main CI workflow and applies Terraform in `terraform/` using a plan→apply split; `apply` can be skipped if there are no Terraform changes.
- PR workflows (`.github/workflows/drop-ci-pr-frontend.yml`, `.github/workflows/drop-ci-pr-backend.yml`) run checks then push `:candidate` and `:pr-<N>` images to GHCR.
- Main workflows (`.github/workflows/drop-ci-main-frontend.yml`, `.github/workflows/drop-ci-main-backend.yml`) push versioned tags (`app-v<date>.<run>` / `api-v<pom-version>`) + `latest`; backend workflow also increments `server/pom.xml` version.
- Staging deploy workflow (`.github/workflows/drop-cd-pr.yml`) triggers on completion of either PR CI workflow and applies Terraform in `terraform-stg/` using a plan→apply split.
- Production deploy workflow (`.github/workflows/deploy.yml`) triggers on completion of either Main CI workflow and applies Terraform in `terraform/` using a plan→apply split; `apply` can be skipped if there are no Terraform changes.
- Infra wiring (secrets, services, ingress, image vars) is defined in `terraform/main.tf`; an alternative GCP target is under `terraform-gcp/`.
## Project conventions to preserve
+1 -1
View File
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Removed deployments to staging in PR pipelines. PR only runs CI now.
- Removed deployments to staging in PR pipelines. PR only runs CI now. Closes [#14](https://lightroasted.vps-kinghost.net/rmcampos/tasknote/issues/14)
### Removed
- Old files from project and moved scripts to `tools` folder.
-3
View File
@@ -2,9 +2,6 @@
version: '3'
vars:
GREETING: Hello, World!
tasks:
docker-build-web:
desc: Build the tasknote-web prod-ready docker image, tagging it as candidate
+3
View File
@@ -0,0 +1,3 @@
setup:
project: tasknote
config: prd
-22
View File
@@ -1,22 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/google" {
version = "5.45.2"
constraints = "~> 5.0"
hashes = [
"h1:k8taQAdfHrv2F/AiGV5BZBZfI+1uaq8g6O8dWzjx42c=",
"zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a",
"zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804",
"zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e",
"zh:3e555cf22a570a4bd247964671f421ed7517970cd9765ceb46f335edc2c6f392",
"zh:688bd5b05a75124da7ae6e885b2b92bd29f4261808b2b78bd5f51f525c1052ca",
"zh:6db3ef37a05010d82900bfffb3261c59a0c247e0692049cb3eb8c2ef16c9d7bf",
"zh:70316fde75f6a15d72749f66d994ccbdde5f5ed4311b6d06b99850f698c9bbf9",
"zh:84b8e583771a4f2bd514e519d98ed7fd28dce5efe0634e973170e1cfb5556fb4",
"zh:9d4b8ef0a9b6677935c604d94495042e68ff5489932cfd1ec41052e094a279d3",
"zh:a2089dd9bd825c107b148dd12d6b286f71aa37dfd4ca9c35157f2dcba7bc19d8",
"zh:f03d795c0fd9721e59839255ee7ba7414173017dc530b4ce566daf3802a0d6dd",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
-127
View File
@@ -1,127 +0,0 @@
resource "google_cloud_run_v2_service" "backend" {
name = "tasknote-api"
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
depends_on = [google_project_service.run]
template {
service_account = google_service_account.cloudrun_sa.email
vpc_access {
connector = google_vpc_access_connector.connector.id
egress = "PRIVATE_RANGES_ONLY"
}
containers {
image = var.backend_image
ports {
container_port = 8585
}
env {
name = "POSTGRES_DB"
value = var.db_name
}
env {
name = "POSTGRES_HOST"
value = google_sql_database_instance.instance.private_ip_address
}
env {
name = "POSTGRES_USER"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.db_user.secret_id
version = google_secret_manager_secret_version.db_user_version.version
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.db_password.secret_id
version = google_secret_manager_secret_version.db_password_version.version
}
}
}
env {
name = "POSTGRES_PORT"
value = "5432"
}
env {
name = "CORS_ALLOWED_ORIGINS"
value = var.cors_allowed_origins
}
env {
name = "SERVER_SERVLET_CONTEXT_PATH"
value = "/"
}
env {
name = "TARGET_ENV"
value = "production"
}
env {
name = "SECURITY_KEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.security_key.secret_id
version = google_secret_manager_secret_version.security_key_version.version
}
}
}
env {
name = "MAILGUN_APIKEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.mailgun_apikey.secret_id
version = google_secret_manager_secret_version.mailgun_apikey_version.version
}
}
}
}
}
}
resource "google_cloud_run_v2_service" "frontend" {
name = "tasknote-app"
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
depends_on = [google_project_service.run]
template {
service_account = google_service_account.cloudrun_sa.email
containers {
image = var.frontend_image
ports {
container_port = 5000
}
env {
name = "VITE_BACKEND_SERVER"
value = google_cloud_run_v2_service.backend.uri
}
}
}
}
# Allow unauthenticated access to both services
resource "google_cloud_run_service_iam_member" "backend_public" {
location = google_cloud_run_v2_service.backend.location
service = google_cloud_run_v2_service.backend.name
role = "roles/run.invoker"
member = "allUsers"
}
resource "google_cloud_run_service_iam_member" "frontend_public" {
location = google_cloud_run_v2_service.frontend.location
service = google_cloud_run_v2_service.frontend.name
role = "roles/run.invoker"
member = "allUsers"
}
# Grant Cloud SQL Client role to the service account
resource "google_project_iam_member" "cloudsql_client" {
project = var.project_id
role = "roles/cloudsql.client"
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
}
-32
View File
@@ -1,32 +0,0 @@
resource "google_sql_database_instance" "instance" {
name = "tasknote-db-instance"
region = var.region
database_version = "POSTGRES_15"
depends_on = [
google_service_networking_connection.private_vpc_connection,
google_project_service.sqladmin,
google_project_service.servicenetworking
]
settings {
tier = "db-f1-micro" # Smallest tier for dev/small app
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.vpc_network.id
}
}
deletion_protection = false # Set to true for production
}
resource "google_sql_database" "database" {
name = var.db_name
instance = google_sql_database_instance.instance.name
}
resource "google_sql_user" "users" {
name = var.db_user
instance = google_sql_database_instance.instance.name
password = var.db_password
}
-45
View File
@@ -1,45 +0,0 @@
#!/bin/bash
PROJECT_ID="replace-me"
VERSION="23"
SOURCE_IMG="ghcr.io/rmcampos/tasknote/api:$VERSION"
DESTINATION_IMG="us-central1-docker.pkg.dev/$PROJECT_ID/tasknote-repo/api:$VERSION"
GHCR_PAT="replace-me"
# Login
echo $GHCR_PAT | docker login ghcr.io -u user --password-stdin
echo "Logged in on ghcr.io"
gcloud auth configure-docker us-central1-docker.pkg.dev
docker login us-central1-docker.pkg.dev
echo "Logged in on GCP Container Registry"
crane auth login ghcr.io -u RMCampos -p $GHCR_PAT
echo "Logged crane in on ghcr.io"
# Check if image already exists in destination
echo "Checking if $DESTINATION_IMG already exists in destination registry..."
if crane manifest "$DESTINATION_IMG" > /dev/null 2>&1; then
echo "Image $DESTINATION_IMG already exists. Skipping push."
else
echo "Image not found in destination. Proceeding with push..."
docker pull $SOURCE_IMG
docker tag $SOURCE_IMG $DESTINATION_IMG
docker push $DESTINATION_IMG
echo "Pushed image to GCP Container Registry"
crane copy $SOURCE_IMG $DESTINATION_IMG
echo "Copied image to GCP Container Registry with crane"
fi
# Verification
echo "Verifying push..."
if crane manifest "$DESTINATION_IMG" > /dev/null 2>&1; then
echo "SUCCESS: Image $DESTINATION_IMG is properly pushed and available in the registry."
gcloud artifacts docker images list us-central1-docker.pkg.dev/project-9f22294e-b6f5-41de-83c/tasknote-repo | grep "api:$VERSION"
else
echo "FAILURE: Image $DESTINATION_IMG was not found in the destination registry."
exit 1
fi
-35
View File
@@ -1,35 +0,0 @@
resource "google_compute_network" "vpc_network" {
name = "tasknote-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "subnet" {
name = "tasknote-subnet"
ip_cidr_range = "10.0.0.0/24"
region = var.region
network = google_compute_network.vpc_network.id
}
# Private IP for Cloud SQL
resource "google_compute_global_address" "private_ip_address" {
name = "tasknote-private-ip"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = google_compute_network.vpc_network.id
}
resource "google_service_networking_connection" "private_vpc_connection" {
network = google_compute_network.vpc_network.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
}
# Serverless VPC Access Connector
resource "google_vpc_access_connector" "connector" {
name = "tasknote-vpc-connector"
region = var.region
ip_cidr_range = "10.8.0.0/28"
network = google_compute_network.vpc_network.name
depends_on = [google_project_service.vpcaccess, google_project_service.compute]
}
-15
View File
@@ -1,15 +0,0 @@
output "backend_url" {
value = google_cloud_run_v2_service.backend.uri
}
output "frontend_url" {
value = google_cloud_run_v2_service.frontend.uri
}
output "cloud_sql_instance_ip" {
value = google_sql_database_instance.instance.private_ip_address
}
output "artifact_registry_repo" {
value = google_artifact_registry_repository.repo.name
}
-25
View File
@@ -1,25 +0,0 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "tasknote"
key = "gcp/terraform.tfstate"
region = "auto"
endpoints = { s3 = "https://d17eb09b6bce2f90e16e800bb2a6baf9.r2.cloudflarestorage.com" }
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
}
}
provider "google" {
project = var.project_id
region = var.region
}
-7
View File
@@ -1,7 +0,0 @@
resource "google_artifact_registry_repository" "repo" {
location = var.region
repository_id = "tasknote-repo"
description = "Docker repository for TaskNote images"
format = "DOCKER"
depends_on = [google_project_service.artifactregistry]
}
-95
View File
@@ -1,95 +0,0 @@
resource "google_service_account" "cloudrun_sa" {
account_id = "tasknote-cloudrun-sa"
display_name = "TaskNote Cloud Run Service Account"
}
resource "google_secret_manager_secret" "db_password" {
secret_id = "db-password"
replication {
user_managed {
replicas {
location = var.region
}
}
}
depends_on = [google_project_service.secretmanager]
}
resource "google_secret_manager_secret_version" "db_password_version" {
secret = google_secret_manager_secret.db_password.id
secret_data = var.db_password
}
resource "google_secret_manager_secret" "db_user" {
secret_id = "db-user"
replication {
user_managed {
replicas {
location = var.region
}
}
}
depends_on = [google_project_service.secretmanager]
}
resource "google_secret_manager_secret_version" "db_user_version" {
secret = google_secret_manager_secret.db_user.id
secret_data = var.db_user
}
resource "google_secret_manager_secret_iam_member" "db_password_access" {
secret_id = google_secret_manager_secret.db_password.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
}
resource "google_secret_manager_secret_iam_member" "db_user_access" {
secret_id = google_secret_manager_secret.db_user.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
}
resource "google_secret_manager_secret" "security_key" {
secret_id = "security-key"
replication {
user_managed {
replicas {
location = var.region
}
}
}
}
resource "google_secret_manager_secret_version" "security_key_version" {
secret = google_secret_manager_secret.security_key.id
secret_data = var.security_key
}
resource "google_secret_manager_secret" "mailgun_apikey" {
secret_id = "mailgun-apikey"
replication {
user_managed {
replicas {
location = var.region
}
}
}
depends_on = [google_project_service.secretmanager]
}
resource "google_secret_manager_secret_version" "mailgun_apikey_version" {
secret = google_secret_manager_secret.mailgun_apikey.id
secret_data = var.mailgun_apikey
}
resource "google_secret_manager_secret_iam_member" "security_key_access" {
secret_id = google_secret_manager_secret.security_key.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
}
resource "google_secret_manager_secret_iam_member" "mailgun_apikey_access" {
secret_id = google_secret_manager_secret.mailgun_apikey.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
}
-34
View File
@@ -1,34 +0,0 @@
resource "google_project_service" "compute" {
service = "compute.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "sqladmin" {
service = "sqladmin.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "run" {
service = "run.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "secretmanager" {
service = "secretmanager.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "vpcaccess" {
service = "vpcaccess.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "artifactregistry" {
service = "artifactregistry.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "servicenetworking" {
service = "servicenetworking.googleapis.com"
disable_on_destroy = false
}
-10
View File
@@ -1,10 +0,0 @@
project_id = "your-gcp-project-id"
region = "us-central1"
db_user = "tasknoteuser"
db_password = "your-db-password"
db_name = "tasknote"
security_key = "your-security-key"
mailgun_apikey = "your-mailgun-apikey"
backend_image = "ghcr.io/rmcampos/tasknote/api:latest"
frontend_image = "ghcr.io/rmcampos/tasknote/app:latest"
cors_allowed_origins = "*"
-50
View File
@@ -1,50 +0,0 @@
variable "project_id" {
type = string
description = "The GCP Project ID"
}
variable "region" {
type = string
default = "us-central1"
description = "The GCP region to deploy resources"
}
variable "db_user" {
type = string
sensitive = true
}
variable "db_password" {
type = string
sensitive = true
}
variable "db_name" {
type = string
default = "tasknote"
}
variable "security_key" {
type = string
sensitive = true
}
variable "mailgun_apikey" {
type = string
sensitive = true
}
variable "backend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/api:latest"
}
variable "frontend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/app:latest"
}
variable "cors_allowed_origins" {
type = string
default = "*"
}
-22
View File
@@ -1,22 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/kubernetes" {
version = "2.38.0"
constraints = "2.38.0"
hashes = [
"h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=",
"zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0",
"zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f",
"zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b",
"zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12",
"zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2",
"zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc",
"zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15",
"zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396",
"zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d",
"zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
"zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db",
]
}
-387
View File
@@ -1,387 +0,0 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "= 2.38.0"
}
}
backend "s3" {
bucket = "tasknote-stg"
key = "kubernetes/terraform.tfstate"
region = "auto"
endpoints = { s3 = "https://d17eb09b6bce2f90e16e800bb2a6baf9.r2.cloudflarestorage.com" }
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
}
variable "db_user" {
type = string
sensitive = true
}
variable "db_password" {
type = string
sensitive = true
}
variable "db_name" {
type = string
sensitive = true
}
variable "security_key" {
type = string
sensitive = true
}
variable "mailgun_apikey" {
type = string
sensitive = true
}
variable "cors_allowed_origins" {
type = string
default = "https://tasknote-stg.darkroasted.vps-kinghost.net"
}
variable "root_log_level" {
type = string
default = "INFO"
}
variable "backend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/api:candidate"
}
variable "frontend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/app:candidate"
}
variable "deploy_version" {
type = string
default = "manual"
}
resource "kubernetes_namespace_v1" "tasknote_stg" {
metadata {
name = "tasknote-stg"
}
}
resource "kubernetes_secret_v1" "tasknote_stg_secrets" {
metadata {
name = "tasknote-stg-secrets"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
data = {
postgres_user = var.db_user
postgres_password = var.db_password
postgres_db = var.db_name
security_key = var.security_key
mailgun_apikey = var.mailgun_apikey
}
}
resource "kubernetes_persistent_volume_claim_v1" "tasknote_stg_db_data" {
metadata {
name = "postgres-data-pvc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "1Gi"
}
}
}
}
resource "kubernetes_deployment_v1" "tasknote_stg_db" {
metadata {
name = "tasknote-stg-db"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-stg-db" } }
template {
metadata { labels = { app = "tasknote-stg-db" } }
spec {
container {
image = "postgres:15.8-bookworm"
name = "postgres"
volume_mount {
name = "postgres-storage"
mount_path = "/var/lib/postgresql/data"
}
env {
name = "POSTGRES_USER"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_user"
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_password"
}
}
}
env {
name = "POSTGRES_DB"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_db"
}
}
}
port { container_port = 5432 }
}
volume {
name = "postgres-storage"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.tasknote_stg_db_data.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_stg_db_svc" {
metadata {
name = "tasknote-stg-db-svc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
selector = { app = "tasknote-stg-db" }
port { port = 5432 }
type = "ClusterIP"
}
}
resource "kubernetes_deployment_v1" "tasknote_stg_backend" {
metadata {
name = "tasknote-stg-backend"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-stg-backend" } }
template {
metadata {
labels = { app = "tasknote-stg-backend" }
annotations = {
"deploy_id" = var.deploy_version
}
}
spec {
container {
image = var.backend_image
name = "backend"
image_pull_policy = "Always"
env {
name = "POSTGRES_DB"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_db"
}
}
}
env {
name = "POSTGRES_HOST"
value = "tasknote-stg-db-svc"
}
env {
name = "POSTGRES_USER"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_user"
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_password"
}
}
}
env {
name = "POSTGRES_PORT"
value = "5432"
}
env {
name = "CORS_ALLOWED_ORIGINS"
value = var.cors_allowed_origins
}
env {
name = "SERVER_SERVLET_CONTEXT_PATH"
value = "/"
}
env {
name = "ROOT_LOG_LEVEL"
value = var.root_log_level
}
env {
name = "SECURITY_KEY"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "security_key"
}
}
}
env {
name = "TARGET_ENV"
value = "staging"
}
env {
name = "MAILGUN_APIKEY"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "mailgun_apikey"
}
}
}
resources {
limits = { memory = "256Mi", cpu = "500m" }
requests = { memory = "256Mi", cpu = "250m" }
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_stg_backend_svc" {
metadata {
name = "tasknote-stg-backend-svc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
selector = { app = "tasknote-stg-backend" }
port {
port = 8585
target_port = 8585
}
}
}
resource "kubernetes_deployment_v1" "tasknote_stg_frontend" {
metadata {
name = "tasknote-stg-frontend"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-stg-app" } }
template {
metadata {
labels = { app = "tasknote-stg-app" }
annotations = {
"deploy_id" = var.deploy_version
}
}
spec {
container {
image = var.frontend_image
name = "frontend"
image_pull_policy = "Always"
port { container_port = 5000 }
env {
name = "VITE_BACKEND_SERVER"
value = "https://tasknoteapi-stg.darkroasted.vps-kinghost.net"
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_stg_frontend_svc" {
metadata {
name = "tasknote-stg-frontend-svc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
selector = { app = "tasknote-stg-app" }
port {
port = 5000
target_port = 5000
}
type = "ClusterIP"
}
}
# Unified Ingress for App and API
resource "kubernetes_ingress_v1" "tasknote_stg_ingress" {
metadata {
name = "tasknote-stg-ingress"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "traefik"
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
}
}
spec {
tls {
hosts = ["tasknote-stg.darkroasted.vps-kinghost.net", "tasknoteapi-stg.darkroasted.vps-kinghost.net"]
secret_name = "tasknote-stg-tls-certs"
}
rule {
host = "tasknote-stg.darkroasted.vps-kinghost.net"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.tasknote_stg_frontend_svc.metadata[0].name
port { number = 5000 }
}
}
}
}
}
rule {
host = "tasknoteapi-stg.darkroasted.vps-kinghost.net"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.tasknote_stg_backend_svc.metadata[0].name
port { number = 8585 }
}
}
}
}
}
}
}