Automate source deployment on main
This commit is contained in:
+10
-38
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`
|
||||
|
||||
Executable
+28
@@ -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"
|
||||
Executable
+123
@@ -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}}'
|
||||
Executable
+72
@@ -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
|
||||
Reference in New Issue
Block a user