Azure App Service
O Servidor como um container Linux personalizado no Azure App Service, em uma única instância, falando com o Azure SQL por meio de uma identidade gerenciada e sem nenhuma senha. Leia antes a Instalação do Servidor para conhecer as quatro coisas que toda implantação tem em comum, em particular o Protetor do Chaveiro e a regra da instância única.
Tudo o que a página do Docker diz sobre chaves de configuração, o protetor, o cofre e os backups vale aqui sem alteração. O que muda é onde o TLS é terminado, quem agenda os reinícios e as configurações específicas do Azure descritas abaixo.
O que não se pode fazer aqui
Nada de scale-out. Uma instância, pelo motivo explicado na página de instalação. Nada no produto vai recusar uma segunda instância; ela simplesmente vai se comportar mal. Confira o número de instâncias depois de qualquer mudança de plano e, se puder, crie um alerta para ele.
1. O plano do App Service
A frota determina uma única coisa sobre o SKU: não pode ser Free, que limita as conexões WebSocket a cinco e responde 429 além disso, enquanto cada Workstation Service mantém uma conexão continuamente. Acima do Free, o teto de WebSockets não é uma restrição. O Always On é obrigatório (veja abaixo) e é um recurso dos tiers dedicados, então dimensione o plano pelo trabalho de assinatura. O P1V3 é um bom começo.
az appservice plan create -g <rg> -n <plan> --is-linux --sku P1V3 --number-of-workers 1
2. Azure SQL
az sql server create -g <rg> -n <sqlserver> --enable-ad-only-auth \
--external-admin-name "<your-group>" \
--external-admin-principal-type Group \
--external-admin-sid <the-group's-object-id>
az sql db create -g <rg> --server <sqlserver> -n TrustBridge --service-objective S1
Use um grupo como administrador do Entra, em vez de uma pessoa que pode sair da empresa. Libere o aplicativo no firewall: "Allow Azure services" sem integração com VNet, ou um private endpoint com ela. Deixar o acesso pela rede pública desativado sem um private endpoint recusa todas as conexões com o erro 47073, e o Servidor gasta todo o prazo de inicialização esperando por ele.
3. A imagem
Qualquer registry serve. O Azure Container Registry aparece aqui porque o App Service consegue baixar imagens dele com uma identidade gerenciada, em vez de uma senha armazenada.
az acr login -n <registry>
docker tag trustbridge-server:1.6.1 <registry>.azurecr.io/trustbridge-server:1.6.1
docker push <registry>.azurecr.io/trustbridge-server:1.6.1
Use uma tag imutável. Com DOCKER_ENABLE_CI ligado, o App Service baixa a imagem de novo a cada
push, então uma tag móvel deixa a plataforma reiniciar o Servidor sem supervisão, e todo reinício
encerra as sessões ativas vinculadas a um Certificado do Cofre.
4. Crie o aplicativo
az webapp create -g <rg> -p <plan> -n <app> \
--container-image-name <registry>.azurecr.io/trustbridge-server:1.6.1
az webapp identity assign -g <rg> -n <app>
az role assignment create --role AcrPull \
--assignee-object-id $(az webapp identity show -g <rg> -n <app> --query principalId -o tsv) \
--assignee-principal-type ServicePrincipal \
--scope $(az acr show -n <registry> --query id -o tsv)
az webapp config set -g <rg> -n <app> --acr-use-identity true
az webapp restart -g <rg> -n <app>
Três situações que aparecem todas como um aplicativo que nunca inicia, com UNAUTHORIZED no log do
container:
- Num registry com ABAC habilitado, o
AcrPullnão concede nada. Um registry no modoRBAC Registry + ABAC Repository PermissionsexigeContainer Registry Repository Reader. Verifique comaz acr show -n <registry> --query roleAssignmentMode -o tsv. - O registry precisa aceitar tokens com audiência ARM, ou o download com identidade gerenciada é
recusado com
token validation failed:az acr config authentication-as-arm update -r <registry> --status enabled. - O reinício não é mera formalidade. O aplicativo foi apontado para uma imagem privada antes de ter uma identidade ou um role, então o primeiro download falhou, e nada tenta de novo sozinho.
Um aplicativo criado pelo portal é um aplicativo sitecontainers: a imagem dele é definida com
az webapp sitecontainers, e não com az webapp config container, e uma alteração de configuração
sozinha nem sempre o recicla; por isso, reinicie-o explicitamente.
5. Dê ao aplicativo acesso ao banco de dados
Conecte-se ao banco de dados como o administrador do Entra dele e crie a identidade como usuário:
CREATE USER [<app>] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [<app>];
ALTER ROLE db_datawriter ADD MEMBER [<app>];
ALTER ROLE db_ddladmin ADD MEMBER [<app>];
O db_ddladmin é o que permite ao Servidor atualizar o próprio schema, o que ele faz por padrão.
Deixe-o de fora somente se também definir Database__MigrateOnStart=false e aplicar as migrações você
mesmo antes de cada atualização. Omitir a permissão e manter o padrão transforma a próxima atualização
numa inicialização recusada.
6. Configurações
az webapp config appsettings set -g <rg> -n <app> --settings \
WEBSITES_PORT=8080 \
WEBSITES_CONTAINER_START_TIME_LIMIT=1800 \
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
KeyRingProtector__Certificate="$(base64 -w0 protector.pfx)" \
ConnectionStrings__TrustBridge="Server=tcp:<sqlserver>.database.windows.net,1433;Database=TrustBridge;Authentication=Active Directory Managed Identity;Encrypt=True" \
Signer__BaseAddress="https://signer.example.com" \
Signer__ApiKey="<application|secret>" \
Admin__Bootstrap__Email="admin@example.com" \
Admin__Bootstrap__Password="<a strong password>"
No PowerShell ou no cmd do Windows, coloque as configurações num arquivo JSON e passe
--settings @params.json: uma chave de API do Signer contém um |, que o shell interpretaria como
pipe.
| Configuração | Por quê |
|---|---|
WEBSITES_PORT=8080 | O container escuta na porta 8080. O App Service detecta 80 e 8080 automaticamente; definir a porta elimina a ambiguidade. |
WEBSITES_CONTAINER_START_TIME_LIMIT=1800 | A configuração de migração de schema deste cenário. O Servidor faz as migrações antes de começar a escutar, e o App Service encerra um container que não respondeu na porta dentro desse número de segundos. O padrão é 230, então uma atualização com uma criação de índice demorada é encerrada, reiniciada e encerrada de novo, num loop que não informa nada. 1800 é o máximo da plataforma e não custa nada quando as migrações são rápidas. |
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true | A plataforma termina o TLS nos front-ends dela. Sem isso, o Servidor vê HTTP simples vindo de um endereço do datacenter: os cookies perdem o Secure, as URLs absolutas saem com http://, e toda linha de auditoria registra o proxy em vez de quem fez a chamada. |
KeyRingProtector__Certificate | Obrigatória. Veja abaixo como usar uma referência do Key Vault em vez de um app setting. |
ConnectionStrings__TrustBridge | Identidade gerenciada, sem senha. |
Admin__Bootstrap__* | Só na primeira inicialização. Remova depois de entrar. |
O WEBSITES_ENABLE_APP_SERVICE_STORAGE está ausente de propósito: esta implantação não precisa de
armazenamento persistente. Os dois chaveiros são linhas no Azure SQL, e o único diretório em que o
container grava é /tmp.
Logs
Ative o log do container, porque uma linha de auditoria que o banco de dados não aceita é gravada
no Log do Servidor no nível Critical, e nada mais a guarda. A saída padrão (stdout) do container não
é retida por padrão no App Service Linux; az webapp log tail é um fluxo ao vivo, não um registro.
az webapp log config -g <rg> -n <app> --docker-container-logging filesystem
Isso é o mínimo: o sink no sistema de arquivos tem tamanho limitado e é rotativo. Uma implantação que
pretende manter a trilha envia os logs para o Application Insights ou para um workspace do Log
Analytics, com a retenção que escolheu. Quando um workspace é associado pelo portal, a variável
APPLICATIONINSIGHTS_CONNECTION_STRING é definida, e o Servidor envia para lá toda linha de log como
um trace com o id de correlação da requisição, sem nenhuma outra configuração.
Logging__ApplicationInsights__ConnectionString é o mesmo valor, na chave do próprio Servidor, para
um workspace associado de outra forma.
Fixe a plataforma
az webapp update -g <rg> -n <app> --client-affinity-enabled true --https-only true
az monitor autoscale list -g <rg> -o table # then delete any that exist
az webapp config set -g <rg> -n <app> --always-on true \
--generic-configurations '{"healthCheckPath": "/"}'
--https-only true é uma decisão que o Servidor deixa de propósito para a implantação: nada nele
redireciona HTTP para HTTPS, porque um Servidor on-premises costuma ser acessado por HTTP simples atrás
do proxy do próprio cliente. Aqui os dois esquemas chegam ao aplicativo, então, sem isso, um console
aberto em http:// recebe um cookie de sessão sem o flag Secure.
O Health check fica em /, e o probe de verdade vai para o seu monitoramento. Com uma única
instância, o Health check não é um recurso de balanceamento de carga, e sim um gatilho de reinício:
sem para onde redirecionar o tráfego, o App Service substitui a instância depois de uma hora de pings
com falha. / responde 200 mesmo sem o banco de dados, então só falha quando o container está morto,
que é a única falha que um reinício resolve. /healthz pode falhar por conta própria (Azure SQL fora
do ar), e ligá-lo ao Health check resultaria num reinício a cada hora que não resolve nada e ainda
interrompe as sessões vinculadas ao cofre a cada vez. Aponte um teste de disponibilidade para
/healthz; ele é anônimo e não auditado, então consultá-lo a cada trinta segundos não custa nada.
--health-check-path não é um parâmetro de az webapp config set; o caminho é uma propriedade de
site-config, e é para isso que serve o --generic-configurations. No PowerShell ou no cmd, coloque o
JSON num arquivo e passe "@params.json".
O Protetor do Chaveiro
Os comandos para gerá-lo estão na página de instalação e são os mesmos aqui. Há dois lugares para colocá-lo, e o segundo é melhor:
- Um app setting, como mostrado acima.
- Uma referência do Key Vault: armazene o base64 como um secret, dê à identidade gerenciada do
aplicativo a permissão
getsobre ele e definaKeyRingProtector__Certificate=@Microsoft.KeyVault(SecretUri=https://<vault>.vault.azure.net/secrets/<name>/).
Guarde o próprio .pfx permanentemente num lugar que nem este aplicativo nem esta assinatura do
Azure consigam acessar. Uma referência do Key Vault é uma casa conveniente para o Servidor em
execução, mas não é um backup: um Key Vault que o mesmo operador pode excluir não é uma cópia guardada
em outro lugar.
Certificados do Cofre neste cenário
az webapp config appsettings set -g $rg -n $app --settings `
Vault__Enabled=true `
Certificates__Validation__License=$([Convert]::ToBase64String([IO.File]::ReadAllBytes("LacunaPkiLicense.config")))
A licença é o mesmo tipo de artefato que o protetor, com data de validade e comprada por alguém, então uma referência do Key Vault também serve para ela; renová-la passa a ser uma nova versão do secret e um reinício.
Uma ressalva é específica deste cenário. O App Service reinicia containers para a própria
manutenção, e uma sessão vinculada a um Certificado do Cofre não sobrevive a um reinício: ela termina
como Stranded, e o cliente cria uma nova. Nada se perde permanentemente, e as sessões vinculadas a
estações de trabalho não são afetadas, mas uma cadência de reinícios que não é sua fica visível para a
assinatura com o cofre.
7. O schema
Não há nada a fazer se você concedeu o db_ddladmin no passo 5. O banco de dados em si precisa já
existir, e existe: o passo 2 o criou. Diferentemente do cenário com Docker, aqui o Servidor não
consegue criar o banco.
Para aplicar as migrações você mesmo, defina Database__MigrateOnStart=false e execute, numa máquina
com o .NET SDK e um checkout na versão implantada:
dotnet tool restore
dotnet ef database update --project src/TrustBridge.Server \
--connection "Server=tcp:<sqlserver>.database.windows.net,1433;Database=TrustBridge;Authentication=Active Directory Default;Encrypt=True;Command Timeout=3600"
Repita antes de cada atualização. O Command Timeout=3600 está ali de propósito.
8. Verifique
az webapp log tail -g <rg> -n <app>
Procure a linha KeyRingPosture com o thumbprint do protetor, a linha da postura do cofre e
Application started. Uma inicialização a frio leva cerca de um minuto até /healthz responder.
Depois:
curl -s https://<app>.azurewebsites.net/healthz # Healthy
Em seguida, entre em https://<app>.azurewebsites.net/admin, defina uma senha de verdade, faça a
inscrição do segundo fator e remova as duas configurações Admin__Bootstrap__*.
Operação
- Domínio e certificado personalizados. As estações de trabalho se conectam ao nome de host que
você informar, e a cadeia precisa ser uma em que elas confiem. Um certificado gerenciado do App
Service num domínio personalizado é a resposta simples;
*.azurewebsites.nettambém funciona, mas amarra a configuração da frota a um nome de host do Azure. - Reinícios custam mais aqui. Deixe
DOCKER_ENABLE_CIdesligado e faça deploy de forma deliberada. Mantenha o Health check num caminho que não possa falhar por conta própria. - Nunca faça scale-out.