Publicando Aplicativos Web no Azure utilizando Identity SQLite e GitHub Actions

Publicar um aplicativo web no Azure pode ser simples e prático quando você usa a linha de comando do início ao fim. Neste artigo, vamos criar uma aplicação Razor Pages com autenticação via Identity, usar o SQLite como banco de dados e fazer o deploy em um App Service Linux, automatizado pelo GitHub Actions.

O código-fonte completo da aplicação que vamos criar está no repositório crisnordev/Article. Assim, você pode comparar cada etapa com o resultado final.

Visão geral do aplicativo web no Azure

O fluxo completo passa por cinco etapas. Primeiro, criamos o repositório remoto e o projeto local; em seguida, geramos as páginas do Identity e personalizamos o cadastro do usuário.

Depois disso, configuramos o envio de e-mails com o SendGrid. Por fim, criamos os recursos no portal e ligamos o repositório ao aplicativo web no Azure por meio de um workflow do GitHub Actions.

O projeto usa o .NET 7, versão disponível quando este fluxo foi montado. Por isso, os comandos e o workflow seguem essa versão do SDK.

Ferramentas necessárias

Para executar os passos deste artigo, você precisa das CLIs do .NET, do Git, do GitHub e do Azure instaladas. Eu uso o Windows Terminal, mas os comandos funcionam em qualquer terminal, no Windows, no Linux ou no Mac.

Para editar o código, você também vai precisar de uma IDE. Eu uso o Rider, porém o VS Code e o Visual Studio atendem igualmente bem.

Criando o repositório remoto no GitHub

Vamos começar pelo repositório remoto. No navegador, acesse o GitHub, abra a página dos seus repositórios e clique em "New".

Na página seguinte, dê um nome ao repositório e não adicione o arquivo .gitignore, porque vamos gerá-lo pela CLI. Em seguida, clique em "Create repository" e copie a URL que o GitHub disponibiliza.

Tela de criação de um novo repositório no GitHub, com o campo de nome preenchido e o botão Create repository
Criação do repositório remoto que vai receber o código e disparar o deploy.

Criando o aplicativo web local antes do Azure

Na sua máquina, é recomendado guardar o repositório local em uma pasta de caminho curto, como C:/dev/. Nela, vamos criar um projeto do tipo webapp chamado Article.

Depois, dentro da pasta do projeto, geramos o .gitignore, iniciamos o Git, renomeamos a branch principal para main e adicionamos a referência ao repositório remoto:

cd ./dev/
dotnet new webapp -o Article
cd ./Article/
dotnet new gitignore
git init
git branch -M main
# Troque a URL pela referência ao seu repositório remoto
git remote add origin https://github.com/crisnordev/Article.git

Lembre-se de trocar a URL do último comando pela do seu repositório. Caso contrário, o push vai falhar por falta de permissão.

Gerando as páginas do Identity

O ASP.NET Core Identity é a biblioteca do .NET que vai cuidar da autenticação dos usuários do nosso aplicativo web no Azure. Para gerar as páginas dele pela linha de comando, usamos o scaffolder do Identity.

Instalando o gerador e os pacotes

Primeiro, instale a ferramenta de geração de código do ASP.NET e os pacotes de que o projeto precisa:

dotnet tool install -g dotnet-aspnet-codegenerator
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.Tools

Se o scaffolder retornar algum erro mais adiante, confira as versões dos pacotes. Afinal, elas precisam ser compatíveis com o framework alvo do projeto.

Executando o scaffolder

Agora, liste as opções do scaffolder e execute-o informando a classe do usuário, as páginas que devem ser criadas e o uso do SQLite:

dotnet aspnet-codegenerator identity --help
dotnet aspnet-codegenerator identity --userClass ApplicationUser --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation;Account._StatusMessage;Account.Manage.Index" --useSqLite

Como o PowerShell usa o ponto e vírgula para separar comandos, a lista de arquivos fica entre aspas. Se você quiser gerar todas as páginas do Identity, basta remover o parâmetro --files.

Para exibir os botões de login, abra Pages/Shared/_Layout.cshtml. Logo depois do </ul> que fecha os links de navegação, adicione a linha <partial name="_LoginPartial" />.

Modelo de usuário personalizado

Junto com as páginas, o scaffolder criou a classe do modelo de usuário, que vai acompanhar o aplicativo web até o Azure. Como sugestão, eu movi a pasta Data e criei uma pasta Models na raiz do projeto para guardar a classe ApplicationUser.

Essa classe herda de IdentityUser. Além disso, vamos trocar o tipo do Id para Guid, que por padrão é string, e adicionar a propriedade Name:

// Usuário da aplicação com Id do tipo Guid e um nome personalizado
public class ApplicationUser : IdentityUser<Guid>
{
    public string Name { get; set; }
}
Classe ApplicationUser do aplicativo web no Azure herdando de IdentityUser com Id do tipo Guid e propriedade Name
Classe ApplicationUser na pasta Models, com o Id em Guid e a propriedade Name.

Se você mudou o local do DataContext e do ApplicationUser, corrija os namespaces e os using das classes que os utilizam. Do contrário, o projeto não compila.

No DataContext, também precisamos referenciar o ApplicationUser e o IdentityRole, ambos usando Guid como tipo do Id:

// Contexto do Identity com usuário e papel usando Guid como chave
public class DataContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{
    public DataContext(DbContextOptions<DataContext> options) : base(options) { }
}

Personalizando o registro e o perfil

Com o modelo pronto, o próximo passo é receber o nome do usuário nas telas do aplicativo web antes de levá-lo ao Azure. Para isso, vamos alterar a página de registro e a página de gerenciamento da conta.

Página de registro

Em Areas/Identity/Pages/Account/Register.cshtml.cs, adicione a propriedade Name à classe InputModel:

[Required]
[Display(Name = "Name")]
public string Name { get; set; }

Em seguida, no método OnPostAsync(), logo após a linha var user = CreateUser();, adicione user.Name = Input.Name;. Dessa forma, o valor digitado vai para a propriedade do usuário.

Depois, em Register.cshtml, antes da <div> que recebe o Input.Email, inclua o campo do nome:

<div class="form-floating mb-3">
    <input asp-for="Input.Name" class="form-control" autocomplete="name" aria-required="true" placeholder="Name"/>
    <label asp-for="Input.Name">Name</label>
    <span asp-validation-for="Input.Name" class="text-danger"></span>
</div>

Página de gerenciamento da conta

Em Account/Manage/Index.cshtml.cs, adicione também a propriedade Name ao InputModel. Então, no método LoadAsync(), preencha o nome ao criar o InputModel:

Input = new InputModel
{
    Name = user.Name,
    PhoneNumber = phoneNumber
};

No método OnPostAsync(), depois do if que altera o telefone, atualize o nome quando ele mudar:

// Atualiza o nome apenas se o usuário alterou o valor
if (Input.Name != user.Name)
{
    user.Name = Input.Name;
}

await _userManager.UpdateAsync(user);

Em Account/Manage/Index.cshtml, depois da <div> do UserName, adicione o campo:

<div class="form-floating mb-3">
    <input asp-for="Input.Name" class="form-control" placeholder="Please choose your name." />
    <label asp-for="Input.Name" class="form-label"></label>
</div>

Por fim, em Pages/Shared/_LoginPartial.cshtml, troque o trecho Hello @UserManager.GetUserName(User)! por Hello @((await UserManager.GetUserAsync(User))?.Name)!. Assim, a saudação exibe o nome do usuário no lugar do e-mail.

Serviço de e-mail com SendGrid

Para que o aplicativo web no Azure envie os e-mails de confirmação, eu uso a API do SendGrid, que oferece uma conta gratuita. No portal dele, acesse "Settings", "API Keys" e "Create API Key".

Dê um nome à chave, escolha "Restricted Access" e leve a opção "Mail Send" até "Full Access". Em seguida, clique em "Create API Key".

O SendGrid mostra a chave uma única vez. Portanto, copie e guarde o valor em um local seguro, porque ninguém além de você deve ter acesso a ele.

Implementando o IEmailSender

Na aplicação, adicione o pacote com dotnet add package SendGrid. Depois, crie a pasta Services com a classe EmailSender:

public class EmailSender : IEmailSender
{
    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        // Interrompe o envio se a chave do SendGrid não foi configurada
        if (string.IsNullOrEmpty(Configuration.SendGridKey.SendGridApiKey))
            throw new Exception("SendGrid key is missing");

        await Execute(Configuration.SendGridKey.SendGridApiKey, subject, message, toEmail);
    }

    public static async Task Execute(string apiKey, string subject, string message, string toEmail)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage
        {
            From = new EmailAddress("noreply@example.com", "Article App"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(toEmail));

        // Desativa o rastreamento de cliques nos links do e-mail
        msg.SetClickTracking(false, false);
        await client.SendEmailAsync(msg);
    }
}

O Identity usa injeção de dependência da interface IEmailSender para enviar e-mails. Por isso, no Program.cs, registre a implementação com builder.Services.AddSingleton<IEmailSender, EmailSender>();.

O ciclo de vida Singleton faz sentido aqui, já que a classe não guarda estado no aplicativo web, nem localmente nem no Azure. Com isso, sempre que o Identity precisar do IEmailSender, ele usa a implementação EmailSender.

Lendo a chave da API

No appsettings.json, adicione uma seção para a chave. O valor real ficará nas configurações do aplicativo web no Azure, então aqui usamos apenas um marcador:

{
  "SendGridKey": {
    "SendGridApiKey": "My_SendGrid_Key"
  }
}

Na raiz do projeto, crie a classe Configuration para guardar essa chave:

public class Configuration
{
    public static SendGridConfig SendGridKey { get; set; }

    public class SendGridConfig
    {
        public string SendGridApiKey { get; set; }
    }
}

Então, no Program.cs, leia a seção e atribua o valor à propriedade estática:

// Lê a seção SendGridKey e preenche a configuração estática
var sendGridKey = new Configuration.SendGridConfig();
app.Configuration.GetSection("SendGridKey").Bind(sendGridKey);
Configuration.SendGridKey = sendGridKey;

Por fim, ajuste o final do OnPostAsync() em Register.cshtml.cs e a página RegisterConfirmation, como mostra a imagem a seguir. Além disso, confirme que options.SignIn.RequireConfirmedAccount está como true na configuração do Identity.

Código do método OnPostAsync e da página RegisterConfirmation ajustados para exigir a confirmação de conta por e-mail
Ajustes no registro para que a confirmação da conta aconteça pelo e-mail enviado.

Esse ajuste é importante porque, segundo a documentação do ASP.NET Core Identity, a página RegisterConfirmation padrão exibe o link de confirmação apenas para testes. Em produção, a verificação automática deve ficar desativada.

Criando o aplicativo web no Azure

Todo o deploy acontece no Microsoft Azure, a plataforma de nuvem da Microsoft, que permite criar uma conta gratuita. Ao criar a conta, o portal gera uma assinatura chamada "Azure Subscription 1".

Grupo de recursos do aplicativo web no Azure

Com a assinatura pronta, procure por "Grupos de recursos" na barra de busca e clique em "+ Criar". Selecione a assinatura, dê um nome ao grupo e escolha a região "(South America) Brazil South".

Em seguida, clique em "Revisar + criar" e depois em "Criar". O grupo de recursos vai reunir tudo o que o aplicativo web no Azure precisa.

Da mesma forma, abra "Serviços de Aplicativos" e clique em "+ Criar". Selecione a assinatura, o grupo de recursos criado e um nome único para a aplicação, já que ele vira parte da URL.

Em "Publicar", escolha "Código"; na "Pilha de runtime", selecione a versão do .NET do projeto; e em "Sistema operacional", escolha "Linux", na região "Brazil South". No plano Linux, clique em "Criar novo", dê um nome a ele e, em "Alterar tamanho", selecione o plano "Gratuito F1, 1 GB de memória".

Para finalizar, clique em "Revisar + criar" e em "Criar". Pronto, o aplicativo web no Azure já existe, embora ainda sem o nosso código.

Connection string e configurações do aplicativo web no Azure

Quando criamos o projeto com SQLite, a connection string aponta para a raiz da aplicação. No entanto, para publicar o aplicativo web no Azure, precisamos apontar o banco para a pasta wwwroot, senão a aplicação lança uma exceção ao tentar acessar o arquivo.

No portal, abra "Serviços de Aplicativos" e clique no nome da aplicação. Você pode parar a execução em "Parar" enquanto configura tudo.

No menu lateral, vá até "Configurações" e clique em "+ New connection string". Preencha o nome DefaultConnection, o valor DataSource=wwwroot/app.db;Cache=Shared e o tipo Custom.

No appsettings.json, a connection string fica apenas com um marcador:

{
  "ConnectionStrings": {
    "DefaultConnection": "My_Connection_String"
  }
}

No Program.cs, use o mesmo nome ao ler a connection string:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

Na mesma página, clique em "+ New application setting" para guardar a chave do SendGrid. Em um App Service Linux, o separador entre seção e item é o sublinhado duplo, então o nome fica SendGridKey__SendGridApiKey.

Segundo a documentação de configuração do App Service, o Linux não aceita dois-pontos nos nomes das configurações. Por isso, a chave hierárquica SendGridKey:SendGridApiKey vira SendGridKey__SendGridApiKey, e o valor é a chave criada no SendGrid, que nunca deve ser exposta.

Se você quer entender melhor como essas configurações sobrescrevem o appsettings.json, o artigo sobre deploy de ASP.NET com GitHub Actions e Microsoft Azure complementa este passo.

Deploy do aplicativo web no Azure com GitHub Actions

Com o ambiente pronto, falta automatizar a publicação. A partir daqui, toda mudança na branch main vai atualizar o aplicativo web no Azure automaticamente.

Centro de Implantação

No portal, abra a aplicação e selecione "Centro de Implantação" no menu lateral. Em "Source", escolha "GitHub" e autentique sua conta pelo próprio navegador.

Depois, selecione seu usuário em "Organization", o repositório da aplicação em "Repository" e a branch main em "Branch". Se quiser conferir o workflow antes, clique em "Preview file" e, em seguida, salve com "Save".

O Azure então cria a pasta .github/workflows no repositório remoto, com o arquivo main_{nome-do-app}.yml. Esse arquivo contém os passos que o GitHub Actions vai executar.

Secrets do repositório

No GitHub, abra "Settings", depois "Actions" e "General". Em "Actions permissions", marque "Allow all actions and reusable workflows" e salve.

Em seguida, abra "Secrets" e "Actions". Lá você encontra o secret AZUREAPPSERVICE_PUBLISHPROFILE_... que o Azure criou, e vamos adicionar mais dois.

O primeiro se chama AZURE_SQLITE_CONNECTION_STRING e recebe o valor DataSource=wwwroot/app.db;Cache=Shared. O segundo, AZURE_CREDENTIALS, depende de um service principal criado pela CLI do Azure:

# Troque AppName, SubscriptionId e ResourceGroupName pelos seus valores
az ad sp create-for-rbac --name "AppName" --role contributor --scopes /subscriptions/SubscriptionId/resourceGroups/ResourceGroupName --sdk-auth

Esse comando retorna um JSON, que você copia e cola como valor do secret. Vale saber que versões recentes da CLI marcam a opção --sdk-auth como obsoleta, e a Microsoft recomenda o login com OpenID Connect em projetos novos.

Ajustando o workflow do aplicativo web no Azure

Execute um git pull para trazer o workflow para o repositório local. Então, adicione a connection string, o login no Azure, a instalação do EF Tools e o database update:

name: Build and deploy ASP.NET Core app to Azure Web App - articleidentity

on:
  push:
    branches:
      - main
  workflow_dispatch:

env:
  ConnectionStrings__DefaultConnection: ${{ secrets.AZURE_SQLITE_CONNECTION_STRING }}

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '7.x'

      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Install EF Tools
        run: dotnet tool install --global dotnet-ef

      - name: Build with dotnet
        run: dotnet build --configuration Release

      - name: Update database
        run: dotnet ef database update

      - name: dotnet publish
        run: dotnet publish -c Release -o ${{ env.DOTNET_ROOT }}/myapp

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v4
        with:
          name: .net-app
          path: ${{ env.DOTNET_ROOT }}/myapp

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: 'Production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v4
        with:
          name: .net-app

      - name: Deploy to Azure Web App
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v3
        with:
          app-name: 'articleidentity'
          slot-name: 'Production'
          publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_XXXXXXXX }}
          package: .

A variável ConnectionStrings__DefaultConnection faz o dotnet ef database update criar o banco em wwwroot/app.db no runner. Como o dotnet publish copia o conteúdo de wwwroot, o arquivo do banco segue no pacote publicado.

As versões das actions acima são as majors atuais, porque as versões antigas de upload-artifact e download-artifact foram descontinuadas pelo GitHub. Para aprofundar o assunto, veja também como criar workflows reutilizáveis no GitHub Actions.

Primeiro deploy do aplicativo web no Azure

Agora, crie a primeira migration com dotnet ef migrations add v1. Em seguida, troque temporariamente a connection string do appsettings.json por DataSource=wwwroot/app.db;Cache=Shared e execute dotnet ef database update.

Depois, desfaça essa alteração e volte para o marcador My_Connection_String. Assim, nenhum valor real fica no repositório.

No portal, abra a aplicação e clique em "Iniciar" para colocá-la em execução. Por fim, envie tudo ao repositório remoto para disparar o workflow:

git add .
git commit -m "Add app.db, migrations and workflow changes"
git push -u origin main

Você acompanha cada etapa na aba "Actions" do repositório. Quando o workflow terminar, o aplicativo web no Azure estará publicado, com deploy automatizado, e você pode testá-lo pela URL exibida em "Serviços de Aplicativos".

Cuidados com SQLite no aplicativo web no Azure

Esse fluxo é ótimo para estudos e protótipos, mas tem limites. O FAQ do App Service no Linux explica que o sistema de arquivos é um compartilhamento de rede montado, o que impede locks exclusivos no arquivo do SQLite.

Por isso, a Microsoft recomenda um banco gerenciado, como Azure SQL, MySQL ou PostgreSQL, para cargas reais. Além disso, como o arquivo app.db segue no pacote, um novo deploy pode sobrescrever o banco que já está em produção.

Em outras palavras, trate o SQLite aqui como um banco de demonstração. Quando o aplicativo web no Azure ganhar usuários de verdade, migre os dados para um serviço gerenciado.

Conclusão

Neste passo a passo, criamos uma aplicação Razor Pages com Identity, personalizamos o cadastro com o nome do usuário e configuramos a confirmação de conta por e-mail com o SendGrid. Depois, publicamos o aplicativo web no Azure em um App Service Linux gratuito.

Com o GitHub Actions, cada push na branch main gera um novo deploy, sem passos manuais. Portanto, você ganha um processo de entrega previsível desde o primeiro commit.

Se quiser se aprofundar em Azure, Git, GitHub e DevOps, o curso Fundamentos do Azure, Git, GitHub e DevOps cobre esses temas em detalhes.

FAQ

Por que usar dois sublinhados no nome da configuração no Azure?

No App Service Linux, os nomes de configuração não aceitam dois-pontos. Por isso, a hierarquia SendGridKey:SendGridApiKey é escrita como SendGridKey__SendGridApiKey.

Posso usar SQLite em produção no App Service?

Não é recomendado, porque o sistema de arquivos do App Service Linux é um compartilhamento de rede que impede locks exclusivos. Para produção, use Azure SQL, MySQL ou PostgreSQL.

Preciso do Visual Studio para gerar as páginas do Identity?

Não, o dotnet-aspnet-codegenerator gera as páginas pela linha de comando. Basta instalar a ferramenta e os pacotes de design no projeto.

Onde guardo a chave do SendGrid com segurança?

Guarde a chave nas configurações do App Service, e nunca no repositório. No appsettings.json, mantenha apenas um marcador.

A opção --sdk-auth ainda funciona?

Ela ainda existe, mas as versões recentes da CLI do Azure a marcam como obsoleta. Em projetos novos, prefira o login do GitHub Actions com OpenID Connect.