In the previous post, we have seen how two Azure Container Apps can communicate with each other synchronously without and with Dapr using service-to-service invocation and services discovery using HTTP protocol.
In this post, I will cover how 2 services deployed to Azure Container Apps communicate synchronously over gRPC without using Dapr and then we will Daperize the 2 services and utilize the service-to-service invocation features coming with Dapr.
The scenarios I’ll cover are the following:
- Scenario 1: Invoke gRPC services deployed to Container Apps using GrpcClient.
- Scenario 2: Invoke gRPC services deployed to Container Apps via Dapr Sidecar using GrpcClient (Part 2)
- Scenario 3: Invoke gRPC services deployed to Container Apps via Dapr Sidecar using DaprClient SDK (Part 2)
gRPC Communication In Azure Container Apps
Basically what I’m going to build today is a simple gRPC enabled service/server that exposes 3 endpoints to manage personal expenses, this gRPC service will be deployed into Azure Container Apps and will be invoked/called from a simple minimal .Net Web API which will act as gRPC client and will be deployed to Azure Container Apps too.
The source code for this tutorial is available on GitHub.
The scenarios depend on each other, so if you are interested in scenario 3 for example please consider looking into the scenarios in order.
Scenario 1: Invoke gRPC services using GrpcClient (Dapr disabled)
In this scenario, I will create the gRPC service and the gRPC client which invokes the service, use PostMan interactive UI to call the gRPC services, and then deploy both gRPC service and client to Azure Container Apps. The scenario should reflect the architecture diagram below:
Create the gRPC Service
Step 1: Create the gRPC Service project
Using VS Code or Visual Studio 2022, I will create a new project named “Expenses.Grpc.Server” of the type “ASP.NET Core gRPC Service”. If you are using VS Code you can create the project by running dotnet new grpc -o Expenses.Grpc.Server This will create a new gRPC enabled project with a default “greet.proto” file which we are going to update in the next step.
Step 2: Create a new Proto file
Now, I will add a new Proto file named “expense.proto” under the folder “Protos” which will contain all RPC methods needed to generate the gRPC server stubs, request inputs, and response outputs for the methods, add the file and paste the content below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
syntax = "proto3"; option csharp_namespace = "Expenses.Grpc.Server"; message ExpenseModel{ int32 id =1; string provider =2; double amount =3; string category = 4; string owner = 5; int32 workflowstatus = 6; optional string description = 7; } message GetExpensesRequest { string owner = 1; } message GetExpensesResponse { repeated ExpenseModel expenses = 1; } message GetExpenseByIdRequest { int32 id = 1; } message GetExpenseByIdResponse { ExpenseModel expense = 1; } message AddExpenseRequest { string provider =1; double amount =2; string category = 3; string owner = 4; int32 workflowstatus = 5; optional string description = 6; } message AddExpenseResponse { ExpenseModel expense = 1; } service ExpenseSvc { rpc GetExpenses(GetExpensesRequest) returns (GetExpensesResponse) {} rpc GetExpenseById(GetExpenseByIdRequest) returns (GetExpenseByIdResponse) {} rpc AddExpense(AddExpenseRequest) returns (AddExpenseResponse) {} } |
What I’ve done here is straightforward, this Proto file exposes 3 RPC methods as the following
- GetExpenses: Returns list of “ExpenseModel” based on the expense owner input string.
- GetExpenseById: Returns a single “ExpenseModel” based on the provided expense id.
- AddExpense: Add a new expense to the repository based on the provided “ExpenseRequest”, and returns the saved “ExpenseModel” to the caller.
Step 3: Generate gRPC service stubs based on the Proto file
In order to generate the server stubs, I need to invoke the protocol buffer compiler to generate the code for the target language (.NET in my case) .NET uses the Grpc.Tools NuGet package with MSBuild to provide automatic code generation of server stubs, so when we build the project or call “dotnet build”, the compiler will automatically generate server stubs based on the Proto file definition.
To enable this, we need to add this new Proto file to the gRPC service project, so open the project file “Expenses.Grpc.Server.csproj” and paste the below code:
1 2 3 |
<ItemGroup> <Protobuf Include="Protos\expense.proto" GrpcServices="Server" /> </ItemGroup> |
You can remove any default Proto files coming with the default template. Notice how I set the “GrpcServices” property to the value of a “Server” as we are generating server stubs now.
Once added, build the project, and it should generate the code of server stubs for you.
Step 4: Create a repository to store data In-Memory
To keep things simple, I will use a static list to store expenses and manipulate them, we will use the same repository in the next post in scenario 3 once we create the gRPC service implementation in a different way. To create the repository add a new interface named “IExpensesRepo.cs” under a new folder named “Services” and paste the code below:
1 2 3 4 5 6 7 8 9 |
namespace Expenses.Grpc.Server.Services { public interface IExpensesRepo { List<ExpenseModel> GetExpensesByOwner(string owner); ExpenseModel? GetExpenseById(int id); ExpenseModel AddExpense(ExpenseModel expense); } } |
Then add a new file named “ExpensesRepo.cs” which implements the interface “IExpensesRepo.cs” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
namespace Expenses.Grpc.Server.Services { public class ExpensesRepo : IExpensesRepo { private static List<ExpenseModel> _expensesList = new List<ExpenseModel>(); private void GenerateRandomExpenses() { if (_expensesList.Count > 0) { return; } _expensesList.Add(new ExpenseModel { Id = 1, Provider = "Golds Gym", Amount = 290, Category = "Fitness Activity", Owner = "tjoudeh@mail.com", Workflowstatus = 1, Description = "" }); _expensesList.Add(new ExpenseModel { Id = 2, Provider = "Adidas", Amount = 100, Category = "Athletic Shoes", Owner = "tjoudeh@mail.com", Workflowstatus = 1, Description = "" }); _expensesList.Add(new ExpenseModel { Id = 3, Provider = "FreeMind", Amount = 25, Category = "Yoga Class", Owner = "xyz@yahoo.com", Workflowstatus = 2, Description = "" }); } public ExpensesRepo() { GenerateRandomExpenses(); } public ExpenseModel AddExpense(ExpenseModel expense) { expense.Id = _expensesList.Max(e => e.Id) + 1; _expensesList.Add(expense); return expense; } public ExpenseModel? GetExpenseById(int id) { return _expensesList.SingleOrDefault(e => e.Id == id); } public List<ExpenseModel> GetExpensesByOwner(string owner) { var expensesList = _expensesList.Where(t => t.Owner.Equals(owner, StringComparison.OrdinalIgnoreCase)).OrderByDescending(o => o.Id).ToList(); return expensesList; } } } |
What I’ve done here is simple, it is a service that exposes three methods responsible to list expenses by expense owner, getting a single expense by Id, and adding a new expense and storing it in the static list.
Step 5: Implement the Service based on generated gRPC service stubs
Now I will add the actual implementation of the Expenses Service by inheriting it from the auto-generated stubs. To do so add a new file named “ExpenseService.cs” under the folder named “Services” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
using Grpc.Core; namespace Expenses.Grpc.Server.Services { public class ExpenseService : ExpenseSvc.ExpenseSvcBase { private readonly ILogger<ExpenseService> _logger; private readonly IExpensesRepo _expensesRepo; public ExpenseService(IExpensesRepo expensesRepo, ILogger<ExpenseService> logger) { _logger = logger; _expensesRepo = expensesRepo; _logger.LogInformation("Invoking Constructor"); } public override Task<GetExpensesResponse> GetExpenses(GetExpensesRequest request, ServerCallContext context) { _logger.LogInformation("Getting expenses for owner: {owner}", request.Owner); var response = new GetExpensesResponse(); var filteredResults = _expensesRepo.GetExpensesByOwner(request.Owner); response.Expenses.AddRange(filteredResults); return Task.FromResult(response); } public override Task<AddExpenseResponse> AddExpense(AddExpenseRequest request, ServerCallContext context) { _logger.LogInformation("Adding expense for provider {provider} for owner: {owner}", request.Provider, request.Owner); var response = new AddExpenseResponse(); var expenseModel = new ExpenseModel() { Owner = request.Owner, Amount = request.Amount, Category = request.Category, Provider = request.Provider, Workflowstatus = request.Workflowstatus, Description = request.Description }; _expensesRepo.AddExpense(expenseModel); response.Expense = expenseModel; return Task.FromResult(response); } public override Task<GetExpenseByIdResponse> GetExpenseById(GetExpenseByIdRequest request, ServerCallContext context) { _logger.LogInformation("Getting expense by id: {id}", request.Id); var response = new GetExpenseByIdResponse(); var expense = _expensesRepo.GetExpenseById(request.Id); response.Expense = expense; return Task.FromResult(response); } } } |
What I’ve done here is the following:
- The service “ExpenseService” is inherited from the abstract class “ExpenseSvc.ExpenseSvcBase”, this abstract class is auto-generated by the protocol buffer compiler based on the “expense.proto” file definition. The class exists in the following location on my machine “~\Expenses.Grpc.Server\obj\Debug\net6.0\Protos\ExpenseGrpc.cs”
- I’m overriding the three methods that are generated by the protocol buffer compiler and exist in the class “ExpenseSvc.ExpenseSvcBase” Those are methods defined in the “expense.proto” file. Each method accepts a “Request” input parameter that defines what the request accepts, and a “ServerCallContext” parameter that contains authentication context, headers, etc… Each method returns a “Response” based on the “expense.proto” definition. All those Request inputs and Response outputs classes are auto-generated by the gRPC tooling for .NET.
- For example, if we take a look at the method “GetExpenses”, we’ll notice that it accepts an input request of type “GetExpensesRequest” which encapsulates the “Owner” string property which I will be passing to the method “GetExpensesByOwner” from the repository “ExpensesRepo” to return a list of expenses associated with this owner. The response type of this method is a list of expenses that we are adding to the “Expenses” property part of the “GetExpensesResponse”.
Step 6: Add “Expenses” gRPC service to the routes pipeline and Register “IExpensesRepo” repository
Now we need to add the “ExpensesService” gRPC service to the routing pipeline so clients can access the three methods, and we need the repository “IExpensesRepo” as a singleton service so it is injected into the “ExpensesService” service constructor, to do so, open the file “Program.cs” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using Expenses.Grpc.Server.Services; var builder = WebApplication.CreateBuilder(args); // Additional configuration is required to successfully run gRPC on macOS. // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 // Add services to the container. builder.Services.AddSingleton<IExpensesRepo, ExpensesRepo>(); builder.Services.AddGrpc(); var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGrpcService<ExpenseService>(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.Run(); |
Step 7: (Optional) Enable gRPC service reflection
In order to call a gRPC service, any gRPC-enabled client tooling should have access to the Protobuf contract of the service (.proto) file before being able to invoke the gRPC service, to simplify this, we can enable gRPC reflection on the server, so tools such as PostMan (I’m going to use it for testing next step) will use reflection to automatically discover service contracts. Once gRPC reflection is enabled on the gRPC server; it adds a new gRPC service to the app that clients can call to discover services.
To do so, add a reference for the NuGet package “Grpc.AspNetCore.Server.Reflection” and then open the file “Program.cs” and add the highlighted lines below:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var builder = WebApplication.CreateBuilder(args); // code omitted for brevity builder.Services.AddGrpcReflection(); var app = builder.Build(); // code omitted for brevity app.MapGrpcReflectionService(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.Run(); |
What I’ve done here is the following:
- “AddGrpcReflection” to register services that enable reflection.
- “MapGrpcReflectionService” to add a reflection service endpoint.
With those changes in place, client apps that support gRPC reflection can call the reflection service to discover services hosted by the server. It is worth noting that when reflection is enabled, it only enables service discovery and doesn’t bypass server-side security.
Step 8: Test gRPC service using PostMan
To start testing the gRPC service locally, run the project “Expenses.Grpc.Server” using dotnet run or from Visual Studio using Kestrel. Take note of the https port as shown I’m the image below:
Open PostMan and follow the steps below:
- Select the “New” button and choose “gRPC Request”.
- Enter the gRPC server’s hostname and port in the server URL. In my case, it is “localhost:7029”. Don’t include the http or https scheme in the URL. I’m using the port with https, so I select the padlock next to the server URL to enable TLS in Postman.
- Navigate to the “Service definition” section, then select server reflection or import the “expense.proto” file. In my case, our gRPC service has reflection enabled so I will use the “server reflection” approach. When complete, the dropdown list next to the server URL textbox has a list of gRPC methods available.
- Navigate to the “Settings” section, and turn off “Enable server certificate verification”, this is only needed when running gRPC server locally.
- To call a gRPC method, in my case I will test the method “AddExpense” so select from the dropdown, copy the message content from below and paste it in the message body textbox, then select “Invoke” to send the gRPC call to the server.
1 2 3 4 5 6 7 8 |
{ "provider": "Golds Gym 6", "amount": 350, "category": "Fitness Activity", "owner": "tjoudeh@mail.com", "workflowstatus": 2, "description": "Gym Subscription" } |
If all is configured successfully, you should receive back the created expense with an “Id” property assigned to it, it should look like the below image:
Create the gRPC Client App
Step 1: Create the gRPC Client project
Now I will add a new Minimal Web Api which will act as a gRPC client for the gRPC service and will define three REST API endpoints to access the gRPC methods, using VS Code or Visual Studio 2022, create a new project named “Expenses.Grpc.API” of the type “ASP.NET Core Web Api”, do not forget to uncheck “Use Controllers” and “Configure for HTTPS”. If you are using VS Code you can create the project by running dotnet new webapi -minimal -o Expense.Grpc.Api --no-https This will create a new minimal Web API.
Step 2: Add needed NuGet packages and Proto file reference
I will install the 2 NuGet packages needed for the gRPC client to invoke the gRPC service, to do so, Install package “Grpc.AspNetCore” and package “Grpc.Net.ClientFactory“. The package “Grpc.Net.ClientFactory” is optional to use, but I recommend using it as it will provide a central location for configuring gRPC client instances and it will manage the lifetime of the underlying “HttpClientMessageHandler”
Next, I’ll add a link reference to the “expense.proto” protobuf file so the gRPC client application will be able to automatically generate gRPC client code to invoke the gRPC service, thanks for the built-in .NET integration between MSBuild and the Grpc.Tools NuGet package
Open the file “Expenses.Grpc.Api.csproj” and add the code below:
1 2 3 4 5 |
<ItemGroup> <Protobuf Include="..\Expenses.Grpc.Server\Protos\expense.proto" GrpcServices="Client"> <Link>Protos\expense.proto</Link> </Protobuf> </ItemGroup> |
Notice that I set the “GrpcServices” property value to “Client”, this will tell the gRPC client when building it to generate concrete client types based on the “expense.proto” file definition. The generated gRPC client code will contain “client” methods that translate to the gRPC service I’ve built in the previous steps.
Step 3: Add environment variables
To simplify the testing of gRPC client locally and when deploying it to Azure Container Apps, I added the below environment variables which we are going to update when deploying to Azure Container Apps, so open file “appsettings.json” and add the configuration below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "grpc": { "server": "{0}://localhost:{1}", "localhost": true, "daprClientSDK": false } } |
Step 4: Define API endpoints and invoke gRPC
Because I’m using minimal API, all services configuration and API endpoints will be on the same file “Program.cs”, so I will list the entire code of the file and explain what I’ve done thoroughly, open the file “Program.cs” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
using Expenses.Grpc.Server; using Grpc.Core; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddGrpcClient<ExpenseSvc.ExpenseSvcClient>(o => { var islocalhost = builder.Configuration.GetValue("grpc:localhost", false); var serverAddress = ""; if (islocalhost) { var port = "7029"; var scheme = "https"; serverAddress = string.Format(builder.Configuration.GetValue<string>("grpc:server"), scheme, port); } else { serverAddress = builder.Configuration.GetValue<string>("grpc:server"); } o.Address = new Uri(serverAddress); }); var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGet("/api/expenses", async (ExpenseSvc.ExpenseSvcClient grpcClient, string owner) => { GetExpensesResponse? response; var request = new GetExpensesRequest { Owner = owner }; app?.Logger.LogInformation("Calling grpc server (GetExpenses) for owner: {owner}", owner); response = await grpcClient.GetExpensesAsync(request); return Results.Ok(response.Expenses); }); app.MapGet("/api/expenses/{id}", async (ExpenseSvc.ExpenseSvcClient grpcClient, int id) => { GetExpenseByIdResponse? response; var request = new GetExpenseByIdRequest { Id = id }; app?.Logger.LogInformation("Calling grpc server (GetExpenseByIdRequest) for id: {id}", id); response = await grpcClient.GetExpenseByIdAsync(request); return Results.Ok(response.Expense); }).WithName("GetExpenseById"); app.MapPost("/api/expenses", async (ExpenseSvc.ExpenseSvcClient grpcClient, Dapr.Client.DaprClient daprClient,ExpenseModel expenseModel) => { AddExpenseResponse? response; var request = new AddExpenseRequest { Provider = expenseModel.Provider, Amount = expenseModel.Amount, Category = expenseModel.Category, Owner = expenseModel.Owner, Workflowstatus = expenseModel.Workflowstatus, Description = expenseModel.Description }; app?.Logger.LogInformation("Calling grpc server (AddExpenseRequest) for provider: {provider}", expenseModel.Provider); response = await grpcClient.AddExpenseAsync(request); return Results.CreatedAtRoute("GetExpenseById", new { id = response.Expense.Id }, response.Expense); }); app.Run(); internal class ExpenseModel { public string Provider { get; set; } = string.Empty; public double Amount { get; set; } = 0.0; public string Category { get; set; } = string.Empty; public string Owner { get; set; } = string.Empty; public int Workflowstatus { get; set; } public string Description { get; set; } = string.Empty; } |
What I’ve done here is the following:
- In-line builder.Services.AddGrpcClient<ExpenseSvc.ExpenseSvcClient> I’ve registered a gRPC client, the generic “AddGrpcClient” extension method is used within an instance of WebApplicationBuilder at the app’s entry point, specifying the gRPC typed client class and service address. The client class is auto-generated based on the “expense.proto” definition. The gRPC client type is registered as transient with dependency injection (DI). The client can now be injected and consumed directly in types created by DI.
- The gRPC service address when running locally will be the same address I’ve used when running the gRPC service on localhost, the address value in my case will be “https://localhost:7029”.
- I’ve defined a DTO class named “ExpenseModel” which will be used when sending JSON payload in the POST request body to create a new expense. We could use the “ExpenseModel” class generated by gRPC server but it is always better to use a DTO when exposing API endpoints.
- I’ve defined 2 GET endpoints to get expenses based on the Expense Owner or by Expense Id, and a POST endpoint to allow creating a new expense.
- Taking on of the endpoints for example: GET “/api/expenses/{id}” and analyzing it in detail:
- I’ve injected an instance named “grpcClient” of the “ExpenseSvc.ExpenseSvcClient”. This client contains auto-generated methods based on the “expense.proto” definition.
- I’m taking the “Id” from the route and using it to create an instance of “GetExpenseByIdRequest”.
- Then I’m calling the method “GetExpenseByIdAsync” asynchronously as the following response = await grpcClient.GetExpenseByIdAsync(request); and assign the response to an object of type “GetExpenseByIdResponse”
- Lastly, I’m returning 200 OK with the returned ExpenseModel to the caller.
- The other endpoints are following the same pattern described in the previous point.
Step 5: Test the gRPC client and the gRPC server locally
With those changes in place, I can run both the gRPC server and the client and test locally to do so, run the gRPC server first by calling dotnet run and then the gRPC client using the same command. I’ll be using PostMan to send a regular HTTP request to the client API to one of the endpoints, for example, I will send a POST request to the endpoint “/api/expenses”, the request will look like the below:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
POST /api/expenses/ HTTP/1.1 Host: localhost:5252 Content-Type: application/json Content-Length: 170 { "provider": "Hyve8", "amount": 350, "category": "Gym Subscription", "owner": "tjoudeh@mail.com", "workflowstatus": 1, "description": "" } |
Once you send the request, the endpoint should return 201 created and you should see logs reported on the gRPC server to indicate that the request from gRPC client reached the gRPC server and a new expense has been created, logs should be similar to the below image:
Deploy the gRPC server and the gRPC client to Azure Container Apps
We are ready now to deploy both applications to Azure Container Apps and do an end-to-end test.
Step 1: Add Dockerfile to the gRPC server and client
In order to deploy both applications into a container we need to add Docker files for both apps, so ensure you are on the root of the project “Expenses.Grpc.Server” and add a file named “Docekrfile” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["Expenses.Grpc.Server/Expenses.Grpc.Server.csproj", "Expenses.Grpc.Server/"] RUN dotnet restore "Expenses.Grpc.Server/Expenses.Grpc.Server.csproj" COPY . . WORKDIR "/src/Expenses.Grpc.Server" RUN dotnet build "Expenses.Grpc.Server.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Expenses.Grpc.Server.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Expenses.Grpc.Server.dll"] |
Do the same for the project “Expenses.Grpc.Api” and add a file named “Docekrfile” and paste the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["Expenses.Grpc.Api/Expenses.Grpc.Api.csproj", "Expenses.Grpc.Api/"] RUN dotnet restore "Expenses.Grpc.Api/Expenses.Grpc.Api.csproj" COPY . . WORKDIR "/src/Expenses.Grpc.Api" RUN dotnet build "Expenses.Grpc.Api.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Expenses.Grpc.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Expenses.Grpc.Api.dll"] |
You can use the tooling coming with Visual Studio or VS Code (Docker Extension should be installed) to generate the Docker files for you.
Step 2: Build and Push Container Images to a Container Registry
Now we need to build and push both images to a Container Registry, In my case, I’m using ACR, you can follow this post to see the commands needed to build and push images to ACR. You can use Docker Hub as well.
Step 3: Create Azure Container Apps Environment
To deploy an Azure Container App, we need to have a Container Apps Environment to host it, you can follow this post to see how to create an environment.
Step 4: Deploy gRPC server to Azure Container Apps
Now I’ll deploy the gRPC server to Azure Container App, I’ll be using CLI for creating the Azure Container App, use the right variable names in terms of environment, ACR name, etc.. to match your deployment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$RESOURCE_GROUP="aca-grpc-rg" $LOCATION="westus" $ENVIRONMENT="aca-grpc-env" $BACKEND_SVC_NAME="expenses-grpc-server" $BACKEND_API_NAME="expenses-grpc-api" $ACR_NAME="taskstrackeracr" az containerapp create ` --name $BACKEND_SVC_NAME ` --resource-group $RESOURCE_GROUP ` --environment $ENVIRONMENT ` --image "$ACR_NAME.azurecr.io/$BACKEND_SVC_NAME" ` --registry-server "$ACR_NAME.azurecr.io" ` --target-port 80 ` --ingress 'external' ` --transport http2 ` --min-replicas 1 ` --max-replicas 1 ` --cpu 0.25 --memory 0.5Gi ` --revision-suffix v20221031-1 ` --query configuration.ingress.fqdn |
Notice that I’m setting the “Transport” value here to “http2” as this is needed when the deployed container is exposing gRPC methods. I can set the “Ingress” property to “Internal” to allow only the container apps deployed within the same environment to call the gRPC server (similar to our case) but I kept it “external” as I want to test the gRPC server from PostMan as the next step.
Step 5: Test the gRPC server after deploying to Azure Container App
Similar to what I’ve done previously when testing gRPC server locally, I will take the FQDN of the gRPC server and follow the exact steps done in step 8 earlier, if all is working correctly, you should be able to invoke the gRPC service using service reflection successfully, for example, to create create an expense, PostMan results will look like the below:
Notice that we can “Enable server certificate verification” when testing against a gRPC service deployed to Azure Container Apps as the certificate used by Container Apps is a trusted certificate and the PostMan client won’t complain about it.
Step 6: Deploy gRPC client to Azure Container Apps
Now I’ll deploy the gRPC client API to Azure Container App, I’ll be using CLI for creating the Azure Container App:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
az containerapp create ` --name $BACKEND_API_NAME ` --resource-group $RESOURCE_GROUP ` --environment $ENVIRONMENT ` --image "$ACR_NAME.azurecr.io/$BACKEND_API_NAME" ` --registry-server "$ACR_NAME.azurecr.io" ` --target-port 80 ` --transport http ` --ingress 'external' ` --min-replicas 1 ` --max-replicas 1 ` --cpu 0.25 --memory 0.5Gi ` --env-vars "grpc__server=https://expenses-grpc-server.gentleplant-b85581e3.westus.azurecontainerapps.io" "grpc__localhost=false" ` --query configuration.ingress.fqdn |
Notice that I’m keeping the transport to HTTP here as this is a normal Web API that will invoke the gRPC server. As well I’m setting the environment variables to the address of the gRPC service.
With this in place, we can do our final testing via PostMan or any other REST client, take note of your gRPC client API Container App FQDN and invoke the POST operation to create a new expense, PostMan result should look like the below:
In the next post, I’ll be covering how to invoke gRPC services via Dapr Sidecar using GrpcClient and DaprClient (Dapr Client SDK).
Excellent series of posts.
I benefited a lot going through the posts.
Many thanks for your effort.
I’m glad to hear this Kris, thanks for your comment š