This commit is contained in:
103
docker/deploy/deploy-over-ssh.sh
Executable file
103
docker/deploy/deploy-over-ssh.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Executa deploy.sh em um host remoto via SSH.
|
||||
#
|
||||
# ./deploy-over-ssh.sh <ambiente> <tag>
|
||||
#
|
||||
# O script e o arquivo <ambiente>.env do checkout sao copiados somente para um
|
||||
# diretorio temporario no host remoto. A configuracao sensivel da aplicacao
|
||||
# continua no CONFIG_DIR daquele host; ela nunca passa pelo job nem pelo SSH.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AMBIENTE="${1:-}"
|
||||
TAG="${2:-}"
|
||||
SSH_HOST="${SSH_HOST:-}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${SSH_USER:-}"
|
||||
SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-}"
|
||||
SSH_KNOWN_HOSTS="${SSH_KNOWN_HOSTS:-}"
|
||||
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
|
||||
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
|
||||
REGISTRY_USER="${REGISTRY_USER:-${ACTOR:-}}"
|
||||
|
||||
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${AQUI}/${AMBIENTE}.env"
|
||||
|
||||
log() { echo "[deploy-ssh] $*"; }
|
||||
fail() { echo "[deploy-ssh][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
|
||||
[ -n "${SSH_HOST}" ] || fail "SSH_HOST nao definido"
|
||||
[ -n "${SSH_USER}" ] || fail "SSH_USER nao definido"
|
||||
[ -n "${SSH_PRIVATE_KEY}" ] || fail "SSH_PRIVATE_KEY nao definido"
|
||||
[ -n "${SSH_KNOWN_HOSTS}" ] || fail "SSH_KNOWN_HOSTS nao definido"
|
||||
[ -n "${REGISTRY_TOKEN}" ] || fail "REGISTRY_TOKEN nao definido"
|
||||
[ -n "${REGISTRY_USER}" ] || fail "REGISTRY_USER/ACTOR nao definido"
|
||||
|
||||
command -v ssh >/dev/null || fail "cliente ssh nao encontrado no job"
|
||||
command -v scp >/dev/null || fail "cliente scp nao encontrado no job"
|
||||
|
||||
TMP_LOCAL="$(mktemp -d)"
|
||||
KEY_FILE="${TMP_LOCAL}/id_ed25519"
|
||||
KNOWN_HOSTS_FILE="${TMP_LOCAL}/known_hosts"
|
||||
REMOTE_DIR=""
|
||||
|
||||
cleanup() {
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
if [ -n "${REMOTE_DIR}" ]; then
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
|
||||
"docker logout '${REGISTRY}' >/dev/null 2>&1 || true; rm -rf -- '${REMOTE_DIR}'" \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf -- "${TMP_LOCAL}"
|
||||
exit "${status}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf '%s\n' "${SSH_PRIVATE_KEY}" > "${KEY_FILE}"
|
||||
printf '%s\n' "${SSH_KNOWN_HOSTS}" > "${KNOWN_HOSTS_FILE}"
|
||||
chmod 600 "${KEY_FILE}" "${KNOWN_HOSTS_FILE}"
|
||||
|
||||
SSH_OPTS=(
|
||||
-i "${KEY_FILE}"
|
||||
-p "${SSH_PORT}"
|
||||
-o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
SCP_OPTS=(
|
||||
-i "${KEY_FILE}"
|
||||
-P "${SSH_PORT}"
|
||||
-o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
|
||||
)
|
||||
|
||||
log "conectando em ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
|
||||
REMOTE_DIR="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" 'umask 077; mktemp -d /tmp/conectasus-deploy.XXXXXX')"
|
||||
case "${REMOTE_DIR}" in
|
||||
/tmp/conectasus-deploy.*) ;;
|
||||
*) fail "diretorio temporario remoto inesperado: ${REMOTE_DIR}" ;;
|
||||
esac
|
||||
|
||||
log "enviando script de deploy temporariamente"
|
||||
scp "${SCP_OPTS[@]}" \
|
||||
"${AQUI}/deploy.sh" \
|
||||
"${ENV_FILE}" \
|
||||
"${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}/"
|
||||
|
||||
# O login e feito no host remoto porque o docker pull sera executado por ele.
|
||||
# A senha trafega pelo stdin e nao aparece na linha de comando nem nos logs.
|
||||
printf '%s' "${REGISTRY_TOKEN}" | \
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
|
||||
"docker login '${REGISTRY}' -u '${REGISTRY_USER}' --password-stdin" >/dev/null
|
||||
|
||||
log "executando deploy remoto de ${AMBIENTE}:${TAG}"
|
||||
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
|
||||
"chmod 700 '${REMOTE_DIR}/deploy.sh' && '${REMOTE_DIR}/deploy.sh' '${AMBIENTE}' '${TAG}'"
|
||||
|
||||
log "deploy remoto concluido"
|
||||
159
docker/deploy/deploy.sh
Executable file
159
docker/deploy/deploy.sh
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Deploy de um ambiente do ConectaSUS V2.
|
||||
#
|
||||
# ./deploy.sh <ambiente> <tag>
|
||||
#
|
||||
# Le os parametros do ambiente de docker/deploy/<ambiente>.env, troca a imagem
|
||||
# do container e so considera o deploy bem-sucedido depois de verificar que a
|
||||
# aplicacao responde. Se nao responder, volta sozinho para a imagem anterior.
|
||||
#
|
||||
# Roda a partir do runner, que usa o docker.sock do host — por isso todos os
|
||||
# caminhos de mount sao caminhos do HOST, nao do job.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AMBIENTE="${1:-}"
|
||||
TAG="${2:-}"
|
||||
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
|
||||
IMAGE_NAME="${IMAGE_NAME:-rkm/conectasus-api}"
|
||||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-90}"
|
||||
|
||||
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
log() { echo "[deploy] $*"; }
|
||||
fail() { echo "[deploy][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag>"
|
||||
|
||||
ENV_FILE="${AQUI}/${AMBIENTE}.env"
|
||||
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
set -a; . "${ENV_FILE}"; set +a
|
||||
|
||||
# Gabarito nao preenchido: melhor recusar do que implantar em lugar errado.
|
||||
if grep -q 'TROCAR' "${ENV_FILE}"; then
|
||||
fail "${ENV_FILE} ainda tem valores de gabarito (TROCAR)."
|
||||
fi
|
||||
|
||||
for v in CONTAINER_NAME CONFIG_DIR PORT_API PORT_SPA PORT_WS; do
|
||||
[ -n "${!v:-}" ] || fail "${v} nao definido em ${ENV_FILE}"
|
||||
done
|
||||
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
log "ambiente : ${AMBIENTE}"
|
||||
log "container: ${CONTAINER_NAME}"
|
||||
log "imagem : ${IMAGE}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Guarda a imagem atual para poder voltar. Vale o digest, nao a tag: a tag e
|
||||
# mutavel e pode ja apontar para outra coisa na hora do rollback.
|
||||
# ---------------------------------------------------------------------------
|
||||
ANTERIOR=""
|
||||
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
|
||||
ANTERIOR=$(docker inspect "${CONTAINER_NAME}" --format '{{.Image}}')
|
||||
log "imagem atual: ${ANTERIOR}"
|
||||
else
|
||||
log "container ainda nao existe; primeiro deploy"
|
||||
fi
|
||||
|
||||
log "puxando ${IMAGE}"
|
||||
docker pull "${IMAGE}" >/dev/null || fail "pull falhou (registry acessivel? credencial presente?)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Config do host. O shell do job pode rodar dentro de um container do
|
||||
# act_runner e, portanto, nao enxergar os caminhos do host. A verificacao
|
||||
# precisa passar pelo daemon Docker: e ele que avaliara o bind mount no
|
||||
# host e depois subira a aplicacao com esses mesmos arquivos.
|
||||
#
|
||||
# --mount, diferentemente de -v, recusa uma origem inexistente em vez de
|
||||
# criar um diretorio vazio silenciosamente.
|
||||
# ---------------------------------------------------------------------------
|
||||
log "validando configuracao no host Docker"
|
||||
if ! docker run --rm \
|
||||
--mount "type=bind,src=${CONFIG_DIR},dst=/config,readonly" \
|
||||
--entrypoint sh \
|
||||
"${IMAGE}" \
|
||||
-c '
|
||||
set -eu
|
||||
for f in .env connections.json config.js; do
|
||||
[ -f "/config/${f}" ] || {
|
||||
echo "arquivo obrigatorio ausente: /config/${f}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[ -d /config/storage ] || {
|
||||
echo "diretorio obrigatorio ausente: /config/storage" >&2
|
||||
exit 1
|
||||
}
|
||||
'
|
||||
then
|
||||
fail "configuracao invalida ou inacessivel no host Docker: ${CONFIG_DIR}"
|
||||
fi
|
||||
|
||||
subir() {
|
||||
local img="$1"
|
||||
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
|
||||
docker run -d \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--restart unless-stopped \
|
||||
${NETWORK:+--network "${NETWORK}"} \
|
||||
-p "${PORT_API}:80" \
|
||||
-p "${PORT_SPA}:8080" \
|
||||
-p "${PORT_WS}:6001" \
|
||||
-v "${CONFIG_DIR}/.env:/var/www/api/.env:ro" \
|
||||
-v "${CONFIG_DIR}/connections.json:/var/www/api/connections.json:ro" \
|
||||
-v "${CONFIG_DIR}/config.js:/var/www/frontend/config.js:ro" \
|
||||
-v "${CONFIG_DIR}/storage:/var/www/api/storage" \
|
||||
"${img}" >/dev/null
|
||||
}
|
||||
|
||||
saudavel() {
|
||||
local fim=$((SECONDS + HEALTH_TIMEOUT))
|
||||
while [ ${SECONDS} -lt ${fim} ]; do
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "${CONTAINER_NAME}" 2>/dev/null)" != "true" ]; then
|
||||
return 1
|
||||
fi
|
||||
local spa api
|
||||
spa=$(docker exec "${CONTAINER_NAME}" curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ 2>/dev/null || echo 000)
|
||||
api=$(docker exec "${CONTAINER_NAME}" curl -s -o /dev/null -w '%{http_code}' http://localhost:80/ 2>/dev/null || echo 000)
|
||||
# A API nao tem rota "/", entao 404 e resposta valida; 000 e que nao e.
|
||||
if [ "${spa}" = "200" ] && [ "${api}" != "000" ]; then
|
||||
log "saudavel (spa=${spa} api=${api})"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
log "subindo ${CONTAINER_NAME}"
|
||||
subir "${IMAGE}"
|
||||
|
||||
if saudavel; then
|
||||
log "deploy de ${AMBIENTE} concluido em ${IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Nao respondeu. Um container no ar mas quebrado e pior do que a versao
|
||||
# anterior no ar, entao volta.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "::error::deploy de ${AMBIENTE} falhou na verificacao de saude"
|
||||
docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 || true
|
||||
|
||||
if [ -z "${ANTERIOR}" ]; then
|
||||
fail "sem versao anterior para voltar; container deixado no ar para diagnostico"
|
||||
fi
|
||||
|
||||
log "revertendo para ${ANTERIOR}"
|
||||
subir "${ANTERIOR}"
|
||||
|
||||
if saudavel; then
|
||||
fail "deploy revertido: ${AMBIENTE} voltou para a versao anterior"
|
||||
fi
|
||||
|
||||
fail "deploy falhou E a reversao tambem nao respondeu — ${AMBIENTE} precisa de atencao manual"
|
||||
16
docker/deploy/homolog.env
Normal file
16
docker/deploy/homolog.env
Normal file
@@ -0,0 +1,16 @@
|
||||
# Ambiente de HOMOLOG — parametros de deploy.
|
||||
#
|
||||
# TROCAR os valores abaixo. A esteira recusa implantar enquanto encontrar essa
|
||||
# palavra aqui — melhor falhar do que implantar no lugar errado.
|
||||
#
|
||||
# CONFIG_DIR precisa conter, no HOST: .env, connections.json, config.js e
|
||||
# storage/. O config.js e o que define para qual API o frontend aponta.
|
||||
|
||||
CONTAINER_NAME=conectasus-homolog
|
||||
CONFIG_DIR=/TROCAR/caminho/no/host/homolog/config
|
||||
|
||||
NETWORK=TROCAR
|
||||
|
||||
PORT_API=TROCAR
|
||||
PORT_SPA=TROCAR
|
||||
PORT_WS=TROCAR
|
||||
103
docker/deploy/migrate.sh
Executable file
103
docker/deploy/migrate.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Aplica migrations do ConectaSUS para uma lista explicita de municipios.
|
||||
#
|
||||
# ./migrate.sh <ambiente> <tag> <municipio[,municipio...]>
|
||||
#
|
||||
# A migration roda em um container descartavel da imagem informada. Assim ela
|
||||
# nao depende de qual versao esta no ar e usa os mesmos .env,
|
||||
# connections.json e storage montados no host do ambiente.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AMBIENTE="${1:-}"
|
||||
TAG="${2:-}"
|
||||
MUNICIPIOS_CSV="${3:-}"
|
||||
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
|
||||
IMAGE_NAME="${IMAGE_NAME:-rkm/conectasus-api}"
|
||||
|
||||
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
log() { echo "[migrate] $*"; }
|
||||
fail() { echo "[migrate][ERRO] $*" >&2; exit 1; }
|
||||
|
||||
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag> <municipio[,municipio...]>"
|
||||
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag> <municipio[,municipio...]>"
|
||||
[ -n "${MUNICIPIOS_CSV}" ] || fail "informe ao menos um municipio"
|
||||
|
||||
ENV_FILE="${AQUI}/${AMBIENTE}.env"
|
||||
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
set -a; . "${ENV_FILE}"; set +a
|
||||
|
||||
if grep -q 'TROCAR' "${ENV_FILE}"; then
|
||||
fail "${ENV_FILE} ainda tem valores de gabarito (TROCAR)."
|
||||
fi
|
||||
|
||||
for v in CONFIG_DIR; do
|
||||
[ -n "${!v:-}" ] || fail "${v} nao definido em ${ENV_FILE}"
|
||||
done
|
||||
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
# Nomes de conexao do Laravel nao devem conter espacos nem caracteres que
|
||||
# possam alterar a linha de comando. A lista e propositalmente explicita:
|
||||
# conexoes tecnicas (por exemplo, sigtap) nao devem receber migrations por
|
||||
# acidente.
|
||||
IFS=',' read -r -a MUNICIPIOS_BRUTOS <<< "${MUNICIPIOS_CSV}"
|
||||
declare -a MUNICIPIOS=()
|
||||
declare -A VISTOS=()
|
||||
for bruto in "${MUNICIPIOS_BRUTOS[@]}"; do
|
||||
municipio="$(echo "${bruto}" | tr -d '[:space:]')"
|
||||
[ -n "${municipio}" ] || fail "ha um municipio vazio na lista: ${MUNICIPIOS_CSV}"
|
||||
[[ "${municipio}" =~ ^[A-Za-z0-9_-]+$ ]] || fail "nome de municipio invalido: ${municipio}"
|
||||
[ -z "${VISTOS[${municipio}]:-}" ] || fail "municipio repetido na lista: ${municipio}"
|
||||
VISTOS["${municipio}"]=1
|
||||
MUNICIPIOS+=("${municipio}")
|
||||
done
|
||||
|
||||
log "ambiente : ${AMBIENTE}"
|
||||
log "imagem : ${IMAGE}"
|
||||
log "municipios: ${MUNICIPIOS[*]}"
|
||||
|
||||
log "puxando ${IMAGE}"
|
||||
docker pull "${IMAGE}" >/dev/null || fail "pull falhou (registry acessivel? credencial presente?)"
|
||||
|
||||
# --mount valida as fontes no host do Docker. Isso e essencial quando o job
|
||||
# roda em container do act_runner, cujo filesystem nao contem CONFIG_DIR.
|
||||
MOUNTS=(
|
||||
--mount "type=bind,src=${CONFIG_DIR}/.env,dst=/var/www/api/.env,readonly"
|
||||
--mount "type=bind,src=${CONFIG_DIR}/connections.json,dst=/var/www/api/connections.json,readonly"
|
||||
--mount "type=bind,src=${CONFIG_DIR}/storage,dst=/var/www/api/storage"
|
||||
)
|
||||
|
||||
PHP_RUN=(docker run --rm "${MOUNTS[@]}" --workdir /var/www/api --entrypoint php "${IMAGE}")
|
||||
|
||||
# Confere o JSON e todas as conexoes antes de alterar qualquer banco. Nao lista
|
||||
# nem imprime o conteudo do arquivo, que pode conter credenciais.
|
||||
for municipio in "${MUNICIPIOS[@]}"; do
|
||||
if ! "${PHP_RUN[@]}" -r '
|
||||
$connections = json_decode(file_get_contents("connections.json"), true);
|
||||
if (!is_array($connections) || !array_key_exists($argv[1], $connections)) {
|
||||
fwrite(STDERR, "conexao nao encontrada no connections.json: " . $argv[1] . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
' "${municipio}"; then
|
||||
fail "validacao da conexao falhou; nenhum banco recebeu migration"
|
||||
fi
|
||||
done
|
||||
|
||||
MIGRATE_RUN=("${PHP_RUN[@]}")
|
||||
if [ -n "${NETWORK:-}" ]; then
|
||||
MIGRATE_RUN=(docker run --rm --network "${NETWORK}" "${MOUNTS[@]}" --workdir /var/www/api --entrypoint php "${IMAGE}")
|
||||
fi
|
||||
|
||||
total="${#MUNICIPIOS[@]}"
|
||||
for i in "${!MUNICIPIOS[@]}"; do
|
||||
municipio="${MUNICIPIOS[${i}]}"
|
||||
log "[$((i + 1))/${total}] aplicando migrations em ${municipio}"
|
||||
"${MIGRATE_RUN[@]}" artisan migrate --database="${municipio}" --force
|
||||
done
|
||||
|
||||
log "migrations concluidas para ${total} municipio(s)"
|
||||
16
docker/deploy/prod.env
Normal file
16
docker/deploy/prod.env
Normal file
@@ -0,0 +1,16 @@
|
||||
# Ambiente de PROD — parametros de deploy.
|
||||
#
|
||||
# TROCAR os valores abaixo. A esteira recusa implantar enquanto encontrar essa
|
||||
# palavra aqui — melhor falhar do que implantar no lugar errado.
|
||||
#
|
||||
# CONFIG_DIR precisa conter, no HOST: .env, connections.json, config.js e
|
||||
# storage/. O config.js e o que define para qual API o frontend aponta.
|
||||
|
||||
CONTAINER_NAME=conectasus-prod
|
||||
CONFIG_DIR=/TROCAR/caminho/no/host/prod/config
|
||||
|
||||
NETWORK=TROCAR
|
||||
|
||||
PORT_API=TROCAR
|
||||
PORT_SPA=TROCAR
|
||||
PORT_WS=TROCAR
|
||||
12
docker/deploy/teste.env
Normal file
12
docker/deploy/teste.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# Ambiente de TESTE — parametros de deploy.
|
||||
# Valores extraidos do container que ja roda hoje (stack antonio-santos-v2).
|
||||
|
||||
CONTAINER_NAME=conectasus-test
|
||||
CONFIG_DIR=/dados/home/antonio.santos/development/V2/conectasus_test/config
|
||||
|
||||
# Rede do stack, para o container enxergar o banco pelo nome do servico.
|
||||
NETWORK=antonio-santos-v2_default
|
||||
|
||||
PORT_API=3200
|
||||
PORT_SPA=3202
|
||||
PORT_WS=3201
|
||||
Reference in New Issue
Block a user