diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index dc223927..f1131ccc 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -7,7 +7,7 @@ on: branches: - master env: - VERSION: 10.0.3 + VERSION: 10.0.4 jobs: build-and-deploy: runs-on: ubuntu-latest diff --git a/Api.Data.Audit/.github/workflows/build-and-deploy.yml b/Api.Data.Audit/.github/workflows/build-and-deploy.yml index d5c6262c..991e5700 100644 --- a/Api.Data.Audit/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Audit/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Audit/.kubernetes/configmap.yaml b/Api.Data.Audit/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Audit/.kubernetes/configmap.yaml +++ b/Api.Data.Audit/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml b/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml index 7ebe9759..8fa1f5e6 100644 --- a/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml +++ b/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.EntityEvents/.kubernetes/configmap.yaml b/Api.Data.EntityEvents/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.EntityEvents/.kubernetes/configmap.yaml +++ b/Api.Data.EntityEvents/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml b/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml index 66e9d4cd..9418258e 100644 --- a/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml @@ -36,10 +36,8 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} AUTH_API_KEY_SECRET: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_API_KEY_SECRET || secrets.STAGING_AUTH_API_KEY_SECRET }} @@ -127,59 +125,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Identity.Auth.ApiKey/.kubernetes/configmap.yaml b/Api.Data.Identity.Auth.ApiKey/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Identity.Auth.ApiKey/.kubernetes/configmap.yaml +++ b/Api.Data.Identity.Auth.ApiKey/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml b/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml index 31fc303f..a361cd31 100644 --- a/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml @@ -36,10 +36,8 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} @@ -126,59 +124,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Identity.Auth.External.Custom/.kubernetes/configmap.yaml b/Api.Data.Identity.Auth.External.Custom/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Identity.Auth.External.Custom/.kubernetes/configmap.yaml +++ b/Api.Data.Identity.Auth.External.Custom/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml b/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml index 0975693f..a5b610e1 100644 --- a/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml @@ -36,10 +36,8 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} @@ -126,59 +124,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Identity.Auth.Jwt/.kubernetes/configmap.yaml b/Api.Data.Identity.Auth.Jwt/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Identity.Auth.Jwt/.kubernetes/configmap.yaml +++ b/Api.Data.Identity.Auth.Jwt/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.Identity/.github/workflows/build-and-deploy.yml b/Api.Data.Identity/.github/workflows/build-and-deploy.yml index 2ab2015a..149b5221 100644 --- a/Api.Data.Identity/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Identity/.kubernetes/configmap.yaml b/Api.Data.Identity/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Identity/.kubernetes/configmap.yaml +++ b/Api.Data.Identity/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml b/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml index 93b4c88f..c3992db2 100644 --- a/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml +++ b/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.LazyLoading/.kubernetes/configmap.yaml b/Api.Data.LazyLoading/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.LazyLoading/.kubernetes/configmap.yaml +++ b/Api.Data.LazyLoading/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml index 32d175e6..42f8f0a9 100644 --- a/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: runs-on: @@ -121,59 +119,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.MySql.Collation/.kubernetes/configmap.yaml b/Api.Data.MySql.Collation/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.MySql.Collation/.kubernetes/configmap.yaml +++ b/Api.Data.MySql.Collation/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml index eb341f86..47f6c15a 100644 --- a/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.MySql.Mappings/.kubernetes/configmap.yaml b/Api.Data.MySql.Mappings/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.MySql.Mappings/.kubernetes/configmap.yaml +++ b/Api.Data.MySql.Mappings/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml index 5dc3a3d4..a749a5c1 100644 --- a/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.MySql.Spatial/.kubernetes/configmap.yaml b/Api.Data.MySql.Spatial/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.MySql.Spatial/.kubernetes/configmap.yaml +++ b/Api.Data.MySql.Spatial/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml index 32bb90c6..98f4011e 100644 --- a/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.MySql.StoredProcedures/.kubernetes/configmap.yaml b/Api.Data.MySql.StoredProcedures/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.MySql.StoredProcedures/.kubernetes/configmap.yaml +++ b/Api.Data.MySql.StoredProcedures/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml index 7668a2b9..94e66641 100644 --- a/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.MySql.Views/.kubernetes/configmap.yaml b/Api.Data.MySql.Views/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.MySql.Views/.kubernetes/configmap.yaml +++ b/Api.Data.MySql.Views/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql/.github/workflows/build-and-deploy.yml b/Api.Data.MySql/.github/workflows/build-and-deploy.yml index 9cd43178..827db764 100644 --- a/Api.Data.MySql/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.MySql/.kubernetes/configmap.yaml b/Api.Data.MySql/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.MySql/.kubernetes/configmap.yaml +++ b/Api.Data.MySql/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.MySql/Api.Data.MySql/appsettings.json b/Api.Data.MySql/Api.Data.MySql/appsettings.json index 2871c661..e8baa747 100644 --- a/Api.Data.MySql/Api.Data.MySql/appsettings.json +++ b/Api.Data.MySql/Api.Data.MySql/appsettings.json @@ -26,6 +26,7 @@ "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Api.Data.MySql/README.md b/Api.Data.MySql/README.md index 3ef411a2..abcc8913 100644 --- a/Api.Data.MySql/README.md +++ b/Api.Data.MySql/README.md @@ -80,6 +80,7 @@ Configured the application with the necessary data setup. "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 @@ -140,6 +141,13 @@ spec: key: data-connectionstring ``` +Also added the following variable to the `configmap.yaml`. + +```yaml +data: + Data__AuthenticationType: %SQL_AUTH_TYPE% +``` + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. @@ -147,23 +155,64 @@ Add the following environment variables to the `buid-and-deply.yml`. env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` -Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. +Additionally, these steps has been added to ensure database migrations are applied and the application database user is created, using the application's managed identity, before the +application is deployed. ```yaml -- name: Database Migration & User +- name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + +- name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` --no-build ` @@ -176,39 +225,30 @@ Additionally, this step has been added to ensure database migrations are applied { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; -``` + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; -Last, before applying the new Kubernetes templates, these environmental variables must be set. + if ($LastExitCode -ne 0) + { + throw "error"; + }; -```powershell -$env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; ``` Finally, apply the templates. diff --git a/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml b/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml index 8337fcc2..fd974db3 100644 --- a/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml +++ b/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-postgres-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,66 +122,123 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity shell: pwsh run: | - $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "5432"; - $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; - $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:DATA__CONNECTIONSTRING = $env:SQL_MIGRATION_CONNECTIONSTRING; + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; - & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; if ($LastExitCode -ne 0) - { + { throw "error"; }; - - apt-get update - apt-get install -y postgresql-client + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; - $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + - name: PostgreSQL Database Migration + shell: pwsh + run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 5432; + $env:SQL_SERVER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az postgres flexible-server microsoft-entra-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].principalName" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; + + $env:DATA__CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_TOKEN;SSL Mode=Require;Trust Server Certificate=true"; + + & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; - if ($userExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" - } + throw "error"; + }; + + $env:PRINCIPAL_SQL_PATH = "app-database-principal.sql"; + $env:GRANTS_SQL_PATH = "app-database-grants.sql"; + + $principalSql = @" + DO `$`$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$env:IDENTITY_NAME') THEN + PERFORM pgaadauth_create_principal('$env:IDENTITY_NAME', false, false); + END IF; + END + `$`$; + "@; + + $principalSql | Set-Content $env:PRINCIPAL_SQL_PATH; - $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d postgres ` + --file-path $env:PRINCIPAL_SQL_PATH; - if ($userDbExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" + throw "error"; + }; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" + $grantsSql = @" + GRANT CONNECT ON DATABASE "$env:SQL_NAME" TO "$env:IDENTITY_NAME"; + GRANT USAGE ON SCHEMA public TO "$env:IDENTITY_NAME"; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "$env:IDENTITY_NAME"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$env:IDENTITY_NAME"; + "@; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" + $grantsSql | Set-Content $env:GRANTS_SQL_PATH; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" - } + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d $env:SQL_NAME ` + --file-path $env:GRANTS_SQL_PATH; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:IDENTITY_NAME;SSL Mode=Require;Trust Server Certificate=true"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.PostgreSQL.Spatial/.kubernetes/configmap.yaml b/Api.Data.PostgreSQL.Spatial/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.PostgreSQL.Spatial/.kubernetes/configmap.yaml +++ b/Api.Data.PostgreSQL.Spatial/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml b/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml index 1c3f79d0..8014a890 100644 --- a/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml +++ b/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-postgres-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,66 +122,123 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity shell: pwsh run: | - $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "5432"; - $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; - $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"; + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:DATA__CONNECTIONSTRING = $env:SQL_MIGRATION_CONNECTIONSTRING; + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; - & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; if ($LastExitCode -ne 0) - { + { throw "error"; }; - - apt-get update - apt-get install -y postgresql-client + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; - $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + - name: PostgreSQL Database Migration + shell: pwsh + run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 5432; + $env:SQL_SERVER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az postgres flexible-server microsoft-entra-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].principalName" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; + + $env:DATA__CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_TOKEN;SSL Mode=Require;Trust Server Certificate=true"; + + & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; - if ($userExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" - } + throw "error"; + }; + + $env:PRINCIPAL_SQL_PATH = "app-database-principal.sql"; + $env:GRANTS_SQL_PATH = "app-database-grants.sql"; + + $principalSql = @" + DO `$`$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$env:IDENTITY_NAME') THEN + PERFORM pgaadauth_create_principal('$env:IDENTITY_NAME', false, false); + END IF; + END + `$`$; + "@; + + $principalSql | Set-Content $env:PRINCIPAL_SQL_PATH; - $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d postgres ` + --file-path $env:PRINCIPAL_SQL_PATH; - if ($userDbExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" + throw "error"; + }; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" + $grantsSql = @" + GRANT CONNECT ON DATABASE "$env:SQL_NAME" TO "$env:IDENTITY_NAME"; + GRANT USAGE ON SCHEMA public TO "$env:IDENTITY_NAME"; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "$env:IDENTITY_NAME"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$env:IDENTITY_NAME"; + "@; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" + $grantsSql | Set-Content $env:GRANTS_SQL_PATH; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" - } + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d $env:SQL_NAME ` + --file-path $env:GRANTS_SQL_PATH; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:IDENTITY_NAME;SSL Mode=Require;Trust Server Certificate=true"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.PostgreSQL/.kubernetes/configmap.yaml b/Api.Data.PostgreSQL/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.PostgreSQL/.kubernetes/configmap.yaml +++ b/Api.Data.PostgreSQL/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json b/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json index 2871c661..e8baa747 100644 --- a/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json +++ b/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json @@ -26,6 +26,7 @@ "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Api.Data.PostgreSQL/README.md b/Api.Data.PostgreSQL/README.md index e32673af..8e6bc1c7 100644 --- a/Api.Data.PostgreSQL/README.md +++ b/Api.Data.PostgreSQL/README.md @@ -80,6 +80,7 @@ Configured the application with the necessary data setup. "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 @@ -138,6 +139,13 @@ spec: key: data-connectionstring ``` +Also added the following variable to the `configmap.yaml`. + +```yaml +data: + Data__AuthenticationType: %SQL_AUTH_TYPE% +``` + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. @@ -145,72 +153,127 @@ Add the following environment variables to the `buid-and-deply.yml`. env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-postgres-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` -Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. +Additionally, these steps has been added to ensure database migrations are applied and the application database user is created, using the application's managed identity, before the +application is deployed. ```yaml -- name: Database Migration & User +- name: Managed Identity shell: pwsh run: | - $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "5432"; - $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; - $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:DATA__CONNECTIONSTRING = $env:SQL_MIGRATION_CONNECTIONSTRING; + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; - & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; if ($LastExitCode -ne 0) - { + { throw "error"; }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; - $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" +- name: PostgreSQL Database Migration + shell: pwsh + run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 5432; + $env:SQL_SERVER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az postgres flexible-server microsoft-entra-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].principalName" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - if ($userExists -ne "1") - { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" - } + $env:DATA__CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_TOKEN;SSL Mode=Require;Trust Server Certificate=true"; - $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; - if ($userDbExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" + throw "error"; + }; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" + $env:PRINCIPAL_SQL_PATH = "app-database-principal.sql"; + $env:GRANTS_SQL_PATH = "app-database-grants.sql"; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" + $principalSql = @" + DO `$`$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$env:IDENTITY_NAME') THEN + PERFORM pgaadauth_create_principal('$env:IDENTITY_NAME', false, false); + END IF; + END + `$`$; + "@; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" - } + $principalSql | Set-Content $env:PRINCIPAL_SQL_PATH; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; -``` + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d postgres ` + --file-path $env:PRINCIPAL_SQL_PATH; -Last, before applying the new Kubernetes templates, these environmental variables must be set. + if ($LastExitCode -ne 0) + { + throw "error"; + }; -```powershell -$env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"; + $grantsSql = @" + GRANT CONNECT ON DATABASE "$env:SQL_NAME" TO "$env:IDENTITY_NAME"; + GRANT USAGE ON SCHEMA public TO "$env:IDENTITY_NAME"; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "$env:IDENTITY_NAME"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$env:IDENTITY_NAME"; + "@; + + $grantsSql | Set-Content $env:GRANTS_SQL_PATH; + + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d $env:SQL_NAME ` + --file-path $env:GRANTS_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:IDENTITY_NAME;SSL Mode=Require;Trust Server Certificate=true"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; ``` Finally, apply the templates. diff --git a/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml b/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml index 531a6e7f..c0e97946 100644 --- a/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Repository.AutoSave/.kubernetes/configmap.yaml b/Api.Data.Repository.AutoSave/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Repository.AutoSave/.kubernetes/configmap.yaml +++ b/Api.Data.Repository.AutoSave/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml b/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml index 0ce35b2f..ab0e96f3 100644 --- a/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Repository.Includes/.kubernetes/configmap.yaml b/Api.Data.Repository.Includes/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Repository.Includes/.kubernetes/configmap.yaml +++ b/Api.Data.Repository.Includes/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml b/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml index 8de44e0c..64106ab7 100644 --- a/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.SoftDelete/.kubernetes/configmap.yaml b/Api.Data.SoftDelete/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.SoftDelete/.kubernetes/configmap.yaml +++ b/Api.Data.SoftDelete/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.SqlServer.Spatial/.azure/.diagnostic-settings/logs.json b/Api.Data.SqlServer.Spatial/.azure/.diagnostic-settings/logs.json deleted file mode 100644 index 10c3505a..00000000 --- a/Api.Data.SqlServer.Spatial/.azure/.diagnostic-settings/logs.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "category": "QueryStoreRuntimeStatistics", - "enabled": true - }, - { - "category": "SQLSecurityAuditEvents", - "enabled": true - } -] \ No newline at end of file diff --git a/Api.Data.SqlServer.Spatial/.azure/.diagnostic-settings/metrics.json b/Api.Data.SqlServer.Spatial/.azure/.diagnostic-settings/metrics.json deleted file mode 100644 index 551d2f13..00000000 --- a/Api.Data.SqlServer.Spatial/.azure/.diagnostic-settings/metrics.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "category": "AllMetrics", - "enabled": true, - "timeGrain": "PT1M" - } -] \ No newline at end of file diff --git a/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml b/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml index 40473d5f..26260c6f 100644 --- a/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml @@ -17,7 +17,7 @@ env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_GROUP_DELIVERY: ${{ vars.AZURE_RESOURCE_GROUP_DELIVERY }} - AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + AZURE_GROUP_DATABASE: ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} AZURE_GROUP_LOGS: ${{ vars.AZURE_RESOURCE_GROUP_LOGS }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -42,10 +42,8 @@ env: SQL_EDITION: GeneralPurpose SQL_MAX_SIZE: 64GB SQL_BACKUP_RETENTION: 35 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-sqlserver-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -129,7 +127,47 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Create Database + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: SQL Server Create Database shell: pwsh run: | $env:SQL_SERVER_NAME = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].name" -o tsv; @@ -146,8 +184,8 @@ jobs: --max-size $env:SQL_MAX_SIZE ` --backup-storage-redundancy Geo ` --zone-redundant true; - - $env:MAINTENANCE_CONFIG_ID = az maintenance public-configuration list --query "[?name=='SQL_Default_1'].id" -o tsv; + + $env:MAINTENANCE_CONFIG_ID = "/subscriptions/$env:AZURE_SUBSCRIPTION_ID/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_Default"; az sql db update ` -n $env:SQL_NAME ` @@ -156,15 +194,36 @@ jobs: --maint-config-id $env:MAINTENANCE_CONFIG_ID; $env:DIAGNOSTIC_SETTINGS_NAME = "diagnostics-" + $env:SQL_NAME; + $env:SQL_LOGS_PATH = "sql-diagnostic-logs.json"; + $env:SQL_METRICS_PATH = "sql-diagnostic-metrics.json"; $env:WORKSPACE_ID = az monitor log-analytics workspace list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; $env:SQLDB_ID = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query id -o tsv; + $logsJson = @" + [ + { "category": "QueryStoreRuntimeStatistics", "enabled": true }, + { "category": "SQLSecurityAuditEvents", "enabled": true } + ] + "@; + + $logsJson | Set-Content $env:SQL_LOGS_PATH; + + $metricsJson = @" + [ + { "category": "Basic", "enabled": true }, + { "category": "InstanceAndAppAdvanced", "enabled": true }, + { "category": "WorkloadManagement", "enabled": true } + ] + "@; + + $metricsJson | Set-Content $env:SQL_METRICS_PATH; + az monitor diagnostic-settings create ` --name $env:DIAGNOSTIC_SETTINGS_NAME ` --resource $env:SQLDB_ID ` --workspace $env:WORKSPACE_ID ` - --logs '@.azure/.diagnostic-settings/logs.json' ` - --metrics '@.azure/.diagnostic-settings/metrics.json'; + --logs "@$env:SQL_LOGS_PATH" ` + --metrics "@$env:SQL_METRICS_PATH"; $env:ACTION_GROUP = az monitor action-group list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; @@ -180,7 +239,7 @@ jobs: --description "Alert when CPU usage is above 80% for 5 minutes."; az monitor metrics alert create ` - --name "High Memory/Worker Usage" ` + --name "High Memory And Worker Usage" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` --condition "avg workers_percent > 80" ` @@ -205,12 +264,12 @@ jobs: --name "High Storage IO" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` - --condition "avg io_consumption_percent > 80" ` + --condition "avg physical_data_read_percent > 80" ` --window-size PT5M ` --evaluation-frequency PT1M ` --action $env:ACTION_GROUP ` --severity 2 ` - --description "Alert when Storage IO consumption is above 80% for 5 minutes."; + --description "Alert when data IO usage is above 80% for 5 minutes."; az monitor metrics alert create ` --name "High Storage Percent" ` @@ -235,76 +294,73 @@ jobs: }; }; - - name: Database Migration & User + - name: SQL Server Database Migration shell: pwsh run: | - $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "1433" - $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 1433; + $env:SQL_SERVER = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;Authentication=Active Directory Service Principal;User Id=$env:AZURE_CLIENT_ID;Password=$env:AZURE_CLIENT_SECRET;Encrypt=True;TrustServerCertificate=True;"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - apt-get update - apt-get install -y mssql-tools unixodbc-dev - - $loginExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" - - if ($loginExists -eq 0) - { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" - }; - $userExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + $env:APP_USER_SQL_PATH = "app-database-user.sql"; + + $sql = @" + IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$env:IDENTITY_NAME') + BEGIN + CREATE USER [$env:IDENTITY_NAME] FROM EXTERNAL PROVIDER; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datareader' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datareader ADD MEMBER [$env:IDENTITY_NAME]; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datawriter' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datawriter ADD MEMBER [$env:IDENTITY_NAME]; + END + "@; + + $sql | Set-Content $env:APP_USER_SQL_PATH; + + $env:SQL_TOKEN = az account get-access-token --resource "https://database.windows.net/" --query accessToken -o tsv; + + Invoke-Sqlcmd ` + -ServerInstance $env:SQL_HOST ` + -Database $env:SQL_NAME ` + -AccessToken $env:SQL_TOKEN ` + -InputFile $env:APP_USER_SQL_PATH; - if ($userExists -eq 0) + if ($LastExitCode -ne 0) { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + throw "error"; }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:IDENTITY_CLIENT_ID;Encrypt=True;TrustServerCertificate=True;"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.SqlServer.Spatial/.kubernetes/configmap.yaml b/Api.Data.SqlServer.Spatial/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.SqlServer.Spatial/.kubernetes/configmap.yaml +++ b/Api.Data.SqlServer.Spatial/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.SqlServer/.azure/.diagnostic-settings/logs.json b/Api.Data.SqlServer/.azure/.diagnostic-settings/logs.json deleted file mode 100644 index 10c3505a..00000000 --- a/Api.Data.SqlServer/.azure/.diagnostic-settings/logs.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "category": "QueryStoreRuntimeStatistics", - "enabled": true - }, - { - "category": "SQLSecurityAuditEvents", - "enabled": true - } -] \ No newline at end of file diff --git a/Api.Data.SqlServer/.azure/.diagnostic-settings/metrics.json b/Api.Data.SqlServer/.azure/.diagnostic-settings/metrics.json deleted file mode 100644 index 551d2f13..00000000 --- a/Api.Data.SqlServer/.azure/.diagnostic-settings/metrics.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "category": "AllMetrics", - "enabled": true, - "timeGrain": "PT1M" - } -] \ No newline at end of file diff --git a/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml b/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml index 1fde61c9..c171114c 100644 --- a/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml @@ -17,7 +17,7 @@ env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_GROUP_DELIVERY: ${{ vars.AZURE_RESOURCE_GROUP_DELIVERY }} - AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + AZURE_GROUP_DATABASE: ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} AZURE_GROUP_LOGS: ${{ vars.AZURE_RESOURCE_GROUP_LOGS }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -42,10 +42,8 @@ env: SQL_EDITION: GeneralPurpose SQL_MAX_SIZE: 64GB SQL_BACKUP_RETENTION: 35 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-sqlserver-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -129,7 +127,47 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Create Database + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: SQL Server Create Database shell: pwsh run: | $env:SQL_SERVER_NAME = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].name" -o tsv; @@ -146,8 +184,8 @@ jobs: --max-size $env:SQL_MAX_SIZE ` --backup-storage-redundancy Geo ` --zone-redundant true; - - $env:MAINTENANCE_CONFIG_ID = az maintenance public-configuration list --query "[?name=='SQL_Default_1'].id" -o tsv; + + $env:MAINTENANCE_CONFIG_ID = "/subscriptions/$env:AZURE_SUBSCRIPTION_ID/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_Default"; az sql db update ` -n $env:SQL_NAME ` @@ -156,15 +194,36 @@ jobs: --maint-config-id $env:MAINTENANCE_CONFIG_ID; $env:DIAGNOSTIC_SETTINGS_NAME = "diagnostics-" + $env:SQL_NAME; + $env:SQL_LOGS_PATH = "sql-diagnostic-logs.json"; + $env:SQL_METRICS_PATH = "sql-diagnostic-metrics.json"; $env:WORKSPACE_ID = az monitor log-analytics workspace list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; $env:SQLDB_ID = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query id -o tsv; + $logsJson = @" + [ + { "category": "QueryStoreRuntimeStatistics", "enabled": true }, + { "category": "SQLSecurityAuditEvents", "enabled": true } + ] + "@; + + $logsJson | Set-Content $env:SQL_LOGS_PATH; + + $metricsJson = @" + [ + { "category": "Basic", "enabled": true }, + { "category": "InstanceAndAppAdvanced", "enabled": true }, + { "category": "WorkloadManagement", "enabled": true } + ] + "@; + + $metricsJson | Set-Content $env:SQL_METRICS_PATH; + az monitor diagnostic-settings create ` --name $env:DIAGNOSTIC_SETTINGS_NAME ` --resource $env:SQLDB_ID ` --workspace $env:WORKSPACE_ID ` - --logs '@.azure/.diagnostic-settings/logs.json' ` - --metrics '@.azure/.diagnostic-settings/metrics.json'; + --logs "@$env:SQL_LOGS_PATH" ` + --metrics "@$env:SQL_METRICS_PATH"; $env:ACTION_GROUP = az monitor action-group list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; @@ -180,7 +239,7 @@ jobs: --description "Alert when CPU usage is above 80% for 5 minutes."; az monitor metrics alert create ` - --name "High Memory/Worker Usage" ` + --name "High Memory And Worker Usage" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` --condition "avg workers_percent > 80" ` @@ -205,12 +264,12 @@ jobs: --name "High Storage IO" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` - --condition "avg io_consumption_percent > 80" ` + --condition "avg physical_data_read_percent > 80" ` --window-size PT5M ` --evaluation-frequency PT1M ` --action $env:ACTION_GROUP ` --severity 2 ` - --description "Alert when Storage IO consumption is above 80% for 5 minutes."; + --description "Alert when data IO usage is above 80% for 5 minutes."; az monitor metrics alert create ` --name "High Storage Percent" ` @@ -235,76 +294,73 @@ jobs: }; }; - - name: Database Migration & User + - name: SQL Server Database Migration shell: pwsh run: | - $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "1433" - $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 1433; + $env:SQL_SERVER = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;Authentication=Active Directory Service Principal;User Id=$env:AZURE_CLIENT_ID;Password=$env:AZURE_CLIENT_SECRET;Encrypt=True;TrustServerCertificate=True;"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - apt-get update - apt-get install -y mssql-tools unixodbc-dev - - $loginExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" - - if ($loginExists -eq 0) - { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" - }; - $userExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + $env:APP_USER_SQL_PATH = "app-database-user.sql"; + + $sql = @" + IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$env:IDENTITY_NAME') + BEGIN + CREATE USER [$env:IDENTITY_NAME] FROM EXTERNAL PROVIDER; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datareader' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datareader ADD MEMBER [$env:IDENTITY_NAME]; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datawriter' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datawriter ADD MEMBER [$env:IDENTITY_NAME]; + END + "@; + + $sql | Set-Content $env:APP_USER_SQL_PATH; + + $env:SQL_TOKEN = az account get-access-token --resource "https://database.windows.net/" --query accessToken -o tsv; + + Invoke-Sqlcmd ` + -ServerInstance $env:SQL_HOST ` + -Database $env:SQL_NAME ` + -AccessToken $env:SQL_TOKEN ` + -InputFile $env:APP_USER_SQL_PATH; - if ($userExists -eq 0) + if ($LastExitCode -ne 0) { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + throw "error"; }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:IDENTITY_CLIENT_ID;Encrypt=True;TrustServerCertificate=True;"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.SqlServer/.kubernetes/configmap.yaml b/Api.Data.SqlServer/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.SqlServer/.kubernetes/configmap.yaml +++ b/Api.Data.SqlServer/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json b/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json index 2871c661..e8baa747 100644 --- a/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json +++ b/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json @@ -26,6 +26,7 @@ "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Api.Data.SqlServer/README.md b/Api.Data.SqlServer/README.md index 115a225f..00ce9a53 100644 --- a/Api.Data.SqlServer/README.md +++ b/Api.Data.SqlServer/README.md @@ -80,6 +80,7 @@ Configured the application with the necessary data setup. "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 @@ -138,31 +139,80 @@ spec: key: data-connectionstring ``` +Also added the following variable to the `configmap.yaml`. + +```yaml +data: + Data__AuthenticationType: %SQL_AUTH_TYPE% +``` + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: DOTNET_EF_TOOLS_VERSION: "10.0" - AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + AZURE_GROUP_DATABASE: ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} AZURE_GROUP_LOGS: ${{ vars.AZURE_RESOURCE_GROUP_LOGS }} SQL_EDITION: GeneralPurpose SQL_SERVICE_OBJECTIVE: GP_Gen5_2 SQL_MAX_SIZE: 64GB SQL_BACKUP_RETENTION: 35 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-sqlserver-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` -Additionally, two steps have been added: one to create the application's database if it doesn't already exist, and one to ensure database migrations are applied and the application -database user has been created before the application is deployed. +Additionally, these steps has been added to ensure the database exists, migrations are applied, and the application database user is created (using the application's managed identity) before +the application is deployed. ```yaml -- name: Create Database +- name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + +- name: SQL Server Create Database shell: pwsh run: | + $env:SQL_SERVICE_OBJECTIVE = "GP_Gen5_2"; + $env:SQL_EDITION = "GeneralPurpose"; + $env:SQL_MAX_SIZE = "64GB"; + $env:SQL_BACKUP_RETENTION = "35" $env:SQL_SERVER_NAME = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].name" -o tsv; $env:SQL_DB_EXISTS = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query name -o tsv 2>$null; @@ -177,8 +227,8 @@ database user has been created before the application is deployed. --max-size $env:SQL_MAX_SIZE ` --backup-storage-redundancy Geo ` --zone-redundant true; - - $env:MAINTENANCE_CONFIG_ID = az maintenance public-configuration list --query "[?name=='SQL_Default_1'].id" -o tsv; + + $env:MAINTENANCE_CONFIG_ID = "/subscriptions/$env:AZURE_SUBSCRIPTION_ID/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_Default"; az sql db update ` -n $env:SQL_NAME ` @@ -187,15 +237,36 @@ database user has been created before the application is deployed. --maint-config-id $env:MAINTENANCE_CONFIG_ID; $env:DIAGNOSTIC_SETTINGS_NAME = "diagnostics-" + $env:SQL_NAME; + $env:SQL_LOGS_PATH = "sql-diagnostic-logs.json"; + $env:SQL_METRICS_PATH = "sql-diagnostic-metrics.json"; $env:WORKSPACE_ID = az monitor log-analytics workspace list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; $env:SQLDB_ID = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query id -o tsv; + $logsJson = @" + [ + { "category": "QueryStoreRuntimeStatistics", "enabled": true }, + { "category": "SQLSecurityAuditEvents", "enabled": true } + ] + "@; + + $logsJson | Set-Content $env:SQL_LOGS_PATH; + + $metricsJson = @" + [ + { "category": "Basic", "enabled": true }, + { "category": "InstanceAndAppAdvanced", "enabled": true }, + { "category": "WorkloadManagement", "enabled": true } + ] + "@; + + $metricsJson | Set-Content $env:SQL_METRICS_PATH; + az monitor diagnostic-settings create ` --name $env:DIAGNOSTIC_SETTINGS_NAME ` --resource $env:SQLDB_ID ` --workspace $env:WORKSPACE_ID ` - --logs '@.azure/.diagnostic-settings/logs.json' ` - --metrics '@.azure/.diagnostic-settings/metrics.json'; + --logs "@$env:SQL_LOGS_PATH" ` + --metrics "@$env:SQL_METRICS_PATH"; $env:ACTION_GROUP = az monitor action-group list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; @@ -211,7 +282,7 @@ database user has been created before the application is deployed. --description "Alert when CPU usage is above 80% for 5 minutes."; az monitor metrics alert create ` - --name "High Memory/Worker Usage" ` + --name "High Memory And Worker Usage" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` --condition "avg workers_percent > 80" ` @@ -236,12 +307,12 @@ database user has been created before the application is deployed. --name "High Storage IO" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` - --condition "avg io_consumption_percent > 80" ` + --condition "avg physical_data_read_percent > 80" ` --window-size PT5M ` --evaluation-frequency PT1M ` --action $env:ACTION_GROUP ` --severity 2 ` - --description "Alert when Storage IO consumption is above 80% for 5 minutes."; + --description "Alert when data IO usage is above 80% for 5 minutes."; az monitor metrics alert create ` --name "High Storage Percent" ` @@ -265,79 +336,70 @@ database user has been created before the application is deployed. throw "error"; }; }; -``` -```yaml -- name: Database Migration & User +- name: SQL Server Database Migration shell: pwsh run: | - $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "1433" - $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 1433; + $env:SQL_SERVER = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;Authentication=Active Directory Service Principal;User Id=$env:AZURE_CLIENT_ID;Password=$env:AZURE_CLIENT_SECRET;Encrypt=True;TrustServerCertificate=True;"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - apt-get update - apt-get install -y mssql-tools unixodbc-dev - - $loginExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" - - if ($loginExists -eq 0) - { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" - }; - $userExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + $env:APP_USER_SQL_PATH = "app-database-user.sql"; + + $sql = @" + IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$env:IDENTITY_NAME') + BEGIN + CREATE USER [$env:IDENTITY_NAME] FROM EXTERNAL PROVIDER; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datareader' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datareader ADD MEMBER [$env:IDENTITY_NAME]; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datawriter' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datawriter ADD MEMBER [$env:IDENTITY_NAME]; + END + "@; + + $sql | Set-Content $env:APP_USER_SQL_PATH; + + $env:SQL_TOKEN = az account get-access-token --resource "https://database.windows.net/" --query accessToken -o tsv; + + Invoke-Sqlcmd ` + -ServerInstance $env:SQL_HOST ` + -Database $env:SQL_NAME ` + -AccessToken $env:SQL_TOKEN ` + -InputFile $env:APP_USER_SQL_PATH; - if ($userExists -eq 0) + if ($LastExitCode -ne 0) { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + throw "error"; }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; -``` - -Last, before applying the new Kubernetes templates, these environmental variables must be set. - -```powershell -$env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:IDENTITY_CLIENT_ID;Encrypt=True;TrustServerCertificate=True;"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; ``` Finally, the templates can be applied. diff --git a/Api.Data.Triggers/.github/workflows/build-and-deploy.yml b/Api.Data.Triggers/.github/workflows/build-and-deploy.yml index ba28c013..0cd03615 100644 --- a/Api.Data.Triggers/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Triggers/.github/workflows/build-and-deploy.yml @@ -37,10 +37,8 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -124,59 +122,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Api.Data.Triggers/.kubernetes/configmap.yaml b/Api.Data.Triggers/.kubernetes/configmap.yaml index d13013d1..0209afca 100644 --- a/Api.Data.Triggers/.kubernetes/configmap.yaml +++ b/Api.Data.Triggers/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Storage.Azure/.github/workflows/build-and-deploy.yml b/Api.Storage.Azure/.github/workflows/build-and-deploy.yml index 47920047..18e074c7 100644 --- a/Api.Storage.Azure/.github/workflows/build-and-deploy.yml +++ b/Api.Storage.Azure/.github/workflows/build-and-deploy.yml @@ -119,6 +119,62 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: Storage Role Permissions + shell: pwsh + run: | + $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; + + az role assignment create ` + --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` + --assignee-principal-type ServicePrincipal ` + --role "Storage File Data SMB MI Admin" ` + --scope $env:STORAGE_ACCOUNT_ID + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + - name: Create Fileshare shell: pwsh run: | @@ -168,54 +224,6 @@ jobs: throw "error"; }; - - name: Managed Identity & Federated Credentials - shell: pwsh - run: | - $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; - - if (-not $env:IDENTITY_PRINCIPAL_ID) - { - az identity create ` - -g $env:AZURE_GROUP_KUBERNETES ` - -n $env:IDENTITY_NAME; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - } - - az role assignment create ` - --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` - --assignee-principal-type ServicePrincipal ` - --role "Storage File Data SMB MI Admin" ` - --scope $env:STORAGE_ACCOUNT_ID - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - az identity federated-credential create ` - --name $env:SERVICE_NAME-credentials ` - --resource-group $env:AZURE_GROUP_KUBERNETES ` - --identity-name $env:IDENTITY_NAME ` - --issuer $env:KUBERNETES_ISSUER_URL ` - --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` - --audience api://AzureADTokenExchange; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; - - name: Kubernetes Deploy shell: pwsh run: | diff --git a/Api.Storage.Azure/README.md b/Api.Storage.Azure/README.md index 0e3cdc96..7331c81c 100644 --- a/Api.Storage.Azure/README.md +++ b/Api.Storage.Azure/README.md @@ -183,7 +183,67 @@ env: STORAGE_SHARE_NAME: nano-storage-azure ``` -Additionally, this step has been added to ensure the file share is created before the application is deployed. +Additionally, include these steps to create a managed identity and federated credentials for authenticating with the storage account. + +``` +- name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + +- name: Storage Role Permissions + shell: pwsh + run: | + $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; + + az role assignment create ` + --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` + --assignee-principal-type ServicePrincipal ` + --role "Storage File Data SMB MI Admin" ` + --scope $env:STORAGE_ACCOUNT_ID + + if ($LastExitCode -ne 0) + { + throw "error"; + }; +``` + +..and this step has been added to ensure the file share is created before the application is deployed. ```yaml - name: Create Fileshare @@ -236,62 +296,9 @@ Additionally, this step has been added to ensure the file share is created befor }; ``` -...and includes a step to create a managed identity and federated credentials for authenticating with the storage account. - -``` -- name: Managed Identity & Federated Credentials - shell: pwsh - run: | - $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; - - if (-not $env:IDENTITY_PRINCIPAL_ID) - { - az identity create ` - -g $env:AZURE_GROUP_KUBERNETES ` - -n $env:IDENTITY_NAME; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - } - - az role assignment create ` - --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` - --assignee-principal-type ServicePrincipal ` - --role "Storage File Data SMB MI Admin" ` - --scope $env:STORAGE_ACCOUNT_ID - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - az identity federated-credential create ` - --name $env:SERVICE_NAME-credentials ` - --resource-group $env:AZURE_GROUP_KUBERNETES ` - --identity-name $env:IDENTITY_NAME ` - --issuer $env:KUBERNETES_ISSUER_URL ` - --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` - --audience api://AzureADTokenExchange; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; -``` - Last, during the Kubernetes deployment step, before any resources are applied, environmental variables required for the new `stoerage-pv.yaml` and `stoerage-pvc.yaml` must be set. ```powershell -$env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; $env:VOLUME_NAME_SUFFIX = $env:IDENTITY_CLIENT_ID.Substring(0, 5); ``` diff --git a/Console.Data.MySql/.github/workflows/build-and-deploy.yml b/Console.Data.MySql/.github/workflows/build-and-deploy.yml index 98282ad2..7aef29c9 100644 --- a/Console.Data.MySql/.github/workflows/build-and-deploy.yml +++ b/Console.Data.MySql/.github/workflows/build-and-deploy.yml @@ -33,10 +33,8 @@ env: KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} @@ -109,59 +107,96 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Required"; - Get-Content .kubernetes/sql-auth-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/sql-auth-secret.tmp.yaml; kubectl apply -f .kubernetes/sql-auth-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Console.Data.MySql/.kubernetes/configmap.yaml b/Console.Data.MySql/.kubernetes/configmap.yaml index f2293b0f..4c2b173e 100644 --- a/Console.Data.MySql/.kubernetes/configmap.yaml +++ b/Console.Data.MySql/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% DOTNET_ENVIRONMENT: %DOTNET_ENVIRONMENT% \ No newline at end of file diff --git a/Console.Data.MySql/Console.Data.MySql/appsettings.json b/Console.Data.MySql/Console.Data.MySql/appsettings.json index 87c218f8..2a0a7885 100644 --- a/Console.Data.MySql/Console.Data.MySql/appsettings.json +++ b/Console.Data.MySql/Console.Data.MySql/appsettings.json @@ -13,6 +13,7 @@ "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Console.Data.MySql/README.md b/Console.Data.MySql/README.md index 4429a667..04d5bc06 100644 --- a/Console.Data.MySql/README.md +++ b/Console.Data.MySql/README.md @@ -64,6 +64,7 @@ Configured the application with the necessary data setup. "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 @@ -124,6 +125,13 @@ spec: key: data-connectionstring ``` +Also added the following variable to the `configmap.yaml`. + +```yaml +data: + Data__AuthenticationType: %SQL_AUTH_TYPE% +``` + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. @@ -131,62 +139,100 @@ Add the following environment variables to the `buid-and-deply.yml`. env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` -Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. +Additionally, these steps has been added to ensure database migrations are applied and the application database user is created, using the application's managed identity, before the +application is deployed. ```yaml -- name: Database Migration & User +- name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + +- name: MySQL Database Migration shell: pwsh run: | $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; - $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_SERVER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az mysql flexible-server ad-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].login" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Required"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_TOKEN;SslMode=Required"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - $env:MYSQL_PWD = $env:SQL_ADMIN_PASSWORD - $userExists = mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; + $env:APP_USER_SQL_PATH = "app-database-user.sql"; - if ($userExists -eq 0) - { - mysql ` - --host $env:SQL_HOST ` - --port $env:SQL_PORT ` - --user $env:SQL_ADMIN_USER ` - --ssl-mode=REQUIRED ` - -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + $sql = @" + CREATE AADUSER IF NOT EXISTS '$env:IDENTITY_NAME' IDENTIFIED BY '$env:IDENTITY_CLIENT_ID'; + GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:IDENTITY_NAME'@'%'; + FLUSH PRIVILEGES; + "@; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - } + $sql | Set-Content $env:APP_USER_SQL_PATH; + + az mysql flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + --file-path $env:APP_USER_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:IDENTITY_NAME;SslMode=Required"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; ``` Last, before applying the new Kubernetes templates, these environmental variables must be set. diff --git a/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml b/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml index fa0d8692..d85a70ab 100644 --- a/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml +++ b/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml @@ -34,10 +34,8 @@ env: KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-mysql-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true @@ -109,63 +107,123 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Database Migration & User + - name: Managed Identity shell: pwsh run: | - $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "5432"; - $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; - $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"; + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:DATA__CONNECTIONSTRING = $env:SQL_MIGRATION_CONNECTIONSTRING; + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; - & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; if ($LastExitCode -ne 0) - { + { throw "error"; }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; - $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + - name: PostgreSQL Database Migration + shell: pwsh + run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 5432; + $env:SQL_SERVER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az postgres flexible-server microsoft-entra-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].principalName" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; + + $env:DATA__CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_TOKEN;SSL Mode=Require;Trust Server Certificate=true"; + + & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; - if ($userExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" - } + throw "error"; + }; + + $env:PRINCIPAL_SQL_PATH = "app-database-principal.sql"; + $env:GRANTS_SQL_PATH = "app-database-grants.sql"; + + $principalSql = @" + DO `$`$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$env:IDENTITY_NAME') THEN + PERFORM pgaadauth_create_principal('$env:IDENTITY_NAME', false, false); + END IF; + END + `$`$; + "@; + + $principalSql | Set-Content $env:PRINCIPAL_SQL_PATH; - $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d postgres ` + --file-path $env:PRINCIPAL_SQL_PATH; - if ($userDbExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" + throw "error"; + }; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" + $grantsSql = @" + GRANT CONNECT ON DATABASE "$env:SQL_NAME" TO "$env:IDENTITY_NAME"; + GRANT USAGE ON SCHEMA public TO "$env:IDENTITY_NAME"; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "$env:IDENTITY_NAME"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$env:IDENTITY_NAME"; + "@; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" + $grantsSql | Set-Content $env:GRANTS_SQL_PATH; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" - } + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d $env:SQL_NAME ` + --file-path $env:GRANTS_SQL_PATH; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:IDENTITY_NAME;SSL Mode=Require;Trust Server Certificate=true"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Console.Data.PostgreSQL/.kubernetes/configmap.yaml b/Console.Data.PostgreSQL/.kubernetes/configmap.yaml index f2293b0f..4c2b173e 100644 --- a/Console.Data.PostgreSQL/.kubernetes/configmap.yaml +++ b/Console.Data.PostgreSQL/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% DOTNET_ENVIRONMENT: %DOTNET_ENVIRONMENT% \ No newline at end of file diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json index 87c218f8..2a0a7885 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json @@ -13,6 +13,7 @@ "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Console.Data.PostgreSQL/README.md b/Console.Data.PostgreSQL/README.md index 85abd08a..a218ebf8 100644 --- a/Console.Data.PostgreSQL/README.md +++ b/Console.Data.PostgreSQL/README.md @@ -64,6 +64,7 @@ Configured the application with the necessary data setup. "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 @@ -120,6 +121,13 @@ spec: key: data-connectionstring ``` +Also added the following variable to the `configmap.yaml`. + +```yaml +data: + Data__AuthenticationType: %SQL_AUTH_TYPE% +``` + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. @@ -127,72 +135,127 @@ Add the following environment variables to the `buid-and-deply.yml`. env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-postgres-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` -Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. +Additionally, these steps has been added to ensure database migrations are applied and the application database user is created, using the application's managed identity, before the +application is deployed. ```yaml -- name: Database Migration & User +- name: Managed Identity shell: pwsh run: | - $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "5432"; - $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; - $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:DATA__CONNECTIONSTRING = $env:SQL_MIGRATION_CONNECTIONSTRING; + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; - & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; if ($LastExitCode -ne 0) - { + { throw "error"; }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; - $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" +- name: PostgreSQL Database Migration + shell: pwsh + run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 5432; + $env:SQL_SERVER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; + $env:SQL_USER = az postgres flexible-server microsoft-entra-admin list -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER --query "[0].principalName" -o tsv; + $env:SQL_TOKEN = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv; - if ($userExists -ne "1") - { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" - } + $env:DATA__CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_TOKEN;SSL Mode=Require;Trust Server Certificate=true"; - $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" + & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; - if ($userDbExists -ne "1") + if ($LastExitCode -ne 0) { - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" + throw "error"; + }; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" + $env:PRINCIPAL_SQL_PATH = "app-database-principal.sql"; + $env:GRANTS_SQL_PATH = "app-database-grants.sql"; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" + $principalSql = @" + DO `$`$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$env:IDENTITY_NAME') THEN + PERFORM pgaadauth_create_principal('$env:IDENTITY_NAME', false, false); + END IF; + END + `$`$; + "@; - psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" - } + $principalSql | Set-Content $env:PRINCIPAL_SQL_PATH; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; -``` + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d postgres ` + --file-path $env:PRINCIPAL_SQL_PATH; -Last, before applying the new Kubernetes templates, these environmental variables must be set. + if ($LastExitCode -ne 0) + { + throw "error"; + }; -```powershell -$env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"; + $grantsSql = @" + GRANT CONNECT ON DATABASE "$env:SQL_NAME" TO "$env:IDENTITY_NAME"; + GRANT USAGE ON SCHEMA public TO "$env:IDENTITY_NAME"; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "$env:IDENTITY_NAME"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$env:IDENTITY_NAME"; + "@; + + $grantsSql | Set-Content $env:GRANTS_SQL_PATH; + + az postgres flexible-server execute ` + -n $env:SQL_SERVER ` + -u $env:SQL_USER ` + -p $env:SQL_TOKEN ` + -d $env:SQL_NAME ` + --file-path $env:GRANTS_SQL_PATH; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:IDENTITY_NAME;SSL Mode=Require;Trust Server Certificate=true"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; ``` Finally, apply the templates. diff --git a/Console.Data.SqlServer/.azure/.diagnostic-settings/logs.json b/Console.Data.SqlServer/.azure/.diagnostic-settings/logs.json deleted file mode 100644 index 10c3505a..00000000 --- a/Console.Data.SqlServer/.azure/.diagnostic-settings/logs.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "category": "QueryStoreRuntimeStatistics", - "enabled": true - }, - { - "category": "SQLSecurityAuditEvents", - "enabled": true - } -] \ No newline at end of file diff --git a/Console.Data.SqlServer/.azure/.diagnostic-settings/metrics.json b/Console.Data.SqlServer/.azure/.diagnostic-settings/metrics.json deleted file mode 100644 index 551d2f13..00000000 --- a/Console.Data.SqlServer/.azure/.diagnostic-settings/metrics.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "category": "AllMetrics", - "enabled": true, - "timeGrain": "PT1M" - } -] \ No newline at end of file diff --git a/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml b/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml index 46a6ad5e..b72017c2 100644 --- a/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml +++ b/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml @@ -17,7 +17,7 @@ env: DOTNET_EF_TOOLS_VERSION: "10.0" AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_GROUP_DELIVERY: ${{ vars.AZURE_RESOURCE_GROUP_DELIVERY }} - AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + AZURE_GROUP_DATABASE: ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} AZURE_GROUP_LOGS: ${{ vars.AZURE_RESOURCE_GROUP_LOGS }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -38,10 +38,8 @@ env: SQL_EDITION: GeneralPurpose SQL_MAX_SIZE: 64GB SQL_BACKUP_RETENTION: 35 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-sqlserver-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} concurrency: group: ${{ github.workflow }}-${{ github.repository }} @@ -114,7 +112,47 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; - - name: Create Database + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: SQL Server Create Database shell: pwsh run: | $env:SQL_SERVER_NAME = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].name" -o tsv; @@ -131,8 +169,8 @@ jobs: --max-size $env:SQL_MAX_SIZE ` --backup-storage-redundancy Geo ` --zone-redundant true; - - $env:MAINTENANCE_CONFIG_ID = az maintenance public-configuration list --query "[?name=='SQL_Default_1'].id" -o tsv; + + $env:MAINTENANCE_CONFIG_ID = "/subscriptions/$env:AZURE_SUBSCRIPTION_ID/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_Default"; az sql db update ` -n $env:SQL_NAME ` @@ -141,15 +179,36 @@ jobs: --maint-config-id $env:MAINTENANCE_CONFIG_ID; $env:DIAGNOSTIC_SETTINGS_NAME = "diagnostics-" + $env:SQL_NAME; + $env:SQL_LOGS_PATH = "sql-diagnostic-logs.json"; + $env:SQL_METRICS_PATH = "sql-diagnostic-metrics.json"; $env:WORKSPACE_ID = az monitor log-analytics workspace list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; $env:SQLDB_ID = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query id -o tsv; + $logsJson = @" + [ + { "category": "QueryStoreRuntimeStatistics", "enabled": true }, + { "category": "SQLSecurityAuditEvents", "enabled": true } + ] + "@; + + $logsJson | Set-Content $env:SQL_LOGS_PATH; + + $metricsJson = @" + [ + { "category": "Basic", "enabled": true }, + { "category": "InstanceAndAppAdvanced", "enabled": true }, + { "category": "WorkloadManagement", "enabled": true } + ] + "@; + + $metricsJson | Set-Content $env:SQL_METRICS_PATH; + az monitor diagnostic-settings create ` --name $env:DIAGNOSTIC_SETTINGS_NAME ` --resource $env:SQLDB_ID ` --workspace $env:WORKSPACE_ID ` - --logs '@.azure/.diagnostic-settings/logs.json' ` - --metrics '@.azure/.diagnostic-settings/metrics.json'; + --logs "@$env:SQL_LOGS_PATH" ` + --metrics "@$env:SQL_METRICS_PATH"; $env:ACTION_GROUP = az monitor action-group list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; @@ -165,7 +224,7 @@ jobs: --description "Alert when CPU usage is above 80% for 5 minutes."; az monitor metrics alert create ` - --name "High Memory/Worker Usage" ` + --name "High Memory And Worker Usage" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` --condition "avg workers_percent > 80" ` @@ -190,12 +249,12 @@ jobs: --name "High Storage IO" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` - --condition "avg io_consumption_percent > 80" ` + --condition "avg physical_data_read_percent > 80" ` --window-size PT5M ` --evaluation-frequency PT1M ` --action $env:ACTION_GROUP ` --severity 2 ` - --description "Alert when Storage IO consumption is above 80% for 5 minutes."; + --description "Alert when data IO usage is above 80% for 5 minutes."; az monitor metrics alert create ` --name "High Storage Percent" ` @@ -220,73 +279,73 @@ jobs: }; }; - - name: Database Migration & User + - name: SQL Server Database Migration shell: pwsh run: | - $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "1433" - $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 1433; + $env:SQL_SERVER = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;Authentication=Active Directory Service Principal;User Id=$env:AZURE_CLIENT_ID;Password=$env:AZURE_CLIENT_SECRET;Encrypt=True;TrustServerCertificate=True;"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - $loginExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" + $env:APP_USER_SQL_PATH = "app-database-user.sql"; + + $sql = @" + IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$env:IDENTITY_NAME') + BEGIN + CREATE USER [$env:IDENTITY_NAME] FROM EXTERNAL PROVIDER; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datareader' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datareader ADD MEMBER [$env:IDENTITY_NAME]; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datawriter' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datawriter ADD MEMBER [$env:IDENTITY_NAME]; + END + "@; + + $sql | Set-Content $env:APP_USER_SQL_PATH; + + $env:SQL_TOKEN = az account get-access-token --resource "https://database.windows.net/" --query accessToken -o tsv; + + Invoke-Sqlcmd ` + -ServerInstance $env:SQL_HOST ` + -Database $env:SQL_NAME ` + -AccessToken $env:SQL_TOKEN ` + -InputFile $env:APP_USER_SQL_PATH; - if ($loginExists -eq 0) - { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" - }; - - $userExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" - - if ($userExists -eq 0) + if ($LastExitCode -ne 0) { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + throw "error"; }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:IDENTITY_CLIENT_ID;Encrypt=True;TrustServerCertificate=True;"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; - Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) diff --git a/Console.Data.SqlServer/.kubernetes/configmap.yaml b/Console.Data.SqlServer/.kubernetes/configmap.yaml index f2293b0f..4c2b173e 100644 --- a/Console.Data.SqlServer/.kubernetes/configmap.yaml +++ b/Console.Data.SqlServer/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Data__AuthenticationType: %SQL_AUTH_TYPE% DOTNET_ENVIRONMENT: %DOTNET_ENVIRONMENT% \ No newline at end of file diff --git a/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json b/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json index 87c218f8..2a0a7885 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json +++ b/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json @@ -13,6 +13,7 @@ "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Console.Data.SqlServer/README.md b/Console.Data.SqlServer/README.md index 055ff8c7..d9bc7d91 100644 --- a/Console.Data.SqlServer/README.md +++ b/Console.Data.SqlServer/README.md @@ -63,6 +63,7 @@ Configured the application with the necessary data setup. "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, + "AuthenticationType": "Credentials", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 @@ -121,31 +122,80 @@ spec: key: data-connectionstring ``` +Also added the following variable to the `configmap.yaml`. + +```yaml +data: + Data__AuthenticationType: %SQL_AUTH_TYPE% +``` + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: DOTNET_EF_TOOLS_VERSION: "10.0" - AZURE_GROUP_DATABASE : ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} + AZURE_GROUP_DATABASE: ${{ vars.AZURE_RESOURCE_GROUP_DATABASE }} AZURE_GROUP_LOGS: ${{ vars.AZURE_RESOURCE_GROUP_LOGS }} SQL_EDITION: GeneralPurpose SQL_SERVICE_OBJECTIVE: GP_Gen5_2 SQL_MAX_SIZE: 64GB SQL_BACKUP_RETENTION: 35 + SQL_AUTH_TYPE: Azure SQL_NAME: nanoDb - SQL_USER: api-data-sqlserver-user - SQL_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} - SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` -Additionally, two steps have been added: one to create the application's database if it doesn't already exist, and one to ensure database migrations are applied and the application -database user has been created before the application is deployed. +Additionally, these steps has been added to ensure the database exists, migrations are applied, and the application database user is created (using the application's managed identity) before +the application is deployed. ```yaml -- name: Create Database +- name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + +- name: SQL Server Create Database shell: pwsh run: | + $env:SQL_SERVICE_OBJECTIVE = "GP_Gen5_2"; + $env:SQL_EDITION = "GeneralPurpose"; + $env:SQL_MAX_SIZE = "64GB"; + $env:SQL_BACKUP_RETENTION = "35" $env:SQL_SERVER_NAME = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].name" -o tsv; $env:SQL_DB_EXISTS = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query name -o tsv 2>$null; @@ -160,8 +210,8 @@ database user has been created before the application is deployed. --max-size $env:SQL_MAX_SIZE ` --backup-storage-redundancy Geo ` --zone-redundant true; - - $env:MAINTENANCE_CONFIG_ID = az maintenance public-configuration list --query "[?name=='SQL_Default_1'].id" -o tsv; + + $env:MAINTENANCE_CONFIG_ID = "/subscriptions/$env:AZURE_SUBSCRIPTION_ID/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_Default"; az sql db update ` -n $env:SQL_NAME ` @@ -170,15 +220,36 @@ database user has been created before the application is deployed. --maint-config-id $env:MAINTENANCE_CONFIG_ID; $env:DIAGNOSTIC_SETTINGS_NAME = "diagnostics-" + $env:SQL_NAME; + $env:SQL_LOGS_PATH = "sql-diagnostic-logs.json"; + $env:SQL_METRICS_PATH = "sql-diagnostic-metrics.json"; $env:WORKSPACE_ID = az monitor log-analytics workspace list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; $env:SQLDB_ID = az sql db show -g $env:AZURE_GROUP_DATABASE -s $env:SQL_SERVER_NAME -n $env:SQL_NAME --query id -o tsv; + $logsJson = @" + [ + { "category": "QueryStoreRuntimeStatistics", "enabled": true }, + { "category": "SQLSecurityAuditEvents", "enabled": true } + ] + "@; + + $logsJson | Set-Content $env:SQL_LOGS_PATH; + + $metricsJson = @" + [ + { "category": "Basic", "enabled": true }, + { "category": "InstanceAndAppAdvanced", "enabled": true }, + { "category": "WorkloadManagement", "enabled": true } + ] + "@; + + $metricsJson | Set-Content $env:SQL_METRICS_PATH; + az monitor diagnostic-settings create ` --name $env:DIAGNOSTIC_SETTINGS_NAME ` --resource $env:SQLDB_ID ` --workspace $env:WORKSPACE_ID ` - --logs '@.azure/.diagnostic-settings/logs.json' ` - --metrics '@.azure/.diagnostic-settings/metrics.json'; + --logs "@$env:SQL_LOGS_PATH" ` + --metrics "@$env:SQL_METRICS_PATH"; $env:ACTION_GROUP = az monitor action-group list -g $env:AZURE_GROUP_LOGS --query [0].[id] -o tsv; @@ -194,7 +265,7 @@ database user has been created before the application is deployed. --description "Alert when CPU usage is above 80% for 5 minutes."; az monitor metrics alert create ` - --name "High Memory/Worker Usage" ` + --name "High Memory And Worker Usage" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` --condition "avg workers_percent > 80" ` @@ -219,12 +290,12 @@ database user has been created before the application is deployed. --name "High Storage IO" ` --resource-group $env:AZURE_GROUP_DATABASE ` --scopes $env:SQLDB_ID ` - --condition "avg io_consumption_percent > 80" ` + --condition "avg physical_data_read_percent > 80" ` --window-size PT5M ` --evaluation-frequency PT1M ` --action $env:ACTION_GROUP ` --severity 2 ` - --description "Alert when Storage IO consumption is above 80% for 5 minutes."; + --description "Alert when data IO usage is above 80% for 5 minutes."; az monitor metrics alert create ` --name "High Storage Percent" ` @@ -248,79 +319,70 @@ database user has been created before the application is deployed. throw "error"; }; }; -``` -```yaml -- name: Database Migration & User +- name: SQL Server Database Migration shell: pwsh run: | - $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; - $env:SQL_PORT = "1433" - $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = 1433; + $env:SQL_SERVER = az sql server list -g $env:AZURE_GROUP_DATABASE --query [0].name -o tsv; - $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:DATA__CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;Authentication=Active Directory Service Principal;User Id=$env:AZURE_CLIENT_ID;Password=$env:AZURE_CLIENT_SECRET;Encrypt=True;TrustServerCertificate=True;"; & "/opt/ef-tools/$env:DOTNET_EF_TOOLS_VERSION/dotnet-ef" database update ` - --no-build ` - --configuration Release ` - --startup-project $env:APP_NAME ` - -- ` - --environment $env:ASPNETCORE_ENVIRONMENT; + --no-build ` + --configuration Release ` + --startup-project $env:APP_NAME ` + -- ` + --environment $env:ASPNETCORE_ENVIRONMENT; if ($LastExitCode -ne 0) { throw "error"; }; - - apt-get update - apt-get install -y mssql-tools unixodbc-dev - - $loginExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" - - if ($loginExists -eq 0) - { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d main ` - -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" - }; - $userExists = sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + $env:APP_USER_SQL_PATH = "app-database-user.sql"; + + $sql = @" + IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$env:IDENTITY_NAME') + BEGIN + CREATE USER [$env:IDENTITY_NAME] FROM EXTERNAL PROVIDER; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datareader' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datareader ADD MEMBER [$env:IDENTITY_NAME]; + END + + IF NOT EXISTS (SELECT 1 FROM sys.database_role_members drm + JOIN sys.database_principals r ON drm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON drm.member_principal_id = m.principal_id + WHERE r.name = 'db_datawriter' AND m.name = '$env:IDENTITY_NAME') + BEGIN + ALTER ROLE db_datawriter ADD MEMBER [$env:IDENTITY_NAME]; + END + "@; + + $sql | Set-Content $env:APP_USER_SQL_PATH; + + $env:SQL_TOKEN = az account get-access-token --resource "https://database.windows.net/" --query accessToken -o tsv; + + Invoke-Sqlcmd ` + -ServerInstance $env:SQL_HOST ` + -Database $env:SQL_NAME ` + -AccessToken $env:SQL_TOKEN ` + -InputFile $env:APP_USER_SQL_PATH; - if ($userExists -eq 0) + if ($LastExitCode -ne 0) { - sqlcmd ` - -S "$env:SQL_HOST,$env:SQL_PORT" ` - -U $env:SQL_ADMIN_USER ` - -P $env:SQL_ADMIN_PASSWORD ` - -d $env:SQL_NAME ` - -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + throw "error"; }; - echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; - echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; -``` - -Last, before applying the new Kubernetes templates, these environmental variables must be set. - -```powershell -$env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:IDENTITY_CLIENT_ID;Encrypt=True;TrustServerCertificate=True;"; + echo "SQL_CONNECTIONSTRING=$env:SQL_CONNECTIONSTRING" >> $env:GITHUB_ENV; ``` Finally, the templates can be applied. diff --git a/Console.Storage.Azure/.github/workflows/build-and-deploy.yml b/Console.Storage.Azure/.github/workflows/build-and-deploy.yml index cdd6852f..ee68b370 100644 --- a/Console.Storage.Azure/.github/workflows/build-and-deploy.yml +++ b/Console.Storage.Azure/.github/workflows/build-and-deploy.yml @@ -107,6 +107,62 @@ jobs: echo "CONTAINER_REGISTRY_HOST=$env:CONTAINER_REGISTRY_HOST" >> $env:GITHUB_ENV; + - name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + + - name: Storage Role Permissions + shell: pwsh + run: | + $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; + + az role assignment create ` + --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` + --assignee-principal-type ServicePrincipal ` + --role "Storage File Data SMB MI Admin" ` + --scope $env:STORAGE_ACCOUNT_ID + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + - name: Create Fileshare shell: pwsh run: | @@ -156,54 +212,6 @@ jobs: throw "error"; }; - - name: Managed Identity & Federated Credentials - shell: pwsh - run: | - $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; - - if (-not $env:IDENTITY_PRINCIPAL_ID) - { - az identity create ` - -g $env:AZURE_GROUP_KUBERNETES ` - -n $env:IDENTITY_NAME; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - } - - az role assignment create ` - --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` - --assignee-principal-type ServicePrincipal ` - --role "Storage File Data SMB MI Admin" ` - --scope $env:STORAGE_ACCOUNT_ID - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - az identity federated-credential create ` - --name $env:SERVICE_NAME-credentials ` - --resource-group $env:AZURE_GROUP_KUBERNETES ` - --identity-name $env:IDENTITY_NAME ` - --issuer $env:KUBERNETES_ISSUER_URL ` - --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` - --audience api://AzureADTokenExchange; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; - - name: Kubernetes Deploy shell: pwsh run: | diff --git a/Console.Storage.Azure/README.md b/Console.Storage.Azure/README.md index 2bb50080..946482be 100644 --- a/Console.Storage.Azure/README.md +++ b/Console.Storage.Azure/README.md @@ -155,7 +155,67 @@ env: STORAGE_SHARE_NAME: nano-storage-azure ``` -Additionally, this step has been added to ensure the file share is created before the application is deployed. +Additionally, include these steps to create a managed identity and federated credentials for authenticating with the storage account. + +``` +- name: Managed Identity + shell: pwsh + run: | + $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; + + if (-not $env:IDENTITY_PRINCIPAL_ID) + { + az identity create ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:IDENTITY_NAME; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; + } + + $env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; + + az identity federated-credential create ` + --name $env:SERVICE_NAME-credentials ` + --resource-group $env:AZURE_GROUP_KUBERNETES ` + --identity-name $env:IDENTITY_NAME ` + --issuer $env:KUBERNETES_ISSUER_URL ` + --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` + --audience api://AzureADTokenExchange; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; + echo "IDENTITY_CLIENT_ID=$env:IDENTITY_CLIENT_ID" >> $env:GITHUB_ENV; + echo "IDENTITY_PRINCIPAL_ID=$env:IDENTITY_PRINCIPAL_ID" >> $env:GITHUB_ENV; + +- name: Storage Role Permissions + shell: pwsh + run: | + $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; + + az role assignment create ` + --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` + --assignee-principal-type ServicePrincipal ` + --role "Storage File Data SMB MI Admin" ` + --scope $env:STORAGE_ACCOUNT_ID + + if ($LastExitCode -ne 0) + { + throw "error"; + }; +``` + +..and this step has been added to ensure the file share is created before the application is deployed. ```yaml - name: Create Fileshare @@ -208,62 +268,9 @@ Additionally, this step has been added to ensure the file share is created befor }; ``` -...and includes a step to create a managed identity and federated credentials for authenticating with the storage account. - -``` -- name: Managed Identity & Federated Credentials - shell: pwsh - run: | - $env:IDENTITY_NAME = $env:SERVICE_NAME + "-identity"; - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - $env:KUBERNETES_ISSUER_URL = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].['oidcIssuerProfile.issuerUrl'] -o tsv; - $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; - - if (-not $env:IDENTITY_PRINCIPAL_ID) - { - az identity create ` - -g $env:AZURE_GROUP_KUBERNETES ` - -n $env:IDENTITY_NAME; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - $env:IDENTITY_PRINCIPAL_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query principalId -o tsv; - } - - az role assignment create ` - --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` - --assignee-principal-type ServicePrincipal ` - --role "Storage File Data SMB MI Admin" ` - --scope $env:STORAGE_ACCOUNT_ID - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - az identity federated-credential create ` - --name $env:SERVICE_NAME-credentials ` - --resource-group $env:AZURE_GROUP_KUBERNETES ` - --identity-name $env:IDENTITY_NAME ` - --issuer $env:KUBERNETES_ISSUER_URL ` - --subject "system:serviceaccount:${env:KUBERNETES_NAMESPACE}:${env:SERVICE_NAME}-service-account" ` - --audience api://AzureADTokenExchange; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - echo "IDENTITY_NAME=$env:IDENTITY_NAME" >> $env:GITHUB_ENV; -``` - Last, during the Kubernetes deployment step, before any resources are applied, environmental variables required for the new `stoerage-pv.yaml` and `stoerage-pvc.yaml` must be set. ```powershell -$env:IDENTITY_CLIENT_ID = az identity show -g $env:AZURE_GROUP_KUBERNETES -n $env:IDENTITY_NAME --query clientId -o tsv; $env:VOLUME_NAME_SUFFIX = $env:IDENTITY_CLIENT_ID.Substring(0, 5); ```