zkt26/skuska/prepare-app.sh
Bohdan Kapliuk 15f4373858 skuska
2026-05-13 19:50:55 +03:00

402 lines
12 KiB
Bash

#!/bin/bash
set -euo pipefail
REQUESTED_LOCATION="${LOCATION:-}"
REQUESTED_RESOURCE_GROUP="${RESOURCE_GROUP:-}"
APP_PREFIX="${APP_PREFIX:-skuska}"
LOCATION="${LOCATION:-norwayeast}"
RESOURCE_GROUP="${RESOURCE_GROUP:-${APP_PREFIX}-rg}"
CONTAINER_ENV_RESOURCE_GROUP="${CONTAINER_ENV_RESOURCE_GROUP:-$RESOURCE_GROUP}"
ALLOWED_LOCATIONS="${ALLOWED_LOCATIONS:-norwayeast}"
ENV_FILE=".skuska.env"
if ! command -v az >/dev/null 2>&1; then
echo "Azure CLI is required. Install it and run: az login"
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required to build and push container images."
exit 1
fi
if ! command -v openssl >/dev/null 2>&1; then
echo "openssl is required to generate DB_PASSWORD."
exit 1
fi
az account show >/dev/null
if [ -f "$ENV_FILE" ]; then
# shellcheck disable=SC1090
source "$ENV_FILE"
fi
if [ -n "$REQUESTED_LOCATION" ]; then
LOCATION="$REQUESTED_LOCATION"
fi
if [ -n "$REQUESTED_RESOURCE_GROUP" ]; then
RESOURCE_GROUP="$REQUESTED_RESOURCE_GROUP"
fi
CONTAINER_ENV_RESOURCE_GROUP="${CONTAINER_ENV_RESOURCE_GROUP:-$RESOURCE_GROUP}"
SUFFIX="${SUFFIX:-$(date +%s | tail -c 7)}"
ACR_NAME="${ACR_NAME:-${APP_PREFIX}acr${SUFFIX}}"
CONTAINER_ENV="${CONTAINER_ENV:-${APP_PREFIX}-env}"
APP_NAME="${APP_NAME:-${APP_PREFIX}-app}"
STORAGE_ACCOUNT="${STORAGE_ACCOUNT:-${APP_PREFIX}st${SUFFIX}}"
STORAGE_ACCOUNT="$(echo "$STORAGE_ACCOUNT" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9' | cut -c1-24)"
STORAGE_SHARE="${STORAGE_SHARE:-pgbackup}"
STORAGE_MOUNT_NAME="${STORAGE_MOUNT_NAME:-postgres-backup-storage}"
DB_NAME="${DB_NAME:-appdb}"
DB_USER="${DB_USER:-appuser}"
DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c 24)}"
cat > "$ENV_FILE" <<EOF
APP_PREFIX=$APP_PREFIX
LOCATION=$LOCATION
ALLOWED_LOCATIONS="$ALLOWED_LOCATIONS"
RESOURCE_GROUP=$RESOURCE_GROUP
CONTAINER_ENV_RESOURCE_GROUP=$CONTAINER_ENV_RESOURCE_GROUP
SUFFIX=$SUFFIX
ACR_NAME=$ACR_NAME
CONTAINER_ENV=$CONTAINER_ENV
APP_NAME=$APP_NAME
STORAGE_ACCOUNT=$STORAGE_ACCOUNT
STORAGE_SHARE=$STORAGE_SHARE
STORAGE_MOUNT_NAME=$STORAGE_MOUNT_NAME
DB_NAME=$DB_NAME
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
EOF
wait_for_provider() {
local namespace="$1"
local state=""
echo "Registering Azure provider: $namespace"
az provider register --namespace "$namespace" >/dev/null
for _ in $(seq 1 60); do
state="$(az provider show --namespace "$namespace" --query registrationState -o tsv 2>/dev/null || true)"
echo " $namespace state: ${state:-unknown}"
if [ "$state" = "Registered" ]; then
return 0
fi
sleep 10
done
echo "Provider $namespace was not registered after 10 minutes."
exit 1
}
wait_for_acr() {
local state=""
echo "Waiting for Azure Container Registry: $ACR_NAME"
for _ in $(seq 1 60); do
state="$(az acr show \
--resource-group "$RESOURCE_GROUP" \
--name "$ACR_NAME" \
--query provisioningState \
-o tsv 2>/dev/null || true)"
echo " $ACR_NAME state: ${state:-not found yet}"
if [ "$state" = "Succeeded" ]; then
return 0
fi
if [ "$state" = "Failed" ]; then
echo "Azure Container Registry provisioning failed."
exit 1
fi
sleep 10
done
echo "Azure Container Registry was not ready after 10 minutes."
exit 1
}
get_acr_credentials() {
for _ in $(seq 1 30); do
ACR_LOGIN_SERVER="$(az acr show --resource-group "$RESOURCE_GROUP" --name "$ACR_NAME" --query loginServer -o tsv 2>/dev/null || true)"
ACR_USERNAME="$(az acr credential show --name "$ACR_NAME" --query username -o tsv 2>/dev/null || true)"
ACR_PASSWORD="$(az acr credential show --name "$ACR_NAME" --query passwords[0].value -o tsv 2>/dev/null || true)"
if [ -n "$ACR_LOGIN_SERVER" ] && [ -n "$ACR_USERNAME" ] && [ -n "$ACR_PASSWORD" ]; then
return 0
fi
echo " ACR credentials are not ready yet..."
sleep 10
done
echo "Could not read Azure Container Registry credentials."
exit 1
}
yaml_quote() {
printf "'%s'" "$(printf "%s" "$1" | sed "s/'/''/g")"
}
ensure_container_env() {
local existing_env=""
local candidate=""
local candidates="${CONTAINER_ENV_LOCATIONS:-$ALLOWED_LOCATIONS}"
if az containerapp env show \
--resource-group "$CONTAINER_ENV_RESOURCE_GROUP" \
--name "$CONTAINER_ENV" >/dev/null 2>&1; then
echo "Using existing Container Apps environment: $CONTAINER_ENV in $CONTAINER_ENV_RESOURCE_GROUP"
return 0
fi
existing_env="$(az containerapp env list \
--query "[?name=='$CONTAINER_ENV'] | [0].resourceGroup" \
-o tsv 2>/dev/null || true)"
if [ -n "$existing_env" ] && [ "$existing_env" != "None" ]; then
CONTAINER_ENV_RESOURCE_GROUP="$existing_env"
echo "Found existing Container Apps environment: $CONTAINER_ENV in $CONTAINER_ENV_RESOURCE_GROUP"
return 0
fi
echo "Creating one Container Apps environment: $CONTAINER_ENV"
for candidate in $candidates; do
echo " Trying Container Apps environment region: $candidate"
if az containerapp env create \
--resource-group "$CONTAINER_ENV_RESOURCE_GROUP" \
--name "$CONTAINER_ENV" \
--location "$candidate" >/dev/null 2>&1; then
LOCATION="$candidate"
echo " Created Container Apps environment in $LOCATION"
return 0
fi
done
echo
echo "Could not create Container Apps environment in tested regions."
echo "Your subscription likely blocks Container Apps environments or reached the regional quota."
echo "List existing environments with:"
echo " az containerapp env list -o table"
echo
echo "If Azure Portal shows an existing environment, rerun with:"
echo " CONTAINER_ENV=<existing-env-name> CONTAINER_ENV_RESOURCE_GROUP=<existing-env-rg> ./prepare-app.sh"
echo
echo "You can also provide your own region list:"
echo " CONTAINER_ENV_LOCATIONS=\"region1 region2 region3\" ./prepare-app.sh"
exit 1
}
echo "Installing/updating Azure Container Apps extension..."
az extension add --name containerapp --upgrade >/dev/null
wait_for_provider "Microsoft.App"
wait_for_provider "Microsoft.OperationalInsights"
wait_for_provider "Microsoft.ContainerRegistry"
wait_for_provider "Microsoft.Storage"
EXISTING_RESOURCE_GROUP_LOCATION="$(az group show --name "$RESOURCE_GROUP" --query location -o tsv 2>/dev/null || true)"
if [ -n "$EXISTING_RESOURCE_GROUP_LOCATION" ]; then
LOCATION="$EXISTING_RESOURCE_GROUP_LOCATION"
echo "Using existing resource group $RESOURCE_GROUP in $LOCATION..."
else
echo "Creating resource group in $LOCATION..."
az group create --name "$RESOURCE_GROUP" --location "$LOCATION" >/dev/null
fi
if [ "$CONTAINER_ENV_RESOURCE_GROUP" != "$RESOURCE_GROUP" ]; then
az group show --name "$CONTAINER_ENV_RESOURCE_GROUP" >/dev/null
fi
echo "Creating Azure Container Registry..."
if ! az acr show --resource-group "$RESOURCE_GROUP" --name "$ACR_NAME" >/dev/null 2>&1; then
az acr create \
--resource-group "$RESOURCE_GROUP" \
--name "$ACR_NAME" \
--sku Basic \
--admin-enabled true >/dev/null
fi
wait_for_acr
az acr update --resource-group "$RESOURCE_GROUP" --name "$ACR_NAME" --admin-enabled true >/dev/null
get_acr_credentials
echo "Logging in to Azure Container Registry..."
az acr login --name "$ACR_NAME" >/dev/null
echo "Building and pushing backend image..."
docker build -t "$ACR_LOGIN_SERVER/${APP_PREFIX}-backend:latest" ./backend
docker push "$ACR_LOGIN_SERVER/${APP_PREFIX}-backend:latest"
echo "Building and pushing frontend image..."
docker build --build-arg API_BASE_URL=/api -t "$ACR_LOGIN_SERVER/${APP_PREFIX}-frontend:latest" ./frontend
docker push "$ACR_LOGIN_SERVER/${APP_PREFIX}-frontend:latest"
echo "Building and pushing postgres image..."
docker build -t "$ACR_LOGIN_SERVER/${APP_PREFIX}-postgres:latest" ./postgres
docker push "$ACR_LOGIN_SERVER/${APP_PREFIX}-postgres:latest"
ensure_container_env
CONTAINER_ENV_ID="$(az containerapp env show \
--resource-group "$CONTAINER_ENV_RESOURCE_GROUP" \
--name "$CONTAINER_ENV" \
--query id -o tsv)"
echo "Creating Azure Storage Account for persistent PostgreSQL backups..."
if ! az storage account show --resource-group "$RESOURCE_GROUP" --name "$STORAGE_ACCOUNT" >/dev/null 2>&1; then
az storage account create \
--resource-group "$RESOURCE_GROUP" \
--name "$STORAGE_ACCOUNT" \
--location "$LOCATION" \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 >/dev/null
fi
STORAGE_KEY="$(az storage account keys list \
--resource-group "$RESOURCE_GROUP" \
--account-name "$STORAGE_ACCOUNT" \
--query "[0].value" \
-o tsv)"
echo "Creating Azure Files share for persistent PostgreSQL backups..."
az storage share create \
--account-name "$STORAGE_ACCOUNT" \
--account-key "$STORAGE_KEY" \
--name "$STORAGE_SHARE" \
--quota 5 >/dev/null
echo "Connecting Azure Files share to Container Apps environment..."
az containerapp env storage set \
--resource-group "$CONTAINER_ENV_RESOURCE_GROUP" \
--name "$CONTAINER_ENV" \
--storage-name "$STORAGE_MOUNT_NAME" \
--storage-type AzureFile \
--azure-file-account-name "$STORAGE_ACCOUNT" \
--azure-file-account-key "$STORAGE_KEY" \
--azure-file-share-name "$STORAGE_SHARE" \
--access-mode ReadWrite >/dev/null
APP_YAML=".skuska-containerapp.yaml"
REGISTRY_PASSWORD_YAML="$(yaml_quote "$ACR_PASSWORD")"
DB_PASSWORD_YAML="$(yaml_quote "$DB_PASSWORD")"
cat > "$APP_YAML" <<EOF
properties:
managedEnvironmentId: $CONTAINER_ENV_ID
configuration:
activeRevisionsMode: Single
secrets:
- name: registry-password
value: $REGISTRY_PASSWORD_YAML
- name: db-password
value: $DB_PASSWORD_YAML
registries:
- server: $ACR_LOGIN_SERVER
username: $ACR_USERNAME
passwordSecretRef: registry-password
ingress:
external: true
targetPort: 80
transport: auto
allowInsecure: false
template:
containers:
- name: frontend
image: $ACR_LOGIN_SERVER/${APP_PREFIX}-frontend:latest
resources:
cpu: 0.25
memory: 0.5Gi
- name: backend
image: $ACR_LOGIN_SERVER/${APP_PREFIX}-backend:latest
env:
- name: DB_HOST
value: 127.0.0.1
- name: DB_PORT
value: "5432"
- name: DB_USER
value: $DB_USER
- name: DB_PASSWORD
secretRef: db-password
- name: DB_NAME
value: $DB_NAME
- name: DB_SSL
value: "false"
resources:
cpu: 0.5
memory: 1Gi
- name: postgres
image: $ACR_LOGIN_SERVER/${APP_PREFIX}-postgres:latest
env:
- name: POSTGRES_USER
value: $DB_USER
- name: POSTGRES_PASSWORD
secretRef: db-password
- name: POSTGRES_DB
value: $DB_NAME
- name: PGDATA
value: /tmp/postgres-data
- name: BACKUP_DIR
value: /backup
- name: BACKUP_INTERVAL_SECONDS
value: "60"
resources:
cpu: 0.5
memory: 1Gi
volumeMounts:
- volumeName: postgres-backup
mountPath: /backup
scale:
minReplicas: 1
maxReplicas: 1
volumes:
- name: postgres-backup
storageType: AzureFile
storageName: $STORAGE_MOUNT_NAME
EOF
echo "Deploying one Container App with three containers: frontend, backend, postgres..."
az containerapp delete --resource-group "$RESOURCE_GROUP" --name "$APP_NAME" --yes >/dev/null 2>&1 || true
az containerapp create \
--resource-group "$RESOURCE_GROUP" \
--name "$APP_NAME" \
--environment "$CONTAINER_ENV_ID" \
--yaml "$APP_YAML" >/dev/null
APP_FQDN="$(az containerapp show --resource-group "$RESOURCE_GROUP" --name "$APP_NAME" --query properties.configuration.ingress.fqdn -o tsv)"
rm -f "$APP_YAML"
grep -v -E '^(LOCATION|ACR_LOGIN_SERVER|APP_URL|CONTAINER_ENV_RESOURCE_GROUP)=' "$ENV_FILE" > "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "$ENV_FILE"
cat >> "$ENV_FILE" <<EOF
LOCATION=$LOCATION
CONTAINER_ENV_RESOURCE_GROUP=$CONTAINER_ENV_RESOURCE_GROUP
ACR_LOGIN_SERVER=$ACR_LOGIN_SERVER
APP_URL=https://$APP_FQDN
EOF
echo
echo "Application is ready:"
echo "URL: https://$APP_FQDN"
echo "Container App: $APP_NAME"
echo "Containers: frontend, backend, postgres"
echo "Environment: $CONTAINER_ENV in $CONTAINER_ENV_RESOURCE_GROUP"
echo "Persistent backup volume: Azure Files share $STORAGE_SHARE mounted to postgres at /backup"
echo
echo "Local deployment values were saved to $ENV_FILE. Do not commit this file."