diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da71707a6..b020bf601 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,17 +23,3 @@ jobs: run: uv run ruff check - name: Test run: uv run pytest - - docker: - name: Docker - runs-on: ubuntu-latest - needs: test - steps: - - name: Checkout code - uses: actions/checkout@v5 - - name: Build - uses: docker/build-push-action@v6 - with: - push: false - # To be changed - tags: andig/evopt:latest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..3c98f99bf --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,35 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + image_tag: + description: Image tag to deploy + required: false + default: latest + +env: + IMAGE: evcc/optimizer + RESOURCE_GROUP: rg-optimizer-prod + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy infrastructure + uses: azure/cli@v2 + with: + inlineScript: | + az deployment group create \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --template-file infra/main.bicep \ + --parameters \ + containerImage=${{ env.IMAGE }}:${{ inputs.image_tag || 'latest' }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..3894dfa72 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish Docker + +on: + workflow_dispatch: + +env: + IMAGE: evcc/optimizer + +jobs: + publish: + name: Build & Push + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + - name: Setup Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.IMAGE }}:${{ github.sha }} + ${{ env.IMAGE }}:latest diff --git a/Dockerfile b/Dockerfile index 5279716b8..809d9ba38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ FROM python:3.13-slim COPY --from=builder --chown=app:app /app/.venv /app/.venv # Run the application +ENV PYTHONUNBUFFERED=1 ENV OPTIMIZER_TIME_LIMIT=25 ENV OPTIMIZER_NUM_THREADS=1 ENV GUNICORN_CMD_ARGS="--workers 4 --max-requests 32" diff --git a/Makefile b/Makefile index 4ec0e6926..3d0f4630e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -DOCKER_IMAGE := evcc-io/optimizer +DOCKER_IMAGE ?= evcc/optimizer default: build docker-build diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 000000000..280228290 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,112 @@ +@description('Container image to deploy') +param containerImage string = 'evcc/optimizer:latest' + +@description('Azure region for all resources') +param location string = 'germanywestcentral' + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: 'kv-optimizer-prod' + location: location + properties: { + sku: { + family: 'A' + name: 'standard' + } + tenantId: subscription().tenantId + enableRbacAuthorization: true + } +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2025-02-01' = { + name: 'optimizer-logs' + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource containerAppEnv 'Microsoft.App/managedEnvironments@2025-01-01' = { + name: 'optimizer-env' + location: location + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } +} + +resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { + name: 'optimizer' + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + managedEnvironmentId: containerAppEnv.id + configuration: { + ingress: { + external: true + targetPort: 7050 + } + secrets: [ + { + name: 'jwt-token-secret' + keyVaultUrl: '${keyVault.properties.vaultUri}secrets/jwt-token-secret' + identity: 'system' + } + ] + } + template: { + containers: [ + { + name: 'optimizer' + image: containerImage + resources: { + cpu: json('2') + memory: '4Gi' + } + env: [ + { name: 'OPTIMIZER_TIME_LIMIT', value: '25' } + { name: 'OPTIMIZER_NUM_THREADS', value: '1' } + { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32' } + { name: 'JWT_TOKEN_SECRET', secretRef: 'jwt-token-secret' } + ] + probes: [ + { + type: 'startup' + tcpSocket: { + port: 7050 + } + periodSeconds: 5 + failureThreshold: 10 + } + ] + } + ] + scale: { + minReplicas: 1 + maxReplicas: 5 + rules: [ + { + name: 'http-scaling' + http: { + metadata: { + concurrentRequests: '10' + } + } + } + ] + } + } + } +} + + +output fqdn string = containerApp.properties.configuration.ingress.fqdn