From a2112afb3b09622f39e1aa8d78aa3d7c56515383 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 17 Mar 2026 13:53:01 +0100 Subject: [PATCH 01/13] chore: azure deployment --- .github/workflows/ci.yml | 14 ------ .github/workflows/deploy.yml | 49 ++++++++++++++++++ Makefile | 2 +- infra/main.bicep | 97 ++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 infra/main.bicep 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..202a86706 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,49 @@ +name: Deploy + +on: + workflow_dispatch: + +env: + IMAGE: evcc/optimizer + RESOURCE_GROUP: rg-optimizer-prod + +jobs: + docker: + name: Docker + 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: Build and push + uses: docker/build-push-action@v6 + with: + push: true + tags: | + ${{ env.IMAGE }}:${{ github.sha }} + ${{ env.IMAGE }}:latest + + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: docker + + 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 }}:${{ github.sha }} diff --git a/Makefile b/Makefile index 4ec0e6926..fb7079b6f 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..2ec70ef87 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,97 @@ +@description('Container image to deploy') +param containerImage string = 'evcc/optimizer:latest' + +@description('Azure region for all resources') +param location string = 'germanywestcentral' + +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 + properties: { + managedEnvironmentId: containerAppEnv.id + configuration: { + ingress: { + external: true + targetPort: 7050 + } + } + 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' } + ] + probes: [ + { + type: 'liveness' + httpGet: { + path: '/health' + port: 7050 + } + periodSeconds: 30 + failureThreshold: 3 + } + { + type: 'startup' + httpGet: { + path: '/health' + 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 From dbd59ce3fd0945e0a8df99078f17c184d184d06b Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 17 Mar 2026 13:58:28 +0100 Subject: [PATCH 02/13] testing --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 202a86706..91da46ada 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,6 +2,8 @@ name: Deploy on: workflow_dispatch: + push: + branches: [ chore/deployment ] # TODO remove before merge env: IMAGE: evcc/optimizer From eacd365adad97120b851f1cb290918363638b45d Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 17 Mar 2026 14:10:17 +0100 Subject: [PATCH 03/13] health --- infra/main.bicep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 2ec70ef87..515c1ce55 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -58,7 +58,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { { type: 'liveness' httpGet: { - path: '/health' + path: '/optimize/health' port: 7050 } periodSeconds: 30 @@ -67,7 +67,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { { type: 'startup' httpGet: { - path: '/health' + path: '/optimize/health' port: 7050 } periodSeconds: 5 From 455471d0db6b2ec5df20e9b0c8fc3a67611e46c0 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 18 Mar 2026 15:37:59 +0100 Subject: [PATCH 04/13] docker: arm64/amd64 --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 91da46ada..7246c6ea1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,9 +20,13 @@ jobs: 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 }} From 443b2107dfdfeebac42adf934c4373d9934d3b42 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 08:55:59 +0100 Subject: [PATCH 05/13] add access logs --- infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 515c1ce55..f70284cfd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -52,7 +52,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { env: [ { name: 'OPTIMIZER_TIME_LIMIT', value: '25' } { name: 'OPTIMIZER_NUM_THREADS', value: '1' } - { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32' } + { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32 --access-logfile -' } ] probes: [ { From b58f7f6ca416f4ffc91329c18a7090e43c7772aa Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 09:23:45 +0100 Subject: [PATCH 06/13] add secrets --- infra/main.bicep | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infra/main.bicep b/infra/main.bicep index f70284cfd..b3ebb3fa1 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -4,6 +4,19 @@ 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 @@ -32,6 +45,9 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2025-01-01' = { resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { name: 'optimizer' location: location + identity: { + type: 'SystemAssigned' + } properties: { managedEnvironmentId: containerAppEnv.id configuration: { @@ -39,6 +55,13 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { external: true targetPort: 7050 } + secrets: [ + { + name: 'jwt-token-secret' + keyVaultUrl: '${keyVault.properties.vaultUri}secrets/jwt-token-secret' + identity: 'system' + } + ] } template: { containers: [ @@ -53,6 +76,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { { name: 'OPTIMIZER_TIME_LIMIT', value: '25' } { name: 'OPTIMIZER_NUM_THREADS', value: '1' } { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32 --access-logfile -' } + { name: 'JWT_TOKEN_SECRET', secretRef: 'jwt-token-secret' } ] probes: [ { @@ -94,4 +118,15 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { } } +// Key Vault Secrets User role for the Container App's managed identity +resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(keyVault.id, containerApp.id, '4633458b-17de-408a-b874-0445c86b69e6') + scope: keyVault + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') + principalId: containerApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + output fqdn string = containerApp.properties.configuration.ingress.fqdn From 34e0b6d2e237a79ee7b1b2b4fb5ef440595ff8ae Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 09:30:42 +0100 Subject: [PATCH 07/13] add secrets --- infra/main.bicep | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index b3ebb3fa1..ab03f0d96 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -55,14 +55,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { external: true targetPort: 7050 } - secrets: [ - { - name: 'jwt-token-secret' - keyVaultUrl: '${keyVault.properties.vaultUri}secrets/jwt-token-secret' - identity: 'system' - } - ] - } +} template: { containers: [ { @@ -76,7 +69,6 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { { name: 'OPTIMIZER_TIME_LIMIT', value: '25' } { name: 'OPTIMIZER_NUM_THREADS', value: '1' } { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32 --access-logfile -' } - { name: 'JWT_TOKEN_SECRET', secretRef: 'jwt-token-secret' } ] probes: [ { @@ -118,15 +110,5 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { } } -// Key Vault Secrets User role for the Container App's managed identity -resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(keyVault.id, containerApp.id, '4633458b-17de-408a-b874-0445c86b69e6') - scope: keyVault - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') - principalId: containerApp.identity.principalId - principalType: 'ServicePrincipal' - } -} output fqdn string = containerApp.properties.configuration.ingress.fqdn From 5950aa48213ea883898df5c7b817abdfa74140a2 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 09:41:00 +0100 Subject: [PATCH 08/13] add secrets --- infra/main.bicep | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index ab03f0d96..928f0741e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -55,7 +55,14 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { external: true targetPort: 7050 } -} + secrets: [ + { + name: 'jwt-token-secret' + keyVaultUrl: '${keyVault.properties.vaultUri}secrets/jwt-token-secret' + identity: 'system' + } + ] + } template: { containers: [ { @@ -69,6 +76,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { { name: 'OPTIMIZER_TIME_LIMIT', value: '25' } { name: 'OPTIMIZER_NUM_THREADS', value: '1' } { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32 --access-logfile -' } + { name: 'JWT_TOKEN_SECRET', secretRef: 'jwt-token-secret' } ] probes: [ { From 5542a329ab1e22396bdc4b59e4fc5dfee50752da Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 09:55:08 +0100 Subject: [PATCH 09/13] remove access logs --- infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 928f0741e..77953b54d 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -75,7 +75,7 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { env: [ { name: 'OPTIMIZER_TIME_LIMIT', value: '25' } { name: 'OPTIMIZER_NUM_THREADS', value: '1' } - { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32 --access-logfile -' } + { name: 'GUNICORN_CMD_ARGS', value: '--workers 4 --max-requests 32' } { name: 'JWT_TOKEN_SECRET', secretRef: 'jwt-token-secret' } ] probes: [ From c30ff123145bbefa56b183ce552f953b95d2fe5b Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 10:27:49 +0100 Subject: [PATCH 10/13] health + auth --- infra/main.bicep | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 77953b54d..280228290 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -79,19 +79,9 @@ resource containerApp 'Microsoft.App/containerApps@2025-01-01' = { { name: 'JWT_TOKEN_SECRET', secretRef: 'jwt-token-secret' } ] probes: [ - { - type: 'liveness' - httpGet: { - path: '/optimize/health' - port: 7050 - } - periodSeconds: 30 - failureThreshold: 3 - } { type: 'startup' - httpGet: { - path: '/optimize/health' + tcpSocket: { port: 7050 } periodSeconds: 5 From d329867ac56fc239533761989fd3970f9f2819d0 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 20 Mar 2026 10:38:19 +0100 Subject: [PATCH 11/13] dont buffer --- Dockerfile | 1 + 1 file changed, 1 insertion(+) 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" From c9d861dd0d8b499bf68094b98740413194ef715e Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 21 Mar 2026 13:59:49 +0100 Subject: [PATCH 12/13] Update Makefile Co-authored-by: andig --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fb7079b6f..3d0f4630e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -DOCKER_IMAGE := evcc/optimizer +DOCKER_IMAGE ?= evcc/optimizer default: build docker-build From 2f501930f579c7133668ffce04a60e9eb4ab858a Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 21 Mar 2026 16:10:46 +0100 Subject: [PATCH 13/13] manual deploy and plublish action --- .github/workflows/deploy.yml | 32 ++++++-------------------------- .github/workflows/publish.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7246c6ea1..3c98f99bf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,40 +2,20 @@ name: Deploy on: workflow_dispatch: - push: - branches: [ chore/deployment ] # TODO remove before merge + inputs: + image_tag: + description: Image tag to deploy + required: false + default: latest env: IMAGE: evcc/optimizer RESOURCE_GROUP: rg-optimizer-prod jobs: - docker: - name: Docker - 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 - deploy: name: Deploy runs-on: ubuntu-latest - needs: docker steps: - uses: actions/checkout@v6 @@ -52,4 +32,4 @@ jobs: --resource-group ${{ env.RESOURCE_GROUP }} \ --template-file infra/main.bicep \ --parameters \ - containerImage=${{ env.IMAGE }}:${{ github.sha }} + 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