Automate source deployment on main

This commit is contained in:
Your Name
2026-04-04 03:10:08 +05:00
parent dcddd12c56
commit 6cebd8216c
8 changed files with 320 additions and 41 deletions
+10 -38
View File
@@ -197,49 +197,21 @@ deploy-production-source:
- call-center-prod
needs:
- test
- job: postgres-readiness
optional: true
variables:
DEPLOY_DIR: /home/gitlab-runner/deploy/call-center
APP_IMAGE_NAME: call-center-app
HEALTHCHECK_URL: http://127.0.0.1:8080/health
environment:
name: production
before_script:
- command -v rsync >/dev/null
- command -v docker >/dev/null
- |
if [ -n "$DEPLOY_HOST" ] && [ -n "$DEPLOY_USER" ]; then
command -v ssh >/dev/null
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
fi
- command -v python3 >/dev/null
script:
- export DEPLOY_TARGET_PATH="${DEPLOY_PATH:-/home/mvpcc/call-center}"
- |
if [ -n "$DEPLOY_HOST" ] && [ -n "$DEPLOY_USER" ]; then
ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_TARGET_PATH'"
rsync -av --exclude '.git/' --exclude '.tmp/' --exclude '.codex_tmp/' --exclude '.local_stack/' --exclude '.data_pg/' ./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_TARGET_PATH/"
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"mkdir -p '$DEPLOY_TARGET_PATH/.data_local/generated_ivr_yandex/ivr' '$DEPLOY_TARGET_PATH/.data_local/recordings' '$DEPLOY_TARGET_PATH/.asterisk_assets'"
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
"cd '$DEPLOY_TARGET_PATH/deployment' && \
docker compose -f docker-compose.server.yml up -d --build && \
docker compose -f docker-compose.asterisk.server.yml up -d --build"
else
run_local() {
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
sudo -n "$@"
else
"$@"
fi
}
run_local mkdir -p "$DEPLOY_TARGET_PATH"
run_local rsync -av --exclude '.git/' --exclude '.tmp/' --exclude '.codex_tmp/' --exclude '.local_stack/' --exclude '.data_pg/' ./ "$DEPLOY_TARGET_PATH/"
run_local mkdir -p "$DEPLOY_TARGET_PATH/.data_local/generated_ivr_yandex/ivr" "$DEPLOY_TARGET_PATH/.data_local/recordings" "$DEPLOY_TARGET_PATH/.asterisk_assets"
cd "$DEPLOY_TARGET_PATH/deployment"
run_local docker compose -f docker-compose.server.yml up -d --build
run_local docker compose -f docker-compose.asterisk.server.yml up -d --build
fi
- bash scripts/deploy_gitlab.sh
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- when: never
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
allow_failure: false
@@ -173,7 +173,7 @@ services:
api-gateway:
<<: *service_defaults
container_name: call-center-api-gateway
container_name: call-center-app
depends_on:
- auth-service
- audit-service
+2 -2
View File
@@ -26,7 +26,7 @@ x-app-env: &app_env
x-service-defaults: &service_defaults
build:
context: ..
image: call-center-app:local
image: ${CALL_CENTER_IMAGE:-call-center-app:latest}
restart: unless-stopped
env_file:
- ../.env.production
@@ -175,7 +175,7 @@ services:
api-gateway:
<<: *service_defaults
container_name: call-center-api-gateway
container_name: call-center-app
depends_on:
- auth-service
- audit-service
+80
View File
@@ -0,0 +1,80 @@
# GitLab CI/CD for call-center
## What this setup does
- a push to `main` triggers GitLab CI
- the job runs on a dedicated `shell` runner with tag `call-center-prod`
- the runner syncs the repository into `/home/gitlab-runner/deploy/call-center`
- Docker builds `call-center-app:<commit-sha>` and also tags `call-center-app:latest`
- `docker compose` recreates the stack from `deployment/docker-compose.server.yml`
- the public gateway container is named `call-center-app`
## Required server prerequisites
- Docker installed and running
- outbound access to `https://gitlab.konturai.kz`
- runner token from GitLab with prefix `glrt-...` or a valid project/group runner token
## Install and register the runner
Run on the target server as `root`:
```bash
cd /path/to/call-center
RUNNER_TOKEN=glrt-xxxxxxxx bash scripts/install_gitlab_runner.sh
```
The script:
- installs `gitlab-runner` from the official GitLab repository
- adds user `gitlab-runner` to the `docker` group
- creates deploy directory `/home/gitlab-runner/deploy/call-center`
- registers runner `call-center-prod-runner` with tag `call-center-prod`
If you first want to install the service without registration:
```bash
SKIP_REGISTER=1 bash scripts/install_gitlab_runner.sh
```
## Production environment file
Create the production env file once on the server:
```bash
install -m 600 /dev/null /home/gitlab-runner/deploy/call-center/.env.production
```
Then fill it with the values required by `deployment/docker-compose.server.yml`.
If the project is already running from `/root/call-center`, migrate the current env file and SQLite/files before the first CI deploy:
```bash
bash scripts/bootstrap_gitlab_deploy.sh
```
Alternative:
- keep the env file elsewhere
- pass `DEPLOY_ENV_FILE=/absolute/path/to/.env.production` in GitLab CI/CD variables
## GitLab CI/CD variables
Optional project variables:
- `DEPLOY_DIR` if you want a different deploy directory
- `APP_IMAGE_NAME` if you want a different Docker image name
- `HEALTHCHECK_URL` if the gateway health URL differs
- `DEPLOY_ENV_FILE` if `.env.production` should be copied from another location
## First deployment
1. Register the runner.
2. Add the production env file.
3. Push this configuration to the `main` branch.
4. Confirm the pipeline completes successfully.
5. Verify the container:
```bash
docker ps --filter name=call-center-app
```
+4
View File
@@ -7,6 +7,10 @@ This project is prepared for a GitLab-first delivery flow:
3. GitLab CI builds and pushes container images to GitLab Container Registry.
4. The server only receives the deployment bundle and pulls images from the registry.
For a same-host shell-runner flow that rebuilds `call-center-app` directly on push to `main`, see:
- `docs/runbooks/gitlab-cicd.md`
## Files used
- `.gitlab-ci.yml`
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SOURCE_DIR="${SOURCE_DIR:-/root/call-center}"
DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/call-center}"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run this script as root so it can read the current deployment under /root." >&2
exit 1
fi
if [[ ! -d "$SOURCE_DIR/.data_local" ]]; then
echo "Missing source data directory: $SOURCE_DIR/.data_local" >&2
exit 1
fi
if [[ ! -f "$SOURCE_DIR/.env.production" ]]; then
echo "Missing source env file: $SOURCE_DIR/.env.production" >&2
exit 1
fi
install -d -o gitlab-runner -g gitlab-runner "$DEPLOY_DIR"
rsync -a --delete "$SOURCE_DIR/.data_local/" "$DEPLOY_DIR/.data_local/"
install -m 600 -o gitlab-runner -g gitlab-runner "$SOURCE_DIR/.env.production" "$DEPLOY_DIR/.env.production"
chown -R gitlab-runner:gitlab-runner "$DEPLOY_DIR/.data_local"
echo "Bootstrap complete: copied .data_local and .env.production into $DEPLOY_DIR"
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -Eeuo pipefail
DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/call-center}"
APP_IMAGE_NAME="${APP_IMAGE_NAME:-call-center-app}"
APP_IMAGE_TAG="${APP_IMAGE_TAG:-${CI_COMMIT_SHORT_SHA:-latest}}"
COMPOSE_FILE="${COMPOSE_FILE:-$DEPLOY_DIR/deployment/docker-compose.server.yml}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1:8080/health}"
HEALTHCHECK_TIMEOUT_SECONDS="${HEALTHCHECK_TIMEOUT_SECONDS:-120}"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing required command: $1" >&2
exit 1
fi
}
wait_for_health() {
python3 - "$HEALTHCHECK_URL" "$HEALTHCHECK_TIMEOUT_SECONDS" <<'PY'
import sys
import time
import urllib.request
url = sys.argv[1]
timeout_seconds = int(sys.argv[2])
deadline = time.time() + timeout_seconds
last_error = None
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=5) as response:
if response.getcode() == 200:
print(f"Healthcheck passed: {url}")
sys.exit(0)
last_error = f"unexpected status {response.getcode()}"
except Exception as exc:
last_error = str(exc)
time.sleep(3)
print(f"Healthcheck failed for {url}: {last_error}", file=sys.stderr)
sys.exit(1)
PY
}
require_command docker
require_command rsync
require_command python3
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
else
echo "Missing docker compose or docker-compose" >&2
exit 1
fi
if [[ -z "${CI_PROJECT_DIR:-}" || ! -d "${CI_PROJECT_DIR:-}" ]]; then
echo "CI_PROJECT_DIR is not set or does not exist" >&2
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker daemon is not reachable for the runner user" >&2
exit 1
fi
install -d "$DEPLOY_DIR"
cd "$DEPLOY_DIR"
if [[ "$CI_PROJECT_DIR" != "$DEPLOY_DIR" ]]; then
rsync -a --delete \
--exclude '.git/' \
--exclude '.env.production' \
--exclude '.env.production.*' \
--exclude '.data/' \
--exclude '.data_local/' \
--exclude '.data_gate3/' \
--exclude '.data_gate4/' \
--exclude '.data_uat_dry_run/' \
--exclude '.data_uat_preflight/' \
--exclude '.venv/' \
--exclude '.pytest_cache/' \
--exclude '.local_stack/' \
--exclude 'test-results/' \
--exclude '.codex_backup/' \
--exclude '.db_backups/' \
--exclude '.deploy_backups/' \
"$CI_PROJECT_DIR/" "$DEPLOY_DIR/"
fi
if [[ ! -f "$DEPLOY_DIR/.env.production" ]]; then
if [[ -n "${DEPLOY_ENV_FILE:-}" && -f "${DEPLOY_ENV_FILE}" ]]; then
install -m 600 "$DEPLOY_ENV_FILE" "$DEPLOY_DIR/.env.production"
else
echo "Missing $DEPLOY_DIR/.env.production. Create it once on the server or set DEPLOY_ENV_FILE." >&2
exit 1
fi
fi
install -d "$DEPLOY_DIR/.data_local"
export CALL_CENTER_IMAGE="${APP_IMAGE_NAME}:${APP_IMAGE_TAG}"
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-call-center}"
echo "Deploy directory: $DEPLOY_DIR"
echo "Building image: $CALL_CENTER_IMAGE"
docker build -t "$CALL_CENTER_IMAGE" -t "${APP_IMAGE_NAME}:latest" "$DEPLOY_DIR"
if docker ps -a --format '{{.Names}}' | grep -Fxq 'call-center-api-gateway' \
&& ! docker ps -a --format '{{.Names}}' | grep -Fxq 'call-center-app'; then
echo "Removing legacy gateway container call-center-api-gateway before rename to call-center-app"
docker rm -f call-center-api-gateway
fi
"${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" config -q
"${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" up -d --remove-orphans --force-recreate
wait_for_health
docker ps --filter "name=^call-center-app$" --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -Eeuo pipefail
GITLAB_URL="${GITLAB_URL:-https://gitlab.konturai.kz/}"
RUNNER_NAME="${RUNNER_NAME:-call-center-prod-runner}"
RUNNER_TAGS="${RUNNER_TAGS:-call-center-prod}"
RUNNER_EXECUTOR="${RUNNER_EXECUTOR:-shell}"
RUNNER_TOKEN="${RUNNER_TOKEN:-}"
DEPLOY_DIR="${DEPLOY_DIR:-/home/gitlab-runner/deploy/call-center}"
SKIP_REGISTER="${SKIP_REGISTER:-0}"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run this script as root." >&2
exit 1
fi
if [[ "$SKIP_REGISTER" != "1" && -z "$RUNNER_TOKEN" ]]; then
echo "Set RUNNER_TOKEN from GitLab project/group runner settings before running this script." >&2
exit 1
fi
apt-get update
apt-get install -y ca-certificates curl
if ! command -v gitlab-runner >/dev/null 2>&1; then
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash
apt-get install -y gitlab-runner
fi
if ! getent group docker >/dev/null 2>&1; then
echo "Docker group does not exist. Install Docker before registering the runner." >&2
exit 1
fi
usermod -aG docker gitlab-runner
install -d -o gitlab-runner -g gitlab-runner "$(dirname "$DEPLOY_DIR")"
install -d -o gitlab-runner -g gitlab-runner "$DEPLOY_DIR"
if [[ "$SKIP_REGISTER" == "1" ]]; then
echo "gitlab-runner installed. Registration skipped because SKIP_REGISTER=1."
elif gitlab-runner list 2>/dev/null | grep -Fq "$RUNNER_NAME"; then
echo "Runner $RUNNER_NAME is already registered."
else
register_args=(
--non-interactive
--url "$GITLAB_URL"
--executor "$RUNNER_EXECUTOR"
--description "$RUNNER_NAME"
--tag-list "$RUNNER_TAGS"
--run-untagged="false"
--locked="true"
)
if [[ "$RUNNER_TOKEN" == glrt-* ]]; then
register_args+=(--token "$RUNNER_TOKEN")
else
register_args+=(--registration-token "$RUNNER_TOKEN")
fi
gitlab-runner register "${register_args[@]}"
fi
systemctl enable --now gitlab-runner
systemctl restart gitlab-runner
if [[ "$SKIP_REGISTER" != "1" ]]; then
gitlab-runner verify
fi
if [[ "$SKIP_REGISTER" == "1" ]]; then
echo "gitlab-runner service is installed and running."
else
echo "Runner $RUNNER_NAME is ready."
fi