This guide covers deploying jira2solidtime to Azure App Service using both Azure CLI (imperative) and Terraform (declarative/Infrastructure as Code).
Before you begin, ensure you have:
- Azure Subscription: Active Azure account
- Azure CLI: Version 2.50+ installed (Install Guide)
- Terraform: Version 1.5+ installed (Install Guide) - for IaC approach
- Docker Hub Account: For custom images (optional)
- Configuration File: Your
config.jsonwith API credentials
# Login to Azure
az login
# Set default subscription (if you have multiple)
az account set --subscription "Your Subscription Name"
# Verify login
az account showThe deployment creates the following Azure resources:
- Resource Group: Logical container for all resources
- App Service Plan: Compute resources (Linux, B1 SKU)
- App Service: Web app running the Docker container
- Storage Account: For persistent data (worklog mappings, sync history)
- File Share: Mounted to
/app/datain the container
Estimated Cost: ~12€/month for B1 tier
# Variables
RESOURCE_GROUP="rg-jira2solidtime"
LOCATION="westeurope"
APP_NAME="jira2solidtime-app" # Must be globally unique!
PLAN_NAME="plan-jira2solidtime"
STORAGE_ACCOUNT="stjira2solidtime" # Must be globally unique, lowercase, no hyphens
# Create resource group
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION# Create storage account
az storage account create \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku Standard_LRS \
--kind StorageV2
# Get storage account key
STORAGE_KEY=$(az storage account keys list \
--resource-group $RESOURCE_GROUP \
--account-name $STORAGE_ACCOUNT \
--query '[0].value' \
--output tsv)
# Create file share for data persistence
az storage share create \
--name jira2solidtime-data \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--quota 5# Create Linux App Service Plan (B1 tier)
az appservice plan create \
--name $PLAN_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--is-linux \
--sku B1# Create web app with container
az webapp create \
--resource-group $RESOURCE_GROUP \
--plan $PLAN_NAME \
--name $APP_NAME \
--deployment-container-image-name cddsab/jira2solidtime:0.1.0
# Configure container settings
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--settings \
WEBSITES_PORT=8080 \
DOCKER_REGISTRY_SERVER_URL=https://index.docker.io \
WEBSITES_ENABLE_APP_SERVICE_STORAGE=false# Mount Azure Files to /app/data
az webapp config storage-account add \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--custom-id data \
--storage-type AzureFiles \
--account-name $STORAGE_ACCOUNT \
--share-name jira2solidtime-data \
--access-key $STORAGE_KEY \
--mount-path /app/dataYou need to provide your config.json. Options:
# Upload config.json to file share
az storage file upload \
--share-name jira2solidtime-data \
--source ./config.json \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY
# Update mount to include config
az webapp config storage-account add \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--custom-id config \
--storage-type AzureFiles \
--account-name $STORAGE_ACCOUNT \
--share-name jira2solidtime-data \
--access-key $STORAGE_KEY \
--mount-path /app/config.jsonconfig.json. Consider extending the app to support env vars if needed.
# Restart web app
az webapp restart \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME
# Get app URL
APP_URL=$(az webapp show \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--query defaultHostName \
--output tsv)
echo "Application URL: https://${APP_URL}"
# Test health
curl -I "https://${APP_URL}/"# Stream live logs
az webapp log tail \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME
# Download logs
az webapp log download \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--log-file webapp-logs.zipFor reproducible, version-controlled infrastructure, use Terraform.
The Terraform configuration is located in examples/terraform/azure-app-service/:
examples/terraform/azure-app-service/
├── main.tf # Main infrastructure definition
├── variables.tf # Input variables
├── outputs.tf # Output values
├── terraform.tfvars.example # Example configuration
└── README.md # Terraform-specific docs
# Navigate to Terraform directory
cd examples/terraform/azure-app-service/
# Copy example configuration
cp terraform.tfvars.example terraform.tfvars
# Edit configuration
nano terraform.tfvarsterraform.tfvars:
resource_group_name = "rg-jira2solidtime"
location = "westeurope"
app_service_plan_name = "plan-jira2solidtime"
app_name = "jira2solidtime-app" # Must be globally unique
storage_account_name = "stjira2solidtime" # Must be globally unique
docker_image = "cddsab/jira2solidtime:0.1.0"
sku_name = "B1"# Initialize Terraform (downloads providers)
terraform init
# Validate configuration
terraform validate
# Preview changes
terraform plan# Apply configuration
terraform apply
# Confirm with 'yes' when promptedTerraform will create all resources and output the application URL.
After infrastructure is created, upload config.json:
# Get storage account details from Terraform output
STORAGE_ACCOUNT=$(terraform output -raw storage_account_name)
STORAGE_KEY=$(terraform output -raw storage_account_key)
# Upload config
az storage file upload \
--share-name jira2solidtime-data \
--source ../../config.json \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY# Get app URL
APP_URL=$(terraform output -raw app_url)
# Test application
curl -I "$APP_URL"
# Open in browser
open "$APP_URL"# View current state
terraform show
# Update infrastructure (after changing variables)
terraform apply
# Destroy all resources
terraform destroyFor non-sensitive configuration, use App Settings:
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--settings \
SYNC_SCHEDULE="0 8 * * *" \
SYNC_DAYS_BACK=30For sensitive credentials:
# Create Key Vault
az keyvault create \
--name kv-jira2solidtime \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
# Store secrets
az keyvault secret set \
--vault-name kv-jira2solidtime \
--name jira-api-token \
--value "your-secret-token"
# Grant web app access
az webapp identity assign \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME
# Get managed identity principal ID
PRINCIPAL_ID=$(az webapp identity show \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--query principalId \
--output tsv)
# Grant Key Vault access
az keyvault set-policy \
--name kv-jira2solidtime \
--object-id $PRINCIPAL_ID \
--secret-permissions get listNote: You'll need to modify the app to read from Key Vault.
Enable built-in monitoring:
# Create Application Insights
az monitor app-insights component create \
--app jira2solidtime-insights \
--location $LOCATION \
--resource-group $RESOURCE_GROUP \
--application-type web
# Get instrumentation key
INSTRUMENTATION_KEY=$(az monitor app-insights component show \
--app jira2solidtime-insights \
--resource-group $RESOURCE_GROUP \
--query instrumentationKey \
--output tsv)
# Link to web app
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--settings \
APPINSIGHTS_INSTRUMENTATIONKEY=$INSTRUMENTATION_KEYView metrics and logs in Azure Portal:
- Navigate to App Service → Monitoring → Logs
- Use Kusto Query Language (KQL) for advanced queries
Example query:
AppServiceConsoleLogs
| where TimeGenerated > ago(1h)
| where ResultDescription contains "sync"
| order by TimeGenerated descCreate alerts for critical events:
# Alert on high CPU usage
az monitor metrics alert create \
--name "High CPU Alert" \
--resource-group $RESOURCE_GROUP \
--scopes $(az webapp show --resource-group $RESOURCE_GROUP --name $APP_NAME --query id --output tsv) \
--condition "avg Percentage CPU > 80" \
--window-size 5m \
--evaluation-frequency 1m# Upgrade to S1 (more CPU/memory)
az appservice plan update \
--name $PLAN_NAME \
--resource-group $RESOURCE_GROUP \
--sku S1# Scale out to 2 instances
az appservice plan update \
--name $PLAN_NAME \
--resource-group $RESOURCE_GROUP \
--number-of-workers 2- Using Azure Database for PostgreSQL instead of SQLite
- Implementing distributed locking for sync operations
Restrict access to specific IPs:
# Allow only your office IP
az webapp config access-restriction add \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--rule-name "AllowOffice" \
--action Allow \
--ip-address "203.0.113.0/24" \
--priority 100
# Deny all other traffic
az webapp config access-restriction add \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--rule-name "DenyAll" \
--action Deny \
--ip-address "0.0.0.0/0" \
--priority 200# Enforce HTTPS
az webapp update \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--https-only true# Add custom domain
az webapp config hostname add \
--resource-group $RESOURCE_GROUP \
--webapp-name $APP_NAME \
--hostname jira2solidtime.yourdomain.com
# Bind SSL certificate (free managed certificate)
az webapp config ssl bind \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--certificate-thumbprint <thumbprint> \
--ssl-type SNIEnable automatic backups:
# Create backup storage
az storage account create \
--name stbackupjira2solidtime \
--resource-group $RESOURCE_GROUP \
--sku Standard_LRS
# Configure backup
az webapp config backup create \
--resource-group $RESOURCE_GROUP \
--webapp-name $APP_NAME \
--backup-name daily-backup \
--container-url "https://stbackupjira2solidtime.blob.core.windows.net/backups"# Backup data file share
az storage file download-batch \
--account-name $STORAGE_ACCOUNT \
--source jira2solidtime-data \
--destination ./backup/$(date +%Y%m%d)| Resource | Monthly Cost |
|---|---|
| App Service Plan B1 | ~10€ |
| Storage Account | ~0.50€ |
| Bandwidth (estimated) | ~1€ |
| Total | ~12€ |
- Use Free Tier for Testing: F1 SKU is free but limited
- Stop non-production environments:
az webapp stop --resource-group $RESOURCE_GROUP --name $APP_NAME
- Use Reserved Instances: 1-3 year commitments save 30-50%
- Optimize sync frequency: Reduce API calls by adjusting cron schedule
Symptoms: HTTP 500 errors, app not accessible
Debug:
# View logs
az webapp log tail --resource-group $RESOURCE_GROUP --name $APP_NAME
# Check container status
az webapp show \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--query stateCommon causes:
- Missing
config.jsonin file share - Incorrect
WEBSITES_PORTsetting - Docker image pull failures
Symptoms: Sync history lost on restart
Verify mount:
# List storage mounts
az webapp config storage-account list \
--resource-group $RESOURCE_GROUP \
--name $APP_NAMEAnalyze costs:
# View cost analysis
az consumption usage list --start-date 2025-10-01 --end-date 2025-10-31- Azure Support: Azure Portal → Support + Troubleshooting
- App Issues: GitHub Issues
- Terraform: Terraform Registry
Azure CLI:
# Delete entire resource group (removes all resources)
az group delete --name $RESOURCE_GROUP --yes --no-waitTerraform:
terraform destroy- Enable CI/CD: Automate deployments from GitHub
- Set up monitoring: Application Insights dashboards
- Implement high availability: Multi-region deployment
- Security audit: Azure Security Center recommendations