こんにちは、たびとです。
前々回 Windows 版、前回 Linux 版と作ってきましたが、今回は Docker で作ります。 プログラムの内容は以前と同じで、Docker 用の構築方法だけが異なります。
前々回の Windows 版はこちらから。
前回の Linux 版はこちらから。
この記事の対象者
docker 準備
ここでは、WSL2/Ubuntu 上にインストールした docker を用いています。 詳細は、以前の記事を参考にしてください。
gRPC サーバ
ソースコードは前回の Linux 版と同じですが、launchSettings.json は本番用の資材には含まれないため、編集は不要です。 代わりに Dockerfile 内で指定します。
今回は、greeter ディレクトリ配下に docker 資材を配置していきます。
$ mkdir -p greeter/src/GrpcGreeter $ cd greeter $ touch docker-compose.yml $ cd src $ touch Dockerfile $ cd GrpcGreeter $ dotnet new grpc $ dotnet add package NLog.Web.AspNetCore $ touch nlog.config
証明書作成
前回、作らなかった証明書を作ります。 証明書がないと、ASPNETCORE_URLS に https を指定するとエラーになって、 gRPC が起動しなくなり、docker コンテナも起動しなくなります。
証明書は、greeter ディレクトリ直下に作成します。 docker-compose.yml があるディレクトリに移動してください。
まずは、秘密鍵のファイルを作ります。
$ openssl genrsa 2048 > ssl_greeter.key Generating RSA private key, 2048 bit long modulus (2 primes) ...............................+++++ .........................................+++++ e is 65537 (0x010001)
次に、証明書発行要求ファイルを作ります。 適当に入力してください。面倒であれば、全て Enter キーのみでも構いません。
$ openssl req -new -key ssl_greeter.key -out ssl_greeter.csr You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]: State or Province Name (full name) [Some-State]: Locality Name (eg, city) []: Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []: Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []:
証明書を発行します。
$ openssl x509 -days 3650 -in ssl_greeter.csr -req -signkey ssl_greeter.key -out ssl_greeter.crt Certificate request self-signature ok subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd $ ls docker-compose.yml src ssl_greeter.crt ssl_greeter.csr ssl_greeter.key
gRPC では、ssl_greeter.key と ssl_greeter.crt ファイルを利用します。
ソースコード
前回同様に Visual Studio Code で作成していきます。 greeter 直下で code コマンドを実行すると、Visual Studio Code が起動します。
$ code .
docker-compose.yml を編集します。 gRPC のポートは http が 5094、https が 7094 とします。 コンテナ内部では、http=80・https=443 とします。
version: '3.8' services: #gRPC (ASP.NET Core) app: container_name: 'greeter' build: context: ./src dockerfile: Dockerfile ports: - "5094:80" - "7094:443" environment: TZ: Asia/Tokyo volumes: - ./ssl_greeter.crt:/etc/ssl/certs/ssl_greeter.crt - ./ssl_greeter.key:/etc/ssl/private/ssl_greeter.key tty: true restart: always stdin_open: true
Dockerfile を編集します。
Microsoft の公式サイトに掲載されていた方法は、
構築するときは、sdk:xxx
と SDK イメージを使ってビルドし、
リリース時は、aspnet:xxx
と Runtime イメージを使って軽量化するようです。
これは、そのサンプルをベースに作成しています。
ASPNETCORE_URLS
でポートを指定します。
このサーバは gRPC のみなので、80番と 443番の標準ポートを使います。
証明書がない場合、エラーになって gRPC サーバが起動しないため、
;https://+:443
の箇所は削除してください。
# 本番用(.NET6 runtime) FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal AS runtime ENV ASPNETCORE_URLS=http://+:80;https://+:443 EXPOSE 80 EXPOSE 443 # 構築用(sdk) FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build ARG DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=0 WORKDIR /src COPY ["./GrpcGreeter/GrpcGreeter.csproj", "GrpcGreeter/"] RUN dotnet restore "./GrpcGreeter/GrpcGreeter.csproj" COPY . . RUN dotnet build "./GrpcGreeter/GrpcGreeter.csproj" -c Release -o /app/build FROM build AS publish WORKDIR /src RUN dotnet publish "./GrpcGreeter/GrpcGreeter.csproj" -c Releasse -o /app/publish # 本番用(runtime) FROM runtime AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "GrpcGreeter.dll"]
appsettings.json を編集します。 今回作った証明書を Certificates で指定します。
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "Kestrel": { "EndpointDefaults": { "Protocols": "Http2" }, "Certificates": { "Default": { "Path": "/etc/ssl/certs/ssl_greeter.crt", "KeyPath": "/etc/ssl/private/ssl_greeter.key" } } } }
後は、前回と同様です。
GrpcGreeter.csproj を編集します。
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <None Include="nlog.config"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> <ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Server" /> </ItemGroup> <ItemGroup> <PackageReference Include="Grpc.AspNetCore" Version="2.40.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.1.0" /> </ItemGroup> </Project>
nlog.config を編集します。tcpOutlet の IP アドレスは、各自 Windows の IP アドレスを指定してください。
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="Info" internalLogFile="./logs/internal-nlog-gRPC.log"> <extensions> <add assembly="NLog.Web.AspNetCore"/> </extensions> <targets> <!-- File --> <target name="logFile" xsi:type="File" encoding="UTF-8" writeBom="true" lineEnding="LF" layout="${longdate} ${level:uppercase=true:padding=-5} [${threadid}] ${logger} - ${message} ${exception:format=tostring}" fileName="${basedir}/logs/grpcgreeter.log" archiveFileName="${basedir}/logs/backup/grpcgreeter_{###}.log" archiveEvery="Day" archiveNumbering="Sequence" maxArchiveFiles="10" /> <!-- Console --> <target name="console" xsi:type="ColoredConsole" layout="${level:uppercase=true:padding=-5}: ${message}" /> <!-- Log2Console: WSLのアドレスではなく、通常のIPアドレスを指定すること! --> <target name="tcpOutlet" xsi:type="NLogViewer" address="tcp4://172.18.149.227:4505"/> </targets> <!-- rules to map from logger name to target --> <rules> <logger name="*" minlevel="Trace" writeTo="logFile" /> <logger name="*" minlevel="Trace" writeTo="console" /> <logger name="*" minlevel="Trace" writeTo="tcpOutlet" /> </rules> </nlog>
Program.cs を編集します。
using GrpcGreeter.Services; using NLog; using NLog.Web; var _logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); _logger.Debug("init main"); try { 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 // NLog: Setup NLog for Dependency injection builder.Logging.ClearProviders(); builder.Host.UseNLog(); // Add services to the container. builder.Services.AddGrpc(); var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGrpcService<GreeterService>(); 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(); } catch (Exception ex) { // NLog: catch setup errors _logger.Error(ex, "Stopped program because of exception"); throw; } finally { // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) NLog.LogManager.Shutdown(); }
ビルド&実行
Docker 資材をビルドします。
$ docker-compose build
ビルドが正常終了したら、起動します。
$ docker-compose up -d
コンテナが正常に動作していることを確認します。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c7fd83c99fd1 greeter_grpc "dotnet GrpcGreeter.…" 3 seconds ago Up 3 seconds 0.0.0.0:5094->80/tcp, :::5094->80/tcp, 0.0.0.0:7094->443/tcp, :::7094->443/tcp grpc
docker-compose logs
コマンドでログを確認します。
$ docker-compose logs grpc | DEBUG: init main grpc | TRACE: Discovering gRPC methods for GrpcGreeter.Services.GreeterService. grpc | TRACE: Added gRPC method 'SayHello' to service 'greet.Greeter'. Method type: 'Unary', route pattern: '/greet.Greeter/SayHello'. grpc | DEBUG: Hosting starting grpc | INFO : Now listening on: http://[::]:80 grpc | DEBUG: Loaded hosting startup assembly GrpcGreeter grpc | INFO : Application started. Press Ctrl+C to shut down. grpc | INFO : Hosting environment: Production grpc | INFO : Content root path: /app/ grpc | DEBUG: Hosting started
Log2Console を起動しているなら、以下のメッセージが表示されて正常に起動していることが確認できます。
docker-compose exec
コマンドで、コンテナ内に乗り込んで、ログを調べても構いません。
$ docker-compose exec app /bin/bash または $ docker-compose exec app bash # ls Google.Protobuf.dll GrpcGreeter NLog.dll Grpc.AspNetCore.Server.ClientFactory.dll GrpcGreeter.deps.json appsettings.Development.json Grpc.AspNetCore.Server.dll GrpcGreeter.dll appsettings.json Grpc.Core.Api.dll GrpcGreeter.pdb logs Grpc.Net.Client.dll GrpcGreeter.runtimeconfig.json nlog.config Grpc.Net.ClientFactory.dll NLog.Extensions.Logging.dll web.config Grpc.Net.Common.dll NLog.Web.AspNetCore.dll
正しく起動しなかったり作り変えるときは、
docker-compose down
コマンドを指定して最初から作り直します。
$ docker-compose down
gRPC クライアント
前々回、Windows 版で使ったものを利用します。 作り方の詳細は、前々回の記事を見てください。
Visual Studio 2022 でコンソールアプリを GrpcGreeterClinet の名前で作成します。 次に NuGet で以下のパッケージを追加します。
- Google.Protobuf
- Grpc.Net.Client
- Grpc.Tools
- NLog
nlog.config を追加し、プロパティの出力ディレクトリにコピーを「新しい場合はコピーする」に変更します。
事前準備
docker を作成した Ubuntu の IP アドレスを取得します。
$ ip a show dev eth0 6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:15:5d:88:c1:b8 brd ff:ff:ff:ff:ff:ff inet 172.18.126.178/20 brd 172.18.127.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::215:5dff:fe88:c1b8/64 scope link valid_lft forever preferred_lft forever
ソースコード
Program.cs を編集します。 IP アドレスを Ubuntu の IP アドレス 172.18.126.178 、ポートを 7094 に変更します。
using System.Net.Http; using System.Threading.Tasks; using Grpc.Net.Client; using GrpcGreeterClient; using NLog; var _logger = LogManager.GetCurrentClassLogger(); var server = "https://172.18.126.178:7094"; _logger.Info(server); try { var option = new GrpcChannelOptions() { HttpClient = new HttpClient(new HttpClientHandler { // SSL証明書の検証で常に True を返す ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, }) }; var channel = GrpcChannel.ForAddress(server, option); var client = new Greeter.GreeterClient(channel); var response = await client.SayHelloAsync(new HelloRequest { Name = "World" }); _logger.Info(response.Message); } catch (Exception ex) { _logger.Error(ex); }
GrpcGreeterClinet.csproj を編集します。
GrpcServices="Client"
に変更することを忘れないでください。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup> <ItemGroup> <PackageReference Include="Google.Protobuf" Version="3.21.5" /> <PackageReference Include="Grpc.Net.Client" Version="2.47.0" /> <PackageReference Include="Grpc.Tools" Version="2.47.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="NLog" Version="5.0.2" /> </ItemGroup> <ItemGroup> <Folder Include="Protos\" /> </ItemGroup> <ItemGroup> <None Update="nlog.config"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> </Project>
nlog.config を編集します。
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwExceptions="false" internalLogLevel="Info" internalLogFile="../logs/internal-nlog-Console.log"> <targets> <!-- File --> <target name="logFile" xsi:type="File" encoding="UTF-8" writeBom="true" lineEnding="CRLF" layout="${longdate} ${level:uppercase=true:padding=-5} [${threadid}] ${logger} - ${message} ${exception:format=tostring}" fileName="../logs/${processname}.log" archiveFileName="../logs/backup/${processname}_{###}.log" archiveEvery="Day" archiveNumbering="Sequence" maxArchiveFiles="10" /> <!-- Console --> <target name="console" xsi:type="ColoredConsole" layout="${level:uppercase=true:padding=-5}: ${message}" /> <!-- Log2Console --> <target name="tcpOutlet" xsi:type="NLogViewer" address="tcp4://localhost:4505"/> </targets> <rules> <logger name="*" minlevel="Debug" writeTo="logFile" /> <logger name="*" minlevel="Trace" writeTo="console" /> <logger name="*" minlevel="Trace" writeTo="tcpOutlet" /> </rules> </nlog>
greet.proto を編集します。csharp_namespace
を今回作ったプロジェクト名に変更します。
syntax = "proto3"; option csharp_namespace = "GrpcGreeterClient"; package greet; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply); } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings. message HelloReply { string message = 1; }
ビルド&実行
実行結果は Windows 版と同様なので、Log2Console の結果を掲載します。
Hello World
が最終行に表示されています。
ただし、Windows を再起動したので、Ubuntu のIPアドレスは変更されています。
まとめ
今回は、WSL2/Ubuntu で作った gRPC サーバを docker で実現しました。 前回の課題だった証明書も自作証明書を作って使いました。
前回まで、IP アドレスでアクセスするには、launchSettings.json の localhost を0.0.0.0 に置き換える 必要がありましたが、 docker 版では、環境変数 ASPNETCORE_URLS に http://+:80(443) のように 特別なことをしなくても、IPアドレスからアクセスすることができました。
docker-compose.yml と Dockerfile を使って作ると、 どちらに定義した方がいいのか悩むことがありますが、 両方に定義してみて、どちらでも動くようにすると理解が深まると思います。
続編として、Nginx サーバによるリバースプロキシを追加しました。
最後に参考サイトを掲載しておきます。 では、皆さん、よい旅を。