白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

.NET Framework(4.6.2)のコンソールアプリケーションを.NET(7)にマイグレーションしてみる

.NETのアプリケーションをLinux(Docker)で動かしたいのでマイグレーションをします。
.NET Coreからだったらそんなに変わらない気もするんですが、.NET Frameworkだとまぁまぁ困惑します。

.NET 7って?

そもそも.NET 7って何だろうってところから。
もともとはWindows専用の.NET Frameworkクロスプラットフォームの.NET Coreという構成でしたが、.NET Frameworkが4.8で開発終了して、.NET Core 3.1の次のバージョンが.NET 5という形で統合したみたいです。
.NET 4じゃなくて.NET 5なのは.NET Framework 4系との混同を避けたからとか。

以降は.NET 6、.NET 7、.NET 8(予定)と続いていくようです。
偶数がLTSなので現在は.NET 6が安定、.NET 7が最新という感じでしょうか。

とりあえずプロジェクトを作る

VisualStudioからコンソールアプリを選んで、

フレームワークに.NET 7.0を選ぶと、

なんともシンプルなProgram.csが作られます。

プロジェクトファイルもシンプル。

.NET 6からはProgram.csクラスとかMainメソッドとかはコンソールアプリテンプレートが勝手に作ってくれるらしいです。
そして暗黙的なusingによってももろもろもusingもなくなって本文だけ書けば動く。おー便利。
クラシックなのが良ければ「最上位レベルのステートメントを使用しない」にチェックを入れればよさそう。(ImplicitUsingsの設定は別…?)

発行してみる

プロジェクトを右クリックして発行を押すと発行のプロファイルが作られます。
プロファイル設定を細かく見るとこんな感じ。

ここでフレームワーク依存⇔自己完結とかが選べるし、ターゲットランタイムもいろいろ選べます。簡単マルチプラットフォーム
ここでlinux-x64とか選んで発行すれば、あとはもうdotnetコマンドでLinux上で動かせます。

App.configがないです

設定とか書きたいですが、App.configがなくなってしまいました。悲しい。
代わりにappsettings.jsonを使います。

まずプロジェクトファイルにNuget参照を足して、

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
  </ItemGroup>

こんな感じでjsonファイルを作って、

{
  "ConnectionString": "せつぞくもじれつ"
}

こんな感じで取れます。

var config = new ConfigurationBuilder().AddJsonFile("appsettings.json", true, true)
                                       .Build();
var conStr = config.GetValue<string>("ConnectionString");

デバッグ用で変えたいときとかはデバッグプロパティから環境変数を設定して

appsettings.Development.jsonとか作ってこう。

var environmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
var config = new ConfigurationBuilder().AddJsonFile("appsettings.json", true, true)
                                       .AddJsonFile($"appsettings.{environmentName}.json", true, true)
                                       .Build();
var conStr = config.GetValue<string>("ConnectionString");

ログ機能も欲しいです

コンソールアプリなのでログ機能も欲しいところです。
.NETだとMicrosoft.Extensions.Loggingを使うのが簡単そうです。

またNuget参照を足して、

  <ItemGroup>
    (略)
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
  </ItemGroup>

こんな感じ。これでコンソールログとデバックログに書き出せます。

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole().AddDebug();
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Hello, World!");

ファイル出力したいならNLogとかを使って、

  <ItemGroup>
    (略)
    <PackageReference Include="NLog.Extensions.Logging" Version="5.2.3" />
  </ItemGroup>

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">

  <targets>
    <target name="logFile" xsi:type="File" encoding="UTF-8" fileName=".\logfile.log" />
  </targets>

  <rules>
    <logger name="*" minlevel="Trace" writeTo="logFile" />
  </rules>
</nlog>

こんな感じ。

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole().AddDebug().AddNLog("NLog.config");
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Hello, World!");

便利なような?

.NET Frameworkからだと結構違うところがありますが、実は.NET Core時代からあったものばかりですね。
結構基本的な機能がNugetになっててあれってなることがちょいちょいありました。
あと今回全然触れていないですが、C#自体のバージョンがだいぶ上がってるのでそっちでも変化が…。

次回は今回ごにょごにょした設定ファイルとかログ周りをいい感じにしてくれる汎用ホストという機能を触ってみます。

目標設定何にする?

こんばんは。
気が付けば新年度ももう半月以上が過ぎましたね。
私は毎日がドタバタです…。

さて、新年度といえば皆さん頭を悩ませていますか、目標設定。
何を書こうか迷ったり、何の意味があるのかわからなかったり、あんまりいい思い出はないですかね。

せっかくなのでここらで一つ、目標設定に思いを馳せてみましょう。
これが正しいとかじゃなくて、私の思っていることを徒然と文章にしてみたものだと思って読んでもらえれば。

そもそも目標設定とは何なのか

目標って何を書けばいいんだという前に、そもそもなんで会社で目標設定なんてものをするのかを気にしてみましょう。
日本の企業で取り入れられている目標設定制度は主にMBOというものです。
目標による管理 - Wikipedia

OKRなどもありますが、こちらはもっと短いスパンで設定するのでこの時期にあわあわするならMBOでしょう。
OKR - Wikipedia

とてもシンプルに言うと「社員自身が自らの業務目標を設定することで、社員の主体的・自主的な取り組みを促すための仕組み」くらいでしょうか。
本来的には人事評価の手法ではないですが、進捗や達成度合いによって人事評価を決める手段として用いられることが多いです。

MBOの進め方

あんまり詳しくは書きませんが、ざっくりとこんな感じですかね。

①目標を明確にする
②目標を達成するための計画を立てる
③計画を実行する
④結果を振り返り、評価を行う

そりゃそうだよねって感じですよね。
でも本当にちゃんとこの通りに進むように目標設定できているでしょうか?

・手段だけを書いて目標が書いていなかったりしませんか
 →〇〇をやる(それをやると何が嬉しい?どう成長する?)

・目標を達成するための計画が伴っていますか
 →〇〇になる(どうすればその状態になれる?)

・振り返って評価できるような目標になっていますか
 →〇〇に取り組む(どう取り組んだらOK?どんな結果がでたらOK?)

よく考えると当たり前なんですが、意外とうまくいかなかったりするポイントです。

もうちょっと踏み込んでみる


目標は上司や組織との約束事

目標は私はこんなことをします、こんな風に成長しますという約束です。
上司や組織が期待することと自分自身がやろうと思っていることが噛み合っているかを確認し、何を目標にどんな手段を取るのかをちゃんと宣言しましょう。

注意としては、約束していないことをいくら頑張っても評価されません。(されることもあるかもしれないけど保証はされない)
意外と忘れてしまいがちなのですが、自分はこんなに頑張ったのに全然評価してくれないというときはここができていないことが多いです。すり合わせ大事。
もちろん後からやりたいことが出てくることもあると思いますが、そのときにも随時宣言したり目標を修正したりする必要があります。

評価者はこんなことができるようになったら嬉しい、こういったポイントを評価するよというのをちゃんと伝えたほうが良いです。
手段ではなく観点で伝えられるとGoodです。

その目標は本当に意味のあることか

目標は、自分自身や組織にとって本当に重要なことでなければあまり意味がありません。
その目標が自分自身や組織にとってどの程度重要であるか、どのように影響を与えるかを考える必要があります。

目標を達成するための目標になってしまって、実は特に誰も嬉しくないみたいになっていないか今一度確認してみましょう。
自分ができそうなことをやるのではなく、できたら嬉しいことをやるのが大切です。

そのためには今持っている能力で何ができるかではなく、目標を達成するためにはどんなスキルや知識が必要かを考える必要があります。
それがつまり、この目標に取り組み、達成することであなたがどう成長するかそのものになるわけです。

評価者はメンバーの成長が大きな課題となりますが、結局何を成長させたらいいかわからないとなりがちです。
本当に取り組むべき課題(イシューというやつですね)の達成のためにどんな成長が必要かから逆算すると、どう成長してもらうかがわかりやすくなりそうです。

現状と理想の差分はなにか

目標達成のための取り組みとは、言ってみれば現状と理想の差分を埋めることです。
何をすればその差分が埋まるのかと実際の取り組みがうまく噛み合っているかを確認しましょう。
取り組み側から考え始めるとどうしても着地点が変なところになりがちです。

評価者はこの現状と理想の差分をうまく引き出してあげることが大切です。
特に何も言わなくてもこれができる人もいますが、壁打ちが足りないとなかなかうまく文章や言葉にできない人もいます。
「それってなんでやりたいんだっけ」とか「どういう状態になっているのが理想なんだっけ」とか話しているうちにいい言葉が出てきたりします。

ちなみによくありがちなのが理想自体に補正がかかってしまうパターンです。
「本当の理想は〇〇なんだけど現実的にできるラインを考えると…」みたいに理想自体がずれてしまっていることがあります。
理想に対して計画的にどこまで進めるのかというのも立派な目標です。
100%理想の状態になることが目標達成ではないので、本当の理想は何なのか聞いてみると意外と大きな夢が聞けたりするかもしれません。

設定がゴールじゃないぞ

目標設定が終わるとついひと仕事終えた気持ちになりますが、ここからが本番です。
立てた計画に沿ってしっかりと進め、進捗を確認したり振り返りをしたり、振り返った結果を次に活かしたり。

まぁでもやっぱり大変ですからね。
ちゃんと目標が立てられると結果を書くときにも楽だし、とりあえず目標設定がいい感じにできるようになると幸せですよね。

で、私はなんの目標を書こうかな…。

Docker ComposeでMySQLとnginxのコンテナを立てる

前回の続きです。

参考にさせていただきました。
zenn.dev
note.com

今回はDocker Composeを使うので構築まであれこれ試行錯誤はしたものの、記事にすると手順自体はdocker-compose.ymlをぺたりと貼るだけのところに着地します。

DockerfileとDocker Compose

Dockerのサイトによると、
Dockerfileは「イメージを自動で作成するためのコマンドを含んだテキストファイル」という感じでしょうか。

Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. This page describes the commands you can use in a Dockerfile.

Docker Composeは「複数コンテナーのDockerアプリケーションを定義して実行するためのツール」ということですかね。

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.

というわけで文章だけ見ると複数のコンテナを使う場合だけDocker Composeを使い、単一の場合にはDockerfileを使うと良いみたいに取れますが、実際には単一のイメージを使う場合にもDocker Composeは便利ですし、DockerfileとDocker Composeを組み合わせて使うこともよくあるようです。

どちらかというとDockerfileはイメージを作る作業を自動化したもので、Docker Composeは作ったイメージをどういう設定で動かすか指定するものくらいでいいんじゃないでしょうか。
もちろんその中に複数イメージをどう連携させるかっていう話も入ってくるのですが…。
今回はイメージは基本的に配布されているものをそのままで、dockerコマンドの引数的なものを全部Docker Composeに入れるようなイメージで作っていきます。

ネットワークを作る

docker-compose.ymlにサービスを複数書くと勝手にネットワークができてコンテナ間の参照ができます。
が、今回は1つのサービスとして全部セットでdocker-compose.ymlに書く方式は取らないため、普通にネットワークを作ってそこに参加させる方式を取ります。

ひとまずDBを参照できるネットワークとnginxを経由してHTTPアクセスできるWebのネットワークを作成します。
とりあえず既存のネットワークを見てみると、何もないかと思いきや「bridge」「host」「none」の3つのネットワークが最初からあります。
これらについては詳しく解説してくれているサイトなどがあるので特には触れません。

$ docker network ls
NETWORK ID     NAME    DRIVER    SCOPE
440f88691de8   bridge  bridge    local
c9d361d10742   host    host      local
92c07b77e335   none    null      local

DB用とWeb用のネットワークを作りました。何も指定しないとbridgeネットワークになります。

$ docker network create db_network
$ docker network create web_network
$ docker network ls
NETWORK ID     NAME                       DRIVER    SCOPE
440f88691de8   bridge                     bridge    local
46521ed7ae0a   db_network                 bridge    local
c9d361d10742   host                       host      local
92c07b77e335   none                       null      local
6ea3a2e26663   web_network                bridge    local

docker-compose.ymlを書く

まず好きなところにディレクトリを作ります。
作ったディレクトリにdocker-compose.ymlを置きます。

はい。

version: '3.8'
services:
  mysql:
    image: mysql: 8.0.32
    container_name: mysql
    ports:
      - 3306:3306
    environment:
      TZ: "Asia/Tokyo"
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
    volumes:
      - ./data:/var/lib/mysql
    networks:
      - db_network
networks:
  db_network:
    external: true

ちょっと解説。
servicesの下に登録したいサービスを書きます。複数書いたら複数同時にコンテナを立ち上げられます。
imageにはDockerImageを指定します。Dockerfileを指定する場合はbuildを使います。ここでは省略。
container_nameはコンテナ名です。docker psで表示されるコンテナ名や、他のコンテナから参照するときの名前です。
portsはホスト側のポートとコンテナ内のポートを繋ぎます。この例だとホストの3306ポートへのアクセスはこのコンテナに転送されます。(アプリケーションで使うだけならホスト側にポートを繋ぐ必要はないかもしれませんが、普通に中身が見たいので設定)
environmentは環境変数です。利用したいDockerImageごとに使うものはバラバラですが、ここに初期値を与えることでコンテナ起動時に色々勝手にやってくれることが多いです。(MySQLの場合はrootのパスワードや初期作成するDBの情報が書けます。複数DB作るのはDockerfileでやってねってことかな…?)
volumesはボリュームのマウント設定です。ホスト側のディレクトリをコンテナ内に繋ぐときに使います。後ろに「:ro」をつけると読み取り専用になります。
networksは参加させるネットワーク名です。今回は既に作成してあるdb_networkを使います。(外にあるネットワークなのでexternal: trueが必要)

で、はい。

version: '3.8'
services:
  nginx:
    image: nginx:1.23.3
    container_name: nginx
    ports:
      - 80:80
    environment:
      TZ: "Asia/Tokyo"
    volumes:
      - ./config/default.conf:/etc/nginx/conf.d/default.conf
      - ./log:/var/log/nginx
      - ./html:/var/www/html
    networks:
      - web_network
networks:
  web_network:
    external: true

設定をいじったり、ログを外側に吐いたり、htmlを置いたりできるようにマウントしているくらいですかね。

default.confは一旦こんな感じでマウント元のディレクトリにおいておきます。
とりあえず./htmlにindex.htmlとか置くと見れます。他にはなんのアプリケーションもないので404。

server {
    listen 80;		# IPv4
    listen [::]:80;		# IPv6
    server_name ubuntu-server;	# 受け付けるホスト

    # なんかもろもろのヘッダ
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Host  $host;
    proxy_set_header   X-Forwarded-Port  $server_port;

    root /var/www/html;	# 静的ページのroot
    
    # 400番台エラーのハンドリング(root上書きしてるので戻す)
    error_page 404 /404.html;
    location = /40x.html {
        root /usr/share/nginx/html;
    }
    
    # 500番台エラーのハンドリング
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

これでそれぞれのディレクトリで以下のコマンドを叩くとコンテナが立ち上がります。

$ docker compose up

が、フォアグラウントで実行しても困るので、バックグラウンドで実行する場合はdオプションを付けます。

$ docker compose up -d

バックグラウンドだとログとか何も見えないので、ログが見たい場合には「docker logs」コマンド。

$ docker logs nginx
~()~
/docker-entrypoint.sh: Configuration complete; ready for start up

これでDBを使うときはdb_networkに、nginxの裏側でWebサービスを公開する場合にはweb_networkに繋げばOKという状態になりました。