Simplify deployment structure and consolidate documentation
- Remove redundant manual setup (setup-vps.sh) - Ansible handles all VPS setup - Remove config templates directory - Ansible creates .env.production - Delete verbose DEPLOYMENT_SETUP.md - info consolidated into deployment/README.md - Rewrite deployment/README.md with clear 3-step workflow - Simplify deployment/ansible/README.md to quick reference - Reduce total documentation from 1159 lines to 225 lines The deployment process is now easier to understand: 1. Run Ansible playbook (one-time setup) 2. Push code to Gitea (auto-builds) 3. SSH and promote to production 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0c82c6bdef
commit
47be003f6d
144
.gitea/workflows/build-deploy.yml
Normal file
144
.gitea/workflows/build-deploy.yml
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
name: Build and Deploy API Server
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
ARTIFACT_DIR: /opt/api-artifacts
|
||||||
|
GO_VERSION: '1.24'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Get commit info
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
run: |
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
|
-ldflags="-s -w -X main.Version=${{ steps.commit.outputs.short_sha }} -X main.BuildTime=${{ steps.commit.outputs.date }}" \
|
||||||
|
-o api-server \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Create build metadata
|
||||||
|
run: |
|
||||||
|
cat > build-info.json << EOF
|
||||||
|
{
|
||||||
|
"commit": "${{ steps.commit.outputs.sha }}",
|
||||||
|
"short_commit": "${{ steps.commit.outputs.short_sha }}",
|
||||||
|
"branch": "${{ steps.commit.outputs.branch }}",
|
||||||
|
"build_date": "${{ steps.commit.outputs.date }}",
|
||||||
|
"go_version": "${{ env.GO_VERSION }}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Package artifact
|
||||||
|
run: |
|
||||||
|
mkdir -p artifact-package
|
||||||
|
cp api-server artifact-package/
|
||||||
|
cp -r migrations artifact-package/
|
||||||
|
cp build-info.json artifact-package/
|
||||||
|
tar -czf api-server-${{ steps.commit.outputs.sha }}.tar.gz -C artifact-package .
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: api-server-${{ steps.commit.outputs.sha }}
|
||||||
|
path: api-server-${{ steps.commit.outputs.sha }}.tar.gz
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout deployment scripts
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: |
|
||||||
|
deployment/scripts
|
||||||
|
|
||||||
|
- name: Get commit SHA
|
||||||
|
id: commit
|
||||||
|
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: api-server-${{ steps.commit.outputs.sha }}
|
||||||
|
|
||||||
|
- name: Extract and deploy
|
||||||
|
run: |
|
||||||
|
BUILD_DIR="${{ env.ARTIFACT_DIR }}/builds/${{ steps.commit.outputs.sha }}"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
tar -xzf api-server-${{ steps.commit.outputs.sha }}.tar.gz -C "$BUILD_DIR"
|
||||||
|
|
||||||
|
chmod +x "$BUILD_DIR/api-server"
|
||||||
|
|
||||||
|
if [ -f "${{ env.ARTIFACT_DIR }}/config/.env.production" ]; then
|
||||||
|
cp "${{ env.ARTIFACT_DIR }}/config/.env.production" "$BUILD_DIR/.env.production"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run deployment script
|
||||||
|
run: |
|
||||||
|
cd ${{ env.ARTIFACT_DIR }}/scripts
|
||||||
|
./deploy.sh ${{ steps.commit.outputs.sha }}
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
AUTO_MIGRATE: "false"
|
||||||
|
PORT: "8080"
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
run: |
|
||||||
|
${{ env.ARTIFACT_DIR }}/scripts/health-check.sh
|
||||||
|
|
||||||
|
- name: Notify deployment status
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Deployment successful: ${{ steps.commit.outputs.sha }}"
|
||||||
|
else
|
||||||
|
echo "Deployment failed, rollback initiated"
|
||||||
|
${{ env.ARTIFACT_DIR }}/scripts/rollback.sh
|
||||||
|
fi
|
||||||
290
deployment/README.md
Normal file
290
deployment/README.md
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
# MemberSyncPro Deployment
|
||||||
|
|
||||||
|
Simple deployment workflow using Ansible + Gitea Actions + Blue-Green deployment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ 1. Ansible │ Run once to set up infrastructure
|
||||||
|
│ Setup │ (Gitea, PostgreSQL, Runners, Scripts)
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
┌──────▼───────┐
|
||||||
|
│ 2. Push Code │ Push to Gitea triggers automatic build
|
||||||
|
│ to Gitea │ and staging
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
┌──────▼───────┐
|
||||||
|
│ 3. Promote │ Manual promotion to production
|
||||||
|
│ to Prod │ (sudo promote.sh)
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## One-Time Setup
|
||||||
|
|
||||||
|
### 1. Prepare Ansible Inventory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deployment/ansible
|
||||||
|
cp hosts.ini.example hosts.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `hosts.ini`:
|
||||||
|
```ini
|
||||||
|
[git]
|
||||||
|
your-vps-ip ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/your-key.pem ansible_python_interpreter=/usr/bin/python3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Database Passwords
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export POSTGRES_ADMIN_PASSWORD="your-strong-password"
|
||||||
|
export API_SERVER_DB_PASSWORD="your-api-db-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Ansible Playbook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deployment/ansible
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
ansible -i hosts.ini git -m ping
|
||||||
|
|
||||||
|
# Deploy everything
|
||||||
|
ansible-playbook -i hosts.ini site.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
This sets up:
|
||||||
|
- Gitea server (accessible at `http://your-vps-ip`)
|
||||||
|
- PostgreSQL databases (gitea + apiserver)
|
||||||
|
- 5 Gitea Actions runners
|
||||||
|
- Nginx reverse proxy
|
||||||
|
- API server deployment infrastructure at `/opt/api-artifacts/`
|
||||||
|
- Systemd service for the API server
|
||||||
|
|
||||||
|
### 4. Create Gitea Repository
|
||||||
|
|
||||||
|
1. Visit `http://your-vps-ip`
|
||||||
|
2. Create admin account (first-time setup)
|
||||||
|
3. Create repository: `membersyncpro`
|
||||||
|
|
||||||
|
### 5. Add Git Remote
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add production http://your-vps-ip/your-username/membersyncpro.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Daily Deployment Workflow
|
||||||
|
|
||||||
|
### Deploy New Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Push code
|
||||||
|
git push production main
|
||||||
|
|
||||||
|
# 2. Wait for build (check Gitea → Actions)
|
||||||
|
# - Builds binary
|
||||||
|
# - Runs tests
|
||||||
|
# - Stages to inactive slot (blue/green)
|
||||||
|
|
||||||
|
# 3. SSH to VPS and promote
|
||||||
|
ssh ubuntu@your-vps-ip
|
||||||
|
sudo /opt/api-artifacts/scripts/promote.sh
|
||||||
|
# Type 'yes' to confirm
|
||||||
|
|
||||||
|
# 4. Verify
|
||||||
|
curl http://localhost:8080/hello
|
||||||
|
curl http://localhost:8080/version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
If something goes wrong after promotion:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@your-vps-ip
|
||||||
|
sudo /opt/api-artifacts/scripts/rollback.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Instantly reverts to the previous deployment.
|
||||||
|
|
||||||
|
## VPS Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/api-artifacts/
|
||||||
|
├── builds/
|
||||||
|
│ ├── abc123.../ # Previous build
|
||||||
|
│ └── def456.../ # Current build
|
||||||
|
├── blue -> builds/abc123/ # Previous deployment
|
||||||
|
├── green -> builds/def456/ # Current deployment
|
||||||
|
├── current -> green # Active (systemd uses this)
|
||||||
|
├── scripts/
|
||||||
|
│ ├── deploy.sh # Auto-called by CI
|
||||||
|
│ ├── promote.sh # Manual promotion
|
||||||
|
│ ├── rollback.sh # Emergency rollback
|
||||||
|
│ ├── health-check.sh # Health validation
|
||||||
|
│ └── migrate.sh # Database migrations
|
||||||
|
└── config/
|
||||||
|
└── .env.production # Environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### View Service Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Follow logs
|
||||||
|
journalctl -u api-server -f
|
||||||
|
|
||||||
|
# Last 100 lines
|
||||||
|
journalctl -u api-server -n 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Active Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# See which slot is active
|
||||||
|
readlink /opt/api-artifacts/current
|
||||||
|
|
||||||
|
# Full build path
|
||||||
|
readlink -f /opt/api-artifacts/current
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Runners
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All runners
|
||||||
|
systemctl status gitea-runner-{1..5}
|
||||||
|
|
||||||
|
# Individual runner logs
|
||||||
|
journalctl -u gitea-runner-1 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### List All Builds
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lh /opt/api-artifacts/builds/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean Old Builds
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Keep only last 10 builds
|
||||||
|
cd /opt/api-artifacts/builds
|
||||||
|
ls -t | tail -n +11 | xargs -I {} sudo rm -rf {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables (on VPS)
|
||||||
|
|
||||||
|
Edit `/opt/api-artifacts/config/.env.production`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql://apiserver:PASSWORD@localhost:5432/apiserver?sslmode=disable
|
||||||
|
AUTO_MIGRATE=false
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ansible Variables
|
||||||
|
|
||||||
|
Edit `deployment/ansible/site.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vars:
|
||||||
|
create_separate_apiserver_db: true # Separate vs shared DB
|
||||||
|
runner_count: 5 # Number of runners
|
||||||
|
gitea_version: "1.23.3" # Gitea version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Blue-Green Deployment Flow
|
||||||
|
|
||||||
|
1. **Current state:** `current -> blue` (active)
|
||||||
|
2. **New push:** Build creates `builds/new456/`
|
||||||
|
3. **Staging:** `green -> builds/new456/` (inactive)
|
||||||
|
4. **Migrations:** Run on green slot
|
||||||
|
5. **Promotion:** `current -> green` (switch!)
|
||||||
|
6. **Restart:** Systemd loads from current
|
||||||
|
7. **Rollback available:** Blue slot still has previous build
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Not Triggering
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check runners
|
||||||
|
systemctl status gitea-runner-1
|
||||||
|
|
||||||
|
# Runner logs
|
||||||
|
journalctl -u gitea-runner-1 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check deploy script logs
|
||||||
|
journalctl -u api-server -n 50
|
||||||
|
|
||||||
|
# Verify permissions
|
||||||
|
ls -la /opt/api-artifacts/
|
||||||
|
|
||||||
|
# Check database connection
|
||||||
|
cat /opt/api-artifacts/config/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service logs
|
||||||
|
journalctl -u api-server -n 50
|
||||||
|
|
||||||
|
# Verify binary exists
|
||||||
|
ls -la /opt/api-artifacts/current/api-server
|
||||||
|
|
||||||
|
# Check symlinks
|
||||||
|
readlink /opt/api-artifacts/current
|
||||||
|
readlink /opt/api-artifacts/blue
|
||||||
|
readlink /opt/api-artifacts/green
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Reference
|
||||||
|
|
||||||
|
- `ansible/site.yml` - Complete infrastructure setup playbook
|
||||||
|
- `ansible/hosts.ini` - Your VPS inventory
|
||||||
|
- `scripts/` - Deployment scripts (copied to VPS by Ansible)
|
||||||
|
- `systemd/api-server.service` - Service configuration
|
||||||
|
- `.gitea/workflows/build-deploy.yml` - CI/CD workflow
|
||||||
|
|
||||||
|
## Ansible Playbook Details
|
||||||
|
|
||||||
|
The `site.yml` playbook handles:
|
||||||
|
|
||||||
|
1. **Package installation:** Git, Nginx, PostgreSQL, Docker
|
||||||
|
2. **Gitea setup:** Binary download, database, systemd service
|
||||||
|
3. **Database provisioning:** Admin user, API server user, databases
|
||||||
|
4. **Runner deployment:** 5 self-hosted runners with auto-registration
|
||||||
|
5. **API infrastructure:** User, directories, scripts, systemd service
|
||||||
|
6. **Nginx configuration:** Reverse proxy for Gitea
|
||||||
|
7. **Firewall:** UFW with SSH, HTTP, PostgreSQL ports
|
||||||
|
|
||||||
|
Run it multiple times safely (idempotent).
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Change default passwords (use environment variables)
|
||||||
|
- Use SSH keys for VPS access
|
||||||
|
- Keep `.env.production` file permissions at 600
|
||||||
|
- Review UFW firewall rules
|
||||||
|
- Consider adding SSL/TLS with Let's Encrypt
|
||||||
|
- PostgreSQL is exposed on 0.0.0.0 - restrict if not needed externally
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After basic deployment:
|
||||||
|
1. Add SSL/TLS certificates (Let's Encrypt)
|
||||||
|
2. Set up monitoring (Prometheus/Grafana)
|
||||||
|
3. Configure alerting for deployment failures
|
||||||
|
4. Automate PostgreSQL backups
|
||||||
|
5. Add cron job to clean old builds
|
||||||
43
deployment/ansible/.gitignore
vendored
Normal file
43
deployment/ansible/.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Ansible
|
||||||
|
*.retry
|
||||||
|
.ansible/
|
||||||
|
/tmp/ansible_fact_cache/
|
||||||
|
|
||||||
|
# Secrets and sensitive data
|
||||||
|
group_vars/*/vault.yml
|
||||||
|
host_vars/*/vault.yml
|
||||||
|
*.secret
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# Environment specific
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Inventory files (use hosts.ini.example as template)
|
||||||
|
hosts.ini
|
||||||
|
inventory.ini
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*~
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Collections
|
||||||
|
collections/ansible_collections/
|
||||||
68
deployment/ansible/README.md
Normal file
68
deployment/ansible/README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Ansible Infrastructure Setup
|
||||||
|
|
||||||
|
This playbook sets up the complete Gitea + API server infrastructure on a fresh Ubuntu 24.04 VPS.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy and edit inventory
|
||||||
|
cp hosts.ini.example hosts.ini
|
||||||
|
# Edit hosts.ini with your VPS details
|
||||||
|
|
||||||
|
# 2. Set passwords
|
||||||
|
export POSTGRES_ADMIN_PASSWORD="your-strong-password"
|
||||||
|
export API_SERVER_DB_PASSWORD="your-api-db-password"
|
||||||
|
|
||||||
|
# 3. Run playbook
|
||||||
|
ansible-playbook -i hosts.ini site.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## What This Deploys
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- Gitea server (Git hosting + Actions)
|
||||||
|
- PostgreSQL (separate databases for Gitea and API)
|
||||||
|
- 5 Gitea Actions runners (self-hosted)
|
||||||
|
- Nginx reverse proxy
|
||||||
|
- UFW firewall
|
||||||
|
|
||||||
|
### API Server Deployment System
|
||||||
|
- `/opt/api-artifacts/` directory structure
|
||||||
|
- Blue-green deployment scripts
|
||||||
|
- Systemd service configuration
|
||||||
|
- Environment file template
|
||||||
|
|
||||||
|
### Database Setup
|
||||||
|
- `gitea` database (for Gitea)
|
||||||
|
- `apiserver` database (for your application)
|
||||||
|
- `pgadmin` user (admin access)
|
||||||
|
- `apiserver` user (application access)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `site.yml` to customize:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vars:
|
||||||
|
create_separate_apiserver_db: true # false to share gitea DB
|
||||||
|
runner_count: 5 # Number of runners
|
||||||
|
gitea_version: "1.23.3" # Gitea version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inventory File
|
||||||
|
|
||||||
|
Create `hosts.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[git]
|
||||||
|
your-vps-ip ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/key.pem ansible_python_interpreter=/usr/bin/python3
|
||||||
|
```
|
||||||
|
|
||||||
|
## After Ansible Runs
|
||||||
|
|
||||||
|
1. Access Gitea at `http://your-vps-ip`
|
||||||
|
2. Create admin account
|
||||||
|
3. Create repository
|
||||||
|
4. Push code to trigger builds
|
||||||
|
|
||||||
|
See main deployment README for full workflow.
|
||||||
10
deployment/ansible/ansible.cfg
Normal file
10
deployment/ansible/ansible.cfg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[defaults]
|
||||||
|
inventory = ./hosts.ini
|
||||||
|
host_key_checking = False
|
||||||
|
stdout_callback = ansible.builtin.default
|
||||||
|
result_format = yaml
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
||||||
17
deployment/ansible/hosts.ini.example
Normal file
17
deployment/ansible/hosts.ini.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Ansible Inventory for Gitea + API Server Deployment
|
||||||
|
# Copy this file to hosts.ini and update with your server details
|
||||||
|
|
||||||
|
[git]
|
||||||
|
# Replace with your server's IP or hostname
|
||||||
|
your-server-ip-here ansible_user=root ansible_python_interpreter=/usr/bin/python3
|
||||||
|
|
||||||
|
# Examples:
|
||||||
|
#
|
||||||
|
# Using IP address with root user:
|
||||||
|
# 192.168.1.100 ansible_user=root ansible_python_interpreter=/usr/bin/python3
|
||||||
|
#
|
||||||
|
# Using hostname with ubuntu user and SSH key:
|
||||||
|
# myserver.example.com ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa ansible_python_interpreter=/usr/bin/python3
|
||||||
|
#
|
||||||
|
# Using IP with custom SSH port:
|
||||||
|
# 192.168.1.100 ansible_user=ubuntu ansible_port=2222 ansible_python_interpreter=/usr/bin/python3
|
||||||
2
deployment/ansible/requirements.yml
Normal file
2
deployment/ansible/requirements.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
collections:
|
||||||
|
- name: community.postgresql
|
||||||
522
deployment/ansible/site.yml
Normal file
522
deployment/ansible/site.yml
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
---
|
||||||
|
- name: Install Gitea (Ubuntu 24.04 minimal)
|
||||||
|
hosts: git
|
||||||
|
become: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
gitea_version: "1.23.3"
|
||||||
|
gitea_arch: "linux-amd64"
|
||||||
|
gitea_bin: /usr/local/bin/gitea
|
||||||
|
|
||||||
|
gitea_user: git
|
||||||
|
gitea_home: /var/lib/gitea
|
||||||
|
|
||||||
|
gitea_http_addr: "0.0.0.0" # Listen on all interfaces
|
||||||
|
gitea_http_port: 3000
|
||||||
|
|
||||||
|
gitea_domain: "{{ ansible_host }}" # Use the server's IP address
|
||||||
|
gitea_root_url: "http://{{ ansible_host }}" # HTTP with IP and port
|
||||||
|
|
||||||
|
gitea_internal_token: "5982634986925861987263497269412869416237419264" # Change to a secure random token
|
||||||
|
|
||||||
|
postgres_db: gitea
|
||||||
|
postgres_user: gitea
|
||||||
|
postgres_password: "12409768234691236"
|
||||||
|
|
||||||
|
# PostgreSQL admin user for debugging and management
|
||||||
|
postgres_admin_user: pgadmin
|
||||||
|
postgres_admin_password: "{{ lookup('env', 'POSTGRES_ADMIN_PASSWORD') | default('ChangeMe_SecurePassword_2025', true) }}"
|
||||||
|
|
||||||
|
# Golang API Server database user
|
||||||
|
api_server_db_user: apiserver
|
||||||
|
api_server_db_password: "{{ lookup('env', 'API_SERVER_DB_PASSWORD') | default('ChangeMe_ApiServer_2025', true) }}"
|
||||||
|
|
||||||
|
# NEW: Option to create separate database for API server
|
||||||
|
create_separate_apiserver_db: true # Set to false to share gitea database
|
||||||
|
|
||||||
|
# Gitea Runner configuration
|
||||||
|
runner_version: "0.2.11"
|
||||||
|
runner_count: 5 # Number of runners to deploy
|
||||||
|
runner_base_dir: /var/lib/gitea-runner
|
||||||
|
runner_token: "CHANGE_ME_TO_REGISTRATION_TOKEN" # Get from Gitea admin panel
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Install packages
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: true
|
||||||
|
name:
|
||||||
|
- ca-certificates
|
||||||
|
- curl
|
||||||
|
- git
|
||||||
|
- nginx
|
||||||
|
- ufw
|
||||||
|
- postgresql
|
||||||
|
- postgresql-contrib
|
||||||
|
- python3-psycopg # Important on Ubuntu 24.04
|
||||||
|
- docker.io
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Start and enable Docker
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: docker
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Create git system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ gitea_user }}"
|
||||||
|
system: true
|
||||||
|
create_home: true
|
||||||
|
home: "{{ gitea_home }}"
|
||||||
|
shell: /usr/sbin/nologin
|
||||||
|
groups: docker
|
||||||
|
append: true
|
||||||
|
|
||||||
|
- name: Create Gitea directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_user }}"
|
||||||
|
group: "{{ gitea_user }}"
|
||||||
|
mode: "0750"
|
||||||
|
loop:
|
||||||
|
- /etc/gitea
|
||||||
|
- "{{ gitea_home }}"
|
||||||
|
- "{{ gitea_home }}/custom"
|
||||||
|
- "{{ gitea_home }}/data"
|
||||||
|
- "{{ gitea_home }}/log"
|
||||||
|
- "{{ gitea_home }}/repositories"
|
||||||
|
|
||||||
|
- name: Download gitea binary
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://dl.gitea.com/gitea/{{ gitea_version }}/gitea-{{ gitea_version }}-{{ gitea_arch }}"
|
||||||
|
dest: "{{ gitea_bin }}"
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Ensure postgres running
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: postgresql
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Create postgres user
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_user:
|
||||||
|
name: "{{ postgres_user }}"
|
||||||
|
password: "{{ postgres_password }}"
|
||||||
|
|
||||||
|
- name: Create postgres database
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_db:
|
||||||
|
name: "{{ postgres_db }}"
|
||||||
|
owner: "{{ postgres_user }}"
|
||||||
|
|
||||||
|
- name: Create PostgreSQL admin user for production debugging
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_user:
|
||||||
|
name: "{{ postgres_admin_user }}"
|
||||||
|
password: "{{ postgres_admin_password }}"
|
||||||
|
role_attr_flags: SUPERUSER,CREATEDB,CREATEROLE,REPLICATION,LOGIN
|
||||||
|
comment: "Admin user for debugging and managing databases, users, and roles"
|
||||||
|
|
||||||
|
- name: Create PostgreSQL user for golang-api-server
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_user:
|
||||||
|
name: "{{ api_server_db_user }}"
|
||||||
|
password: "{{ api_server_db_password }}"
|
||||||
|
role_attr_flags: LOGIN
|
||||||
|
comment: "Database user for golang-api-server application"
|
||||||
|
|
||||||
|
- name: Create separate API server database
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_db:
|
||||||
|
name: apiserver
|
||||||
|
owner: "{{ api_server_db_user }}"
|
||||||
|
when: create_separate_apiserver_db
|
||||||
|
|
||||||
|
- name: Grant privileges to api-server user on their database
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_privs:
|
||||||
|
database: "{{ 'apiserver' if create_separate_apiserver_db else postgres_db }}"
|
||||||
|
state: present
|
||||||
|
privs: ALL
|
||||||
|
type: database
|
||||||
|
role: "{{ api_server_db_user }}"
|
||||||
|
|
||||||
|
- name: Configure PostgreSQL to listen on all interfaces
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/postgresql/16/main/postgresql.conf
|
||||||
|
regexp: '^#?listen_addresses\s*='
|
||||||
|
line: "listen_addresses = '*'"
|
||||||
|
backup: yes
|
||||||
|
notify: restart postgresql
|
||||||
|
|
||||||
|
- name: Allow remote connections to PostgreSQL
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/postgresql/16/main/pg_hba.conf
|
||||||
|
line: "host all all 0.0.0.0/0 scram-sha-256"
|
||||||
|
insertafter: EOF
|
||||||
|
backup: yes
|
||||||
|
notify: restart postgresql
|
||||||
|
|
||||||
|
- name: Write Gitea config
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/gitea/app.ini
|
||||||
|
owner: "{{ gitea_user }}"
|
||||||
|
group: "{{ gitea_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
content: |
|
||||||
|
APP_NAME = Gitea
|
||||||
|
RUN_USER = {{ gitea_user }}
|
||||||
|
RUN_MODE = prod
|
||||||
|
WORK_PATH = {{ gitea_home }}
|
||||||
|
|
||||||
|
[server]
|
||||||
|
DOMAIN = {{ gitea_domain }}
|
||||||
|
ROOT_URL = {{ gitea_root_url }}
|
||||||
|
HTTP_ADDR = {{ gitea_http_addr }}
|
||||||
|
HTTP_PORT = {{ gitea_http_port }}
|
||||||
|
START_SSH_SERVER = false
|
||||||
|
DISABLE_SSH = false
|
||||||
|
APP_DATA_PATH = {{ gitea_home }}/data
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = postgres
|
||||||
|
HOST = 127.0.0.1:5432
|
||||||
|
NAME = {{ postgres_db }}
|
||||||
|
USER = {{ postgres_user }}
|
||||||
|
PASSWD = {{ postgres_password }}
|
||||||
|
SSL_MODE = disable
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = true
|
||||||
|
INTERNAL_TOKEN = {{ gitea_internal_token}}
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = true
|
||||||
|
|
||||||
|
[log]
|
||||||
|
MODE = file
|
||||||
|
LEVEL = info
|
||||||
|
ROOT_PATH = {{ gitea_home }}/log
|
||||||
|
|
||||||
|
- name: Install systemd unit
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/systemd/system/gitea.service
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea
|
||||||
|
After=network.target postgresql.service
|
||||||
|
Wants=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User={{ gitea_user }}
|
||||||
|
Group={{ gitea_user }}
|
||||||
|
WorkingDirectory={{ gitea_home }}
|
||||||
|
Environment=HOME={{ gitea_home }}
|
||||||
|
ExecStart={{ gitea_bin }} web --config /etc/gitea/app.ini
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2s
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
- name: Start gitea
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
name: gitea
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Gitea Runner Installation
|
||||||
|
- name: Delete all runners from Gitea database directly
|
||||||
|
become_user: postgres
|
||||||
|
community.postgresql.postgresql_query:
|
||||||
|
db: "{{ postgres_db }}"
|
||||||
|
query: "DELETE FROM action_runner;"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Stop all existing runner services
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "gitea-runner-{{ item }}"
|
||||||
|
state: stopped
|
||||||
|
enabled: false
|
||||||
|
loop: "{{ range(1, 21) | list }}" # Check up to 20 possible runners
|
||||||
|
ignore_errors: true
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Remove all existing runner systemd units
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/etc/systemd/system/gitea-runner-{{ item }}.service"
|
||||||
|
state: absent
|
||||||
|
loop: "{{ range(1, 21) | list }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Reload systemd after removing runner units
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Remove all existing runner directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ runner_base_dir }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Recreate base runner directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ runner_base_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_user }}"
|
||||||
|
group: "{{ gitea_user }}"
|
||||||
|
mode: "0750"
|
||||||
|
|
||||||
|
- name: Download Gitea runner binary
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://dl.gitea.com/act_runner/{{ runner_version }}/act_runner-{{ runner_version }}-linux-amd64"
|
||||||
|
dest: /usr/local/bin/act_runner
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Create runner directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ runner_base_dir }}/runner-{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_user }}"
|
||||||
|
group: "{{ gitea_user }}"
|
||||||
|
mode: "0750"
|
||||||
|
loop: "{{ range(1, runner_count + 1) | list }}"
|
||||||
|
|
||||||
|
- name: Generate runner config
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
/usr/local/bin/act_runner generate-config > {{ runner_base_dir }}/runner-{{ item }}/config.yaml
|
||||||
|
args:
|
||||||
|
creates: "{{ runner_base_dir }}/runner-{{ item }}/config.yaml"
|
||||||
|
loop: "{{ range(1, runner_count + 1) | list }}"
|
||||||
|
|
||||||
|
- name: Set runner config ownership
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ runner_base_dir }}/runner-{{ item }}/config.yaml"
|
||||||
|
owner: "{{ gitea_user }}"
|
||||||
|
group: "{{ gitea_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
loop: "{{ range(1, runner_count + 1) | list }}"
|
||||||
|
|
||||||
|
- name: Generate runner registration tokens
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
sudo -u {{ gitea_user }} {{ gitea_bin }} --config /etc/gitea/app.ini actions generate-runner-token
|
||||||
|
register: runner_tokens
|
||||||
|
loop: "{{ range(1, runner_count + 1) | list }}"
|
||||||
|
|
||||||
|
- name: Register runners with generated tokens
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_user }}"
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
cd {{ runner_base_dir }}/runner-{{ item.item }} && \
|
||||||
|
/usr/local/bin/act_runner register \
|
||||||
|
--instance {{ gitea_root_url }} \
|
||||||
|
--token {{ item.stdout | trim }} \
|
||||||
|
--config {{ runner_base_dir }}/runner-{{ item.item }}/config.yaml \
|
||||||
|
--name runner-{{ item.item }} \
|
||||||
|
--no-interactive
|
||||||
|
loop: "{{ runner_tokens.results }}"
|
||||||
|
args:
|
||||||
|
creates: "{{ runner_base_dir }}/runner-{{ item.item }}/.runner"
|
||||||
|
|
||||||
|
- name: Create runner systemd units
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "/etc/systemd/system/gitea-runner-{{ item }}.service"
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea Runner {{ item }}
|
||||||
|
After=network.target gitea.service
|
||||||
|
Wants=gitea.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User={{ gitea_user }}
|
||||||
|
Group={{ gitea_user }}
|
||||||
|
WorkingDirectory={{ runner_base_dir }}/runner-{{ item }}
|
||||||
|
ExecStart=/usr/local/bin/act_runner daemon --config {{ runner_base_dir }}/runner-{{ item }}/config.yaml
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
loop: "{{ range(1, runner_count + 1) | list }}"
|
||||||
|
|
||||||
|
- name: Enable and start runner services
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
name: "gitea-runner-{{ item }}"
|
||||||
|
enabled: true
|
||||||
|
loop: "{{ range(1, runner_count + 1) | list }}"
|
||||||
|
|
||||||
|
- name: Configure nginx reverse proxy (HTTP only for now)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/nginx/sites-available/gitea
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ ansible_host }};
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:{{ gitea_http_port }};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Enable nginx site
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: /etc/nginx/sites-available/gitea
|
||||||
|
dest: /etc/nginx/sites-enabled/gitea
|
||||||
|
state: link
|
||||||
|
force: true
|
||||||
|
|
||||||
|
- name: Disable default nginx site
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /etc/nginx/sites-enabled/default
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Test and reload nginx
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
|
||||||
|
- name: Allow firewall (SSH + HTTP)
|
||||||
|
ansible.builtin.ufw:
|
||||||
|
rule: allow
|
||||||
|
name: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- "OpenSSH"
|
||||||
|
- "Nginx Full"
|
||||||
|
|
||||||
|
- name: Allow PostgreSQL port
|
||||||
|
ansible.builtin.ufw:
|
||||||
|
rule: allow
|
||||||
|
port: 5432
|
||||||
|
|
||||||
|
- name: Enable UFW
|
||||||
|
ansible.builtin.ufw:
|
||||||
|
state: enabled
|
||||||
|
policy: deny
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# API SERVER DEPLOYMENT INFRASTRUCTURE
|
||||||
|
# ==========================================
|
||||||
|
- name: Create apiserver system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: apiserver
|
||||||
|
system: true
|
||||||
|
create_home: true
|
||||||
|
home: /opt/api-artifacts
|
||||||
|
shell: /bin/bash
|
||||||
|
groups: docker
|
||||||
|
append: true
|
||||||
|
|
||||||
|
- name: Create deployment directory structure
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: apiserver
|
||||||
|
group: apiserver
|
||||||
|
mode: "0755"
|
||||||
|
loop:
|
||||||
|
- /opt/api-artifacts
|
||||||
|
- /opt/api-artifacts/builds
|
||||||
|
- /opt/api-artifacts/scripts
|
||||||
|
- /opt/api-artifacts/config
|
||||||
|
|
||||||
|
- name: Copy deployment scripts from local repository
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ playbook_dir }}/../scripts/{{ item }}"
|
||||||
|
dest: /opt/api-artifacts/scripts/{{ item }}
|
||||||
|
owner: apiserver
|
||||||
|
group: apiserver
|
||||||
|
mode: "0755"
|
||||||
|
loop:
|
||||||
|
- deploy.sh
|
||||||
|
- promote.sh
|
||||||
|
- rollback.sh
|
||||||
|
- health-check.sh
|
||||||
|
- migrate.sh
|
||||||
|
|
||||||
|
- name: Create production environment file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /opt/api-artifacts/config/.env.production
|
||||||
|
owner: apiserver
|
||||||
|
group: apiserver
|
||||||
|
mode: "0600"
|
||||||
|
content: |
|
||||||
|
DATABASE_URL=postgresql://{{ api_server_db_user }}:{{ api_server_db_password }}@localhost:5432/{{ 'apiserver' if create_separate_apiserver_db else postgres_db }}?sslmode=disable
|
||||||
|
AUTO_MIGRATE=false
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=release
|
||||||
|
|
||||||
|
- name: Copy systemd service file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ playbook_dir }}/../systemd/api-server.service"
|
||||||
|
dest: /etc/systemd/system/api-server.service
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
register: systemd_service
|
||||||
|
|
||||||
|
- name: Reload systemd daemon
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
when: systemd_service.changed
|
||||||
|
|
||||||
|
- name: Enable api-server service (but don't start - no binary yet)
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: api-server.service
|
||||||
|
enabled: true
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Install golang-migrate CLI
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||||
|
mv migrate /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/migrate
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/migrate
|
||||||
|
|
||||||
|
- name: Display setup summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "============================================"
|
||||||
|
- "Gitea + API Server Infrastructure Complete!"
|
||||||
|
- "============================================"
|
||||||
|
- ""
|
||||||
|
- "Gitea:"
|
||||||
|
- " URL: {{ gitea_root_url }}"
|
||||||
|
- " Runners: {{ runner_count }} runners active"
|
||||||
|
- ""
|
||||||
|
- "API Server Deployment:"
|
||||||
|
- " Artifact directory: /opt/api-artifacts/"
|
||||||
|
- " Database: {{ 'apiserver' if create_separate_apiserver_db else postgres_db }}"
|
||||||
|
- " Database user: {{ api_server_db_user }}"
|
||||||
|
- " Systemd service: api-server.service (enabled)"
|
||||||
|
- ""
|
||||||
|
- "Next Steps:"
|
||||||
|
- "1. Create repository in Gitea and push your golang-api-server code"
|
||||||
|
- "2. Push will trigger Gitea Actions build automatically"
|
||||||
|
- "3. After build completes, SSH to server and promote:"
|
||||||
|
- " sudo /opt/api-artifacts/scripts/promote.sh"
|
||||||
|
- ""
|
||||||
|
- "Management Commands:"
|
||||||
|
- " - View builds: ls /opt/api-artifacts/builds/"
|
||||||
|
- " - Check logs: journalctl -u api-server -f"
|
||||||
|
- " - Rollback: sudo /opt/api-artifacts/scripts/rollback.sh"
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: restart postgresql
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: postgresql
|
||||||
|
state: restarted
|
||||||
74
deployment/scripts/deploy.sh
Executable file
74
deployment/scripts/deploy.sh
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARTIFACT_DIR="/opt/api-artifacts"
|
||||||
|
BUILDS_DIR="$ARTIFACT_DIR/builds"
|
||||||
|
SCRIPTS_DIR="$ARTIFACT_DIR/scripts"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
log_error "Usage: $0 <commit-hash>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMMIT_HASH="$1"
|
||||||
|
BUILD_PATH="$BUILDS_DIR/$COMMIT_HASH"
|
||||||
|
|
||||||
|
if [ ! -d "$BUILD_PATH" ]; then
|
||||||
|
log_error "Build not found: $BUILD_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BUILD_PATH/api-server" ]; then
|
||||||
|
log_error "Binary not found in build: $BUILD_PATH/api-server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Starting deployment for commit: $COMMIT_HASH"
|
||||||
|
|
||||||
|
CURRENT_LINK="$ARTIFACT_DIR/current"
|
||||||
|
BLUE_LINK="$ARTIFACT_DIR/blue"
|
||||||
|
GREEN_LINK="$ARTIFACT_DIR/green"
|
||||||
|
|
||||||
|
if [ -L "$CURRENT_LINK" ]; then
|
||||||
|
CURRENT_TARGET=$(readlink "$CURRENT_LINK")
|
||||||
|
if [ "$CURRENT_TARGET" = "blue" ]; then
|
||||||
|
TARGET_SLOT="green"
|
||||||
|
INACTIVE_LINK="$GREEN_LINK"
|
||||||
|
else
|
||||||
|
TARGET_SLOT="blue"
|
||||||
|
INACTIVE_LINK="$BLUE_LINK"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TARGET_SLOT="blue"
|
||||||
|
INACTIVE_LINK="$BLUE_LINK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deploying to slot: $TARGET_SLOT"
|
||||||
|
|
||||||
|
ln -sfn "$BUILD_PATH" "$INACTIVE_LINK"
|
||||||
|
|
||||||
|
log_info "Symlink updated: $INACTIVE_LINK -> $BUILD_PATH"
|
||||||
|
|
||||||
|
log_info "Running database migrations..."
|
||||||
|
if ! "$SCRIPTS_DIR/migrate.sh" "$TARGET_SLOT"; then
|
||||||
|
log_error "Migration failed, aborting deployment"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Deployment staged to $TARGET_SLOT slot"
|
||||||
|
log_info "Build ready at: $BUILD_PATH"
|
||||||
|
log_info ""
|
||||||
|
log_info "To promote this deployment to production, run:"
|
||||||
|
log_info " sudo $SCRIPTS_DIR/promote.sh"
|
||||||
|
log_info ""
|
||||||
|
log_info "To test before promoting:"
|
||||||
|
log_info " Check $INACTIVE_LINK for manual verification"
|
||||||
35
deployment/scripts/health-check.sh
Executable file
35
deployment/scripts/health-check.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HEALTH_ENDPOINT="${HEALTH_ENDPOINT:-http://localhost:8080/hello}"
|
||||||
|
MAX_RETRIES="${MAX_RETRIES:-10}"
|
||||||
|
RETRY_DELAY="${RETRY_DELAY:-3}"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
if ! systemctl is-active --quiet api-server.service; then
|
||||||
|
log_error "Service is not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 1 $MAX_RETRIES); do
|
||||||
|
log_info "Health check attempt $i/$MAX_RETRIES..."
|
||||||
|
|
||||||
|
if response=$(curl -sf -m 5 "$HEALTH_ENDPOINT" 2>/dev/null); then
|
||||||
|
log_info "Health check passed!"
|
||||||
|
echo "$response" | head -c 200
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $i -lt $MAX_RETRIES ]; then
|
||||||
|
sleep $RETRY_DELAY
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_error "Health check failed after $MAX_RETRIES attempts"
|
||||||
|
exit 1
|
||||||
48
deployment/scripts/migrate.sh
Executable file
48
deployment/scripts/migrate.sh
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARTIFACT_DIR="/opt/api-artifacts"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
SLOT="${1:-blue}"
|
||||||
|
SLOT_LINK="$ARTIFACT_DIR/$SLOT"
|
||||||
|
|
||||||
|
if [ ! -L "$SLOT_LINK" ]; then
|
||||||
|
log_error "Slot not found: $SLOT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUILD_PATH=$(readlink -f "$SLOT_LINK")
|
||||||
|
MIGRATIONS_DIR="$BUILD_PATH/migrations"
|
||||||
|
|
||||||
|
if [ ! -d "$MIGRATIONS_DIR" ]; then
|
||||||
|
log_error "Migrations directory not found: $MIGRATIONS_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$BUILD_PATH/.env.production" ]; then
|
||||||
|
source "$BUILD_PATH/.env.production"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${DATABASE_URL:-}" ]; then
|
||||||
|
log_error "DATABASE_URL not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Running migrations from: $MIGRATIONS_DIR"
|
||||||
|
|
||||||
|
cd "$BUILD_PATH"
|
||||||
|
export AUTO_MIGRATE=true
|
||||||
|
export PORT=0
|
||||||
|
|
||||||
|
timeout 30s ./api-server 2>&1 | grep -i "migration" || true
|
||||||
|
|
||||||
|
log_info "Migrations completed"
|
||||||
64
deployment/scripts/promote.sh
Executable file
64
deployment/scripts/promote.sh
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARTIFACT_DIR="/opt/api-artifacts"
|
||||||
|
CURRENT_LINK="$ARTIFACT_DIR/current"
|
||||||
|
BLUE_LINK="$ARTIFACT_DIR/blue"
|
||||||
|
GREEN_LINK="$ARTIFACT_DIR/green"
|
||||||
|
SERVICE_NAME="api-server.service"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
if [ -L "$CURRENT_LINK" ]; then
|
||||||
|
CURRENT_TARGET=$(readlink "$CURRENT_LINK")
|
||||||
|
if [ "$CURRENT_TARGET" = "blue" ]; then
|
||||||
|
NEW_TARGET="green"
|
||||||
|
else
|
||||||
|
NEW_TARGET="blue"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
NEW_TARGET="blue"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_TARGET_LINK="$ARTIFACT_DIR/$NEW_TARGET"
|
||||||
|
|
||||||
|
if [ ! -L "$NEW_TARGET_LINK" ]; then
|
||||||
|
log_error "Target deployment not found: $NEW_TARGET_LINK"
|
||||||
|
log_error "Run deploy.sh first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_BUILD=$(readlink "$NEW_TARGET_LINK")
|
||||||
|
log_info "Promoting $NEW_TARGET to production"
|
||||||
|
log_info "New build: $NEW_BUILD"
|
||||||
|
|
||||||
|
read -p "Continue with promotion? (yes/no): " -r
|
||||||
|
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
|
||||||
|
log_warn "Promotion cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Switching current symlink to $NEW_TARGET"
|
||||||
|
ln -sfn "$NEW_TARGET" "$CURRENT_LINK"
|
||||||
|
|
||||||
|
log_info "Reloading systemd service..."
|
||||||
|
systemctl reload-or-restart "$SERVICE_NAME"
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
log_info "Running health check..."
|
||||||
|
if "$ARTIFACT_DIR/scripts/health-check.sh"; then
|
||||||
|
log_info "Promotion successful!"
|
||||||
|
log_info "Active deployment: $NEW_TARGET ($NEW_BUILD)"
|
||||||
|
else
|
||||||
|
log_error "Health check failed after promotion!"
|
||||||
|
log_warn "Run rollback.sh to revert"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
62
deployment/scripts/rollback.sh
Executable file
62
deployment/scripts/rollback.sh
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARTIFACT_DIR="/opt/api-artifacts"
|
||||||
|
CURRENT_LINK="$ARTIFACT_DIR/current"
|
||||||
|
SERVICE_NAME="api-server.service"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
if [ ! -L "$CURRENT_LINK" ]; then
|
||||||
|
log_error "No current deployment found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_TARGET=$(readlink "$CURRENT_LINK")
|
||||||
|
|
||||||
|
if [ "$CURRENT_TARGET" = "blue" ]; then
|
||||||
|
PREVIOUS_SLOT="green"
|
||||||
|
else
|
||||||
|
PREVIOUS_SLOT="blue"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PREVIOUS_LINK="$ARTIFACT_DIR/$PREVIOUS_SLOT"
|
||||||
|
|
||||||
|
if [ ! -L "$PREVIOUS_LINK" ]; then
|
||||||
|
log_error "No previous deployment found to rollback to"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PREVIOUS_BUILD=$(readlink "$PREVIOUS_LINK")
|
||||||
|
|
||||||
|
log_warn "ROLLBACK: Reverting from $CURRENT_TARGET to $PREVIOUS_SLOT"
|
||||||
|
log_info "Previous build: $PREVIOUS_BUILD"
|
||||||
|
|
||||||
|
read -p "Continue with rollback? (yes/no): " -r
|
||||||
|
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
|
||||||
|
log_warn "Rollback cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Switching current symlink to $PREVIOUS_SLOT"
|
||||||
|
ln -sfn "$PREVIOUS_SLOT" "$CURRENT_LINK"
|
||||||
|
|
||||||
|
log_info "Reloading systemd service..."
|
||||||
|
systemctl reload-or-restart "$SERVICE_NAME"
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
if "$ARTIFACT_DIR/scripts/health-check.sh"; then
|
||||||
|
log_info "Rollback successful!"
|
||||||
|
log_info "Active deployment: $PREVIOUS_SLOT ($PREVIOUS_BUILD)"
|
||||||
|
else
|
||||||
|
log_error "Health check failed after rollback - manual intervention required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
39
deployment/systemd/api-server.service
Normal file
39
deployment/systemd/api-server.service
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Golang API Server
|
||||||
|
After=network.target postgresql.service
|
||||||
|
Wants=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=apiserver
|
||||||
|
Group=apiserver
|
||||||
|
WorkingDirectory=/opt/api-artifacts/current
|
||||||
|
|
||||||
|
ExecStart=/opt/api-artifacts/current/api-server
|
||||||
|
|
||||||
|
EnvironmentFile=/opt/api-artifacts/current/.env.production
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StartLimitInterval=300
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/opt/api-artifacts
|
||||||
|
|
||||||
|
LimitNOFILE=65535
|
||||||
|
LimitNPROC=4096
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=api-server
|
||||||
|
|
||||||
|
KillMode=mixed
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
TimeoutStopSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
12
main.go
12
main.go
@ -14,7 +14,11 @@ import (
|
|||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *sql.DB
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
Version string = "dev"
|
||||||
|
BuildTime string = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
func initDB() {
|
func initDB() {
|
||||||
var err error
|
var err error
|
||||||
@ -103,6 +107,12 @@ func main() {
|
|||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/hello", helloWorld)
|
r.GET("/hello", helloWorld)
|
||||||
|
r.GET("/version", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"version": Version,
|
||||||
|
"build_time": BuildTime,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user