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 - call-center-prod
needs: needs:
- test - 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: environment:
name: production name: production
before_script: before_script:
- command -v rsync >/dev/null - command -v rsync >/dev/null
- command -v docker >/dev/null - command -v docker >/dev/null
- | - command -v python3 >/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
script: script:
- export DEPLOY_TARGET_PATH="${DEPLOY_PATH:-/home/mvpcc/call-center}" - bash scripts/deploy_gitlab.sh
- |
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
rules: rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual - if: '$CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- when: never
allow_failure: false allow_failure: false
@@ -173,7 +173,7 @@ services:
api-gateway: api-gateway:
<<: *service_defaults <<: *service_defaults
container_name: call-center-api-gateway container_name: call-center-app
depends_on: depends_on:
- auth-service - auth-service
- audit-service - audit-service
+2 -2
View File
@@ -26,7 +26,7 @@ x-app-env: &app_env
x-service-defaults: &service_defaults x-service-defaults: &service_defaults
build: build:
context: .. context: ..
image: call-center-app:local image: ${CALL_CENTER_IMAGE:-call-center-app:latest}
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- ../.env.production - ../.env.production
@@ -175,7 +175,7 @@ services:
api-gateway: api-gateway:
<<: *service_defaults <<: *service_defaults
container_name: call-center-api-gateway container_name: call-center-app
depends_on: depends_on:
- auth-service - auth-service
- audit-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. 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. 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 ## Files used
- `.gitlab-ci.yml` - `.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