tanabebe blog

EditorConfigを使ってC#のコーディングルールを統一させる

f:id:tanabebe:20210420190419p:plain

1. 背景

いきなりですが,皆さんが業務でプログラムを書くにあたり,コーディングルールは定められていますか?
過去にはありましたが,今は使われていないですとか,ルールはないですねなど.割と思い当たる節があるのではないでしょうか?私も身に覚えがあります.
では,どう解決するべきだろうと考えた結果,EditorConfigを使用してコーディングルールを統一することにしました.タイトルでも触れている通り,言語はC#です.

2. EditorConfigとは

EditorConfigGoogle翻訳にブチ込むと以下とあり,コーディングスタイルを統一するのにうってつけですね.

EditorConfigは、さまざまなエディターやIDEで同じプロジェクトに取り組んでいる複数の開発者に対して一貫したコーディングスタイルを維持するのに役立ちます。 EditorConfigプロジェクトは、コーディングスタイルを定義するためのファイル形式と、エディターがファイル形式を読み取って定義されたスタイルに準拠できるようにするテキストエディタプラグインのコレクションで構成されています。 EditorConfigファイルは読みやすく、バージョン管理システムでうまく機能します。

3. EditorConfig for VS Codeのインストール

EditorConfig for VS Codeをインストールします.

f:id:tanabebe:20210419130455p:plain

4. VS CodeでOmnisharpを有効化する

拡張機能C#にはOmnisharpが搭載されているので関連設定を有効化します.VS Code⌘ + ,もしくはCode→基本設定→設定からOmnisharpで検索します.以下の2項目にチェックを入れます.

  • Omnisharp: Enable Editor Config Support
  • Omnisharp: Enable Roslyn Analyzers

f:id:tanabebe:20210419130613p:plain

チェックを入れたら⌘ + shift + pもしくはF1でコマンドパレットにreloadと入力してReload Windowを選択,VS Codeの再読み込みを行います.

5. ビルド時に有効とさせる

実はこれについては設定がわからなくて少しハマりましたが,Microsoft Docsに書いてありました.目を凝らさないと見落としてしまうので気を付けなければ...
ビルド時にIDEコードスタイルのルールを有効としたいので,対象の.csprojファイルに以下を追加します.

<PropertyGroup>
  <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

これで準備完了です.あとは.editorconfigファイルを作成して設定していきます.

6. コーディングルールを設定していく

EditorConfig for VS Codeをインストールされていると,エクスプローラー上で右クリック時にGenerate .editorconfigの項目が出るので選択します.私はプロジェクトのルートに置きました.

f:id:tanabebe:20210420140535p:plain

一部を抜粋しました.コーディングルールについてはこちらを参考に記載していきましたが,ルールの量も多いので完成への道のりはまだ遠いです.ちなみに前述のビルド有効時の設定値としてIDE0055だけ記載しています.ルールIDは多いのでドキュメントを眺めて適宜設定しましょう.

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# All files
[*]
indent_style = space

# JSON files
[*.json]
indent_size = 2

# CSharp code style settings:
[*.cs]
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = warning

# 改行オプション
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true

# インデントオプション
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents_when_block = true

# varの設定
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion

例えばdotnet_diagnostic.IDE0055.severity = warningに絞って見ていくと,このルールに該当した場合は以下のように警告が表示されます.

f:id:tanabebe:20210419130749p:plain

インデント,スペース,および改行が統一されていない場合はこれに該当します.この例では無駄なスペースが入っているのが原因です.このようにEditorConfigで,動作に関わらない小さなノイズも検知出来ます.
ここではwarningとしていますが,errorと記述することでビルド時のエラー対象としても扱うことが出来ます.委ねられている感じがありがたい.

7. まとめ

VS CodeでEditorConfigを使用してコーディングルールを定義する方法について書きました.今回はC#で進めましたが,もちろん他の言語にも対応しています.作成した.editorconfigファイルをリポジトリ管理すればチーム共有も非常に楽です.
とはいえ記載した内容では完成には程遠いので,今後も理解を深めながら進めます.C#で言うとroslynが参考になりそうでした.こういったルールはついおろそかにしてしまうので,いつでも引き出せるように事前準備が大事ですね.

8. 参考

www.neputa-note.net

editorconfig.org

docs.microsoft.com

docs.microsoft.com

docs.microsoft.com

github.com

.NET5でappsettingsをDIし、独自クラスにバインドする方法

f:id:tanabebe:20210329223028p:plain

DB接続文字列にappsettings.jsonファイルを使用しようと思った時に、すっかり方法を忘れていたので、今回は.NET5でJSONファイル構成プロバイダーを扱う方法について書きます。何パターンか方法はありますが、クラスにバインドする方法で進めていきます。また、作成したプログラムはGitHubにあげています。

github.com

1. 環境

❯❯❯ dotnet --version
5.0.103

Web APIのテンプレート(dotnet new webapi -n Apiなど)でプロジェクトが作成されている前提で進めます。

2. JSON構成ファイルを修正する

appsettings.Deplopment.jsonファイル内に下記のSettings以降を追加します。私はConnectionStringsに自身のAzure SQL Databaseの接続文字列を設定しました。環境毎に読み込み先のjsonファイルを変更する方法には触れませんので、詳細はこちらをご覧ください。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Settings": {
    "Database": {
      "ConnectionStrings": "Your connection string"
    }
  }
}

3. 読み込んだJSON構成ファイルをバインドするクラスを作成

簡素ですが、Databaseというクラスを作成しています。

// DapperTest/Api/Database.cs
public class Database
{
    public string ConnectionStrings { get; set; }
}

4. サービスコンテナに処理を追加する

Startupクラスが呼ばれるタイミングで構成ファイルを読み取り、前述で作成したDatabaseクラスにバインドするように処理を追加します。
JSON構成ファイル内のKeyバインドするクラスのプロパティが一致しない場合 、値は取得出来ないので注意。クラス名は大丈夫です。

// DapperTest/Api/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<Database>(Configuration.GetSection("Settings:Database")); // 追加
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api", Version = "v1" });
    });
}

5. 対象クラスのコンストラクタでインジェクションする

ここではDapperTestControllerというクラスを作成しています。コンストラクタでIOptionsMonitorを受け取り、_optionsに設定しました。これで値を取り出す事が出来ます。ここではSqlConnectionで使用しています。これにて完了。

public class DapperTestController : BaseController
{
    private readonly Database _options;

    public DapperTestController(IOptionsMonitor<Database> options)
    {
        _options = options.CurrentValue;
    }

    [HttpGet]
    public async Task<ActionResult<List<Customer>>> GetSample()
    {
        using (var db = new SqlConnection(_options.ConnectionStrings))
        {
            var results = await db.QueryAsync<Customer>("Select * From Customers");
            return results.ToList();
        }
    }
}

6. まとめ

今回はDapperとの接続方法を試していた際に少しハマってしまった、JSONファイル構成プロバイダーを扱う方法について書きました。
そもそもですが、DIは知ればこそ強力ですね。newでインスタンス化するよりスマートです。ただし、隠蔽されている感じが強くあるので、DIを理解していない場合は命取りにはなりそう。
前述のコンスタラクタでIOptionsMonitorを受け取っていますが、Microsoft Docsを見る限り、これには3種類のオプションがあります。今回は常に最新のオプション値を取得するとあったIOptionsMonitorを使用しました。IOptionsSnapshotも試したところ同じような挙動はするのですが、使い所がパッと思い浮かばなかった、というのが正直な感想です。

7. 参考

docs.microsoft.com

docs.microsoft.com

blog.ecbeing.tech

www.amazon.co.jp

.NET 5 × Entity Framework Core でサクッとデータベースとシードデータを作成する

f:id:tanabebe:20210317221145p:plain

1. 環境

2. Entity Framework Coreとは

.NET用のORMです。2016年中頃に.NET Coreへ導入されました。
Entity Frameworkを完全に書き換えたものなので、パフォーマンス面でも大幅に改善されています。もちろんLINQ対応しています。
ざっくり言ってしまうと通常記述しなければいけないコードを簡略化できる便利な仕組みです。
※参考 - Entity Framework Core の概要

サクッと行けるのでやっていきます。
今回作成したプログラムはGitHubにあげています。

github.com

3. プロジェクトの準備

.NETもしっかりCLIがあるのでコマンドを叩いて準備していきます。まずはディレクトリを作成します。

❯❯❯ mkdir DatabaseTest
❯❯❯ cd DatabaseTest/

次はソリューションファイルとプロジェクトを作成します。

DatabaseTest ❯❯❯ dotnet new sln
DatabaseTest ❯❯❯ dotnet new webapi -n Api

どのようなテンプレートを扱えるか忘れた場合、以下のコマンドを打つと良いです。

DatabaseTest ❯❯❯ dotnet new -l

とりあえず困ったらdotnet -hでヘルプを見ましょう。

ソリューションにプロジェクトを追加します。

DatabaseTest ❯❯❯ dotnet sln add Api

4. VS Codeで作業していく

ここからはVS Codeで進めていきます。

まず、以下のNuGet Packageをインストールします。

Nuget Gallery拡張機能がインストールされている前提です。

Nuget Gallaryを開くと以下のような画面になるのでプロジェクトを選択し、インストールします。

f:id:tanabebe:20210317111842p:plain

4.1. モデル用クラスを追加

実際は責務を分けるために別プロジェクトで作成するのが良いですが、今回はApiプロジェクトにまとめていきます。

// DatabaseTest/Api/Model/User.cs
public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public double Height { get; set; }
    public double Weight { get; set; }
}

4.2. DbContextを継承した派生クラスを作成

DataContextのコンストラクタがないとエラーを吐くので忘れずに追加しましょう。VS Codeだと .のショートカットキーを使うとサクッと生成出来ます。
ちなみに変数名がテーブル名となり、この例だとUsersというテーブルが最終的に作成されます。

// DatabaseTest/Api/DataService/DataContext.cs
public class DataContext : DbContext
{
    public DataContext(DbContextOptions options) : base(options)
    {
    }
    public DbSet<User> Users { get; set; }
}

4.3. Startup.csの修正

以下の通り、Startup.csを修正してSQLiteの依存関係を追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api", Version = "v1" });
    });
    // 追加
    services.AddDbContext<DataContext>(opt =>
    {
        opt.UseSqlite(Configuration.GetConnectionString("DefaultConnections"));
    });
}

Api/appsettings.Development.jsonに接続文字列を追加します。

{
  "ConnectionStrings": {
    "DefaultConnection": "Data source=database.db"
  }
}

ついでにApi/Properties/launchSettings.jsonも修正します。アプリケーション実行時に毎回ブラウザが起動するのは煩わしいので、対象プロジェクト(ここではApi)に記述されているlaunchBrowserfalseにし、ブラウザ起動を抑止します。

{
    "Api": {
      "commandName": "Project",
      "dotnetRunMessages": "true",
      "launchBrowser": false,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
}

データを投入するクラスを作成します。

// Api/DataService/Seed.cs
public static async Task UserData(DataContext context)
{
    if(context.Users.Any()) return;

    var users = new List<User>
    {
        new User 
        {
            Name = "Suzuki",
            Age = 20,
            Height = 172.1,
            Weight = 60.4
        },
        new User
        {
            Name = "Tanaka",
            Age = 30,
            Height = 182.5,
            Weight = 72.4
        },
        new User
        {
            Name = "Yamada",
            Age = 25,
            Height = 160.5,
            Weight = 48.9
        },
        new User
        {
            Name = "Sato",
            Age = 22,
            Height = 175.6,
            Weight = 65.2
        }
    };
    await context.Users.AddRangeAsync(users);
    await context.SaveChangesAsync();
}

dotnet-efがインストールされているか確認します。

DatabaseTest ❯❯❯ dotnet tool list --global
パッケージ ID       バージョン      コマンド
-----------------------------------
dotnet-ef      5.0.4      dotnet-ef

dotnet-efが表示されていなければインストールしましょう。もし、既にインストールされていれば以下のコマンドはスキップしてOKです。

DatabaseTest ❯❯❯ dotnet tool install --global dotnet-ef

マイグレーションします。

DatabaseTest ❯❯❯ dotnet ef migrations add InitialCreate -s Api/

ここまで来たらdotnet ef database updateのコマンドを打つことでデータベースが作成出来ますが、今回はアプリケーション実行時にデータベースの作成、シードデータを投入したいので最後にProgram.csMainメソッドを修正します。

// Api/Program.cs
public static async Task Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    // C# 8.0以降
    using var scope = host.Services.CreateScope();
    var services = scope.ServiceProvider;

    try
    {
        // GetServiceでnullチェックでも良い
        var context = services.GetRequiredService<DataContext>();
        // 待機しないと怒られます
        await context.Database.MigrateAsync();
        // シードデータ投入
        await Seed.UserData(context);
    } catch (Exception e)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(e, "migrateでエラーが発生しました。");
    }
    await host.RunAsync();
}

ようやくアプリケーションの実行です。dotnet watch runとしているのはファイルの更新を検出し、自動的にビルド、アプリケーション起動が出来るので常用しています。

DatabaseTest ❯❯❯ cd Api/
Api ❯❯❯ dotnet watch run

実行するとターミナルにログが出力されます。内容を見るとCREATE TABLEINSERTが実行されているのがわかります。appsettings.Development.jsonで設定したdatabase.dbファイルが作成されているので確認します。
VS CodeのコマンドパレットからSQLiteを選択します。

f:id:tanabebe:20210317111847p:plain

database.dbを選択します。

f:id:tanabebe:20210317111853p:plain

SQLite Explorerが表示されるのでUsersテーブルが作成されていることが確認出来ました。最後にデータが投入されているか確認します。
問題なく作成されています。良かった良かった。

f:id:tanabebe:20210317111900p:plain

5. まとめ

Visual Studioからポチポチとやってしまう事が多かったので、CLIVS Codeで作成するケースを書き起こしてみました。
今回はモデルに変更を加えた後にどういった手順を踏むかについては触れていませんが、ゴリゴリとプログラムを書くことなく、データベースとテーブルの作成、シードデータの投入が出来ました。
なんて楽なんだ。あとはControllerDataContextを注入して、LINQで書いていく感じです。公式にもチュートリアルがあるので、以下を参考にするのも良いです。

docs.microsoft.com

こちらを見る限り、データ操作だとORMに関してはEntity Framework CoreとDapperが書かれているので、ORMを使った場合はこの2択なのかな?というところです。
2016年の記事ですが、以下でパフォーマンスのデータを提供している記事もありました。Dapperも今度試します。

docs.microsoft.com

Microsoft Azure Fundamentalsを受けてきた

f:id:tanabebe:20201015093652p:plain

背景

業務でAzureを使ってはいるもの基礎があるかどうかは良くわからないという感じでした。受けてみようかなぁ...という想いはあったので「来週時間出来たな...せや!資格取得や!というノリで申し込みました。
実際に受験してみた結果どうだったのかという体験記となります。

Microsoft Azure Fundamentalsとは

Microsoftの認定資格の1つとなります。AZ-900と言うとこの資格を指します。

Exam AZ-900: Microsoft Azure Fundamentals - Learn | Microsoft Docsdocs.microsoft.com

では,どんな知識が必要なの?というところですが,公式を見ると以下のとおりです。

Azure Fundamentals 試験は、クラウドの概念、 Azure サービス、 Azure ワークロード、 Azure のセキュリティとプライバシー、 Azure の価格とサポートに関する知識を証明する機会です。受験者は、ネットワーキング、ストレージ、コンピューティング、アプリケーション サポート、アプリケーション開発の概念を含む一般的なテクノロジの概念に精通している必要があります。

ふむふむ...この資格を持っていることでAzureの基礎知識を有している事の証明が出来るということですね。 ロールベースを見ても初級の認定資格となっているので気は少し楽です。とはいえ,よほどの自信がない限りノー勉で臨むというのは無謀かもしれません。

準備期間

受験へ向けた勉強期間はきっちり1週間と決めて臨みました。
ベンダー資格を受けるのは初めてだったのと,常に子供と2人きりステータスなので育児の折り合いも含めると,もっと緩い線を引いた方が精神的には良かったです。

何をしたか

  1. Microsoft LearnのAzureの基礎を完了させる
  2. テキスト&問題集を周回する
  3. 正答率が低い所の知識補填をする

この流れで勉強を進めていきました。

1〜2日目(土〜日)

まず「ここは良くわからん...」という部分はメモりつつMicrosoft LearnでAzureの基礎を完了させました。
もちろん理解が薄い所もありましたが,まずは全体像の把握をして後述の問題集を解いて苦手部分を埋めようと考えてました。あまりに躓きが多く,毎度調べてネットの海へ潜ると本質から逸れていくので,躓きががあっても一度は通した方が圧倒的に良いです。

docs.microsoft.com

3日目〜6日目(月〜木)

Microsoft Learnを始めたタイミングでこちらのテキスト&問題集を購入しておいたので,この期間中は出来るだけ周回するようにしました。

www.amazon.co.jp

問題に慣れるというところと,Microsoft Learnで行った基礎の復習も出来たので購入して大正解でした。 周回すると正答率の良し悪しの切り分けが出来るので,頭に入っていない箇所が洗い出せます。

7日目(金)

私の場合セキュリティ,プライバシー,コンプライアンス部分の正答率が特に悪かったので,Microsoft LearnAzureの基礎問題集で該当箇所の復習をしました。ただ,この日に直前になって別ページ(改訂された?)を見つけて「もしかして参照しているところ違う?」と不安にかられましたが,「大丈夫!イケる!」と脳内汚染して早めに寝ました。

docs.microsoft.com

受験当日

前日(7日目)と同じく正答率があまり良くない部分だけ見直して臨みました。なので受験当日はほぼ何もしていないに等しい感じですね。
実際の問題についてはNDAもあるので詳細を事細かく書くことは出来ませんが,購入した問題集と類似した問題も多かったです。
30分ほどで全問完了できたものの,「え?この問題に該当する資料ってどこにも無かったよね?」というのもあったので「落ちそうだなぁ…」という感触でした。問題によってはあとで問題を見直すなど選択出来るのですが,潔くポチッと終了させてしまいました。
合格基準の点は700/1000なのですが,結果は...



















780とギリギリでした...あぶねぇ...

すぐに結果がわかるのは知っていたので相当ドキドキしましたが,結果が良いモノだとやはり嬉しいですね。
合格後はMicrosoft認定ダッシュボードにて認定証の閲覧や認証バッジなどがダウンロード出来ます。
また,受験完了後に知りましたがMicrosoftMicrosoft Azure Virtual Training Dayという無料トレーニングを頻繁に行ってくれているので,こちらに参加するのも有効だと思います。

docs.microsoft.com

まとめ

座学が本当に苦手な私でも期限縛りを科すことでギリギリ合格することが出来ました。
初級レベルと言うだけあり合格への敷居は低いという印象です。履歴書に自信を持ってかける資格ではないかもしれないですが,Azureに足を踏み入れる方は基礎学習という点でこの資格を入り口として良いのではないでしょうか。これを機に次はAzure Developer Associateを目指します。

push-to-hatenablogを使い,はてなブログへの投稿記事をGitHubで管理したら最高だった!

1. 背景

ブログを書く時,私の場合「Visual Studio Code(以降 VS Code」で書いて「はてなブログ」へコピペで投稿する.といった手順を踏んでいます.

記事を書く時は「VS Code」に集中したい. commit時はtextlintを走らせて投稿記事をリポジトリで管理したい.

という想いはあれど怠けていたので,解消すべく実現しました. 今回は「push-to-hatenablog」を導入して「GitHub」と「はてなブログ」を連携する方法を紹介していきます.

2. 前提条件

以下が導入済み.

  • Docker
  • VS Code(お気に入りのIDEで問題ないです)
  • GitHubアカウント

3. push-to-hatenablog

github.com

なんと,はてなブログ連携用の環境を公開してくれております. こちらを使うとリポジトリpushした際に「GitHub Actions」が走ります.push.yml内を見てるとblogsyncと連動し,masterブランチと別ブランチの差分を見て記事の更新を自動で行うようになっています.便利すぎる.
blogsyncについてはこちら.

github.com

4. 設定していこう

4.1. GitHubにブログ管理用のリポジトリを作成する

privateのリポジトリを作成します.この時点ではファイルを追加する必要はないです.

f:id:tanabebe:20200624164757p:plain

4.2. push-to-hatenablogをダウンロードする

cloneでも問題ないのですが,ここではダウンロードします. ダウンロードしたpush-to-hatenablog-master.zipを解凍し,フォルダをリネームして配置します.このフォルダ配下がリポジトリ管理対象となります.

f:id:tanabebe:20200624165335p:plain

4.3. blogsync.example.yamlを修正する

解凍したフォルダ内にblogsync.example.yamlというファイルがあるので,blogsync.yamlへリネームし,以下の内容で変更します.

[ブログのドメイン]:
  username: [user name]
  password: [AtomPubのAPI KEY]
default:
  local_root: entries

上記の「password」については,「はてなブログ」の管理ページにて「設定->詳細設定->AtomPubのAPIキー」で確認出来ます.

4.4. GitHubでSecretを作成

GitHub Actions」で機密情報を保護するためにSecretを使います.push.ymlファイルを見るとこの中でSecretを使用しています.Creating and storing encrypted secretsのページへ飛ぶと説明がありますが,以下の画面からSecretの登録が可能です.

f:id:tanabebe:20200624165401p:plain

New secretのボタンを押すとNameValueの入力項目が表示されるので下表の通りDOMAINBSYSecretを1つずつ作成します.

Name Value
DOMAIN ブログのドメイン
BSY
[ブログのドメイン]:\n
username: [ユーザ名]\n
password: [AtomPubのAPIキー]\n
default:\n
local_root: entries

4.5. GitHubリポジトリpushする

ここまで設定出来たら,コマンドを叩いてブログ記事をpullしていきましょう.

❯❯❯ docker-compose run go blogsync pull [ブログのドメイン]

これでブログの記事が以下のように取得出来ます.

f:id:tanabebe:20200630121427p:plain

記事が全て取得出来たのでmasterブランチへpushしていきますが,まずは.gitignoreを先に追加しましょう.

❯❯❯ git init
❯❯❯ git add .gitignore
❯❯❯ git commit -m ".gitignore file add."
❯❯❯ git remote add origin https://github.com/[user]/[your repository name].git
❯❯❯ git push -u origin master

.gitignorepushしたら,あとは残りのファイルを全てpushしましょう.

❯❯❯ git add -A
❯❯❯ git commit -m "add all file."
❯❯❯ git push

これでmasterブランチはOKですが,記事の更新にはmasterブランチと別ブランチの差分が必要ですので,別ブランチを作成して作業ブランチを切り替えます.

# お好きなブランチ名でOKです
❯❯❯ git branch blog
❯❯❯ git switch blog

ここまで行えば後は記事を書いていくだけです.

5. 新規記事の下書きを追加する

draftという記事タイトルを持つ下書き記事を投稿します.

❯❯❯ docker-compose run go blogsync post --title=draft --draft [ドメイン名] < draft.md

これで下書きが「はてなブログ」へ投稿され,ローカルではentries/[ドメイン名]/entry/YYYY/MM/DD/HHmmss.mdが作成されます.このファイルを「VS Code」上で編集していき,リポジトリpushしていく事で記事の更新が可能となります.なんて便利なんだっ! 余談ですが,lint-stagedhuskyを導入しているとcommit時に文章校正が出来ます.

husky > pre-commit (node v12.18.2)
✔ Preparing...
⚠ Running tasks...
  ❯ Running tasks for *.md
    ✖ textlint [FAILED]
↓ Skipped because of errors from tasks. [SKIPPED]
✔ Reverting to original state because of errors...
✔ Cleaning up... 

✖ textlint:

   18:12  error  一文に二回以上利用されている助詞 "に" がみつかりました。  ja-technical-writing/no-doubled-joshi
  129:33  error  文末が"."で終わっていません。                            ja-technical-writing/ja-no-mixed-period

✖ 2 problems (2 errors, 0 warnings)

6. 気を付ける点

画像追加時は「はてなブログ」上へファイルアップロードが必要なため,以下手順を踏む必要があります.(他に良い方法があるかは不明)

  • はてなブログ上で画像を追加して保存
  • masterブランチで記事の取得し直し
  • 別ブランチにmerge

こうすることでmasterと別ブランチの記事が同一になるので,記事の更新が出来ます. ※日を跨いでいると新しい日付で下書き記事が取得されるので更新する記事に注意しましょう.

7. まとめ

「push-to-hatenablog」はとても強力なツールでした.VS Code」で集中して記事を書けるようになり,「GitHub」のリポジトリで投稿記事を管理することも,commit時にtextlintを走らせ,Typoを減らす事も出来ました.
画像追加時には少々手間がかかるもののmergeをする機会もあり,気を付けないと間違った記事を更新する可能性があるので,Git学習の一貫だと思えば特に苦はないです. 共同で記事を書いている方は「GitHub Actions」のタイミングを変更して「reviewdog」と組み合わせる.というのも面白そうですね.

8. 参考

github.com

github.com

はてなブログ記事のGitHub管理環境「push-to-hatenablog」のセットアップと使い方

Go×Fyne×ExcelizeでExcelからDDLファイルを生成する簡易ツールを作成しました

f:id:tanabebe:20191118123120p:plain

コロナの影響により保育園への送り迎えと電車通勤がなくなり,空いた時間が出来た.この時間を使って社内用ではあるが,ExcelファイルからPostgreSQL用のDDL作成ツールを作成した.
今回はGo実装のGUI Tool Kit Fyneを使用し,Excelの操作にはExcelizeを使用した.

要件

いずれもExcelファイルを起点とし,以下をクライアントのPCで実現させる.

  1. DDL生成時の除外テーブルを指定させる.
  2. Schema指定を行えるようにする.
  3. Schema未指定時はSchemaの作成はせず,テーブルはpublicのSchemaとする.
  4. 論理名からコメントを取得し設定する.

実際の動作は以下となる.残念なくらい簡素だ.

f:id:tanabebe:20200414141047g:plain

こちらのテンプレートを読み込んで生成したSQLがこちらだ.

DROP TABLE IF EXISTS public.users;
CREATE TABLE public.users (
    id BIGSERIAL,
    name varchar(20),
    password text,
    mailaddress varchar(255),
    created timestamp,
    modified timestamp,
    PRIMARY KEY (id)
);
COMMENT ON TABLE public.users IS 'ユーザー';
COMMENT ON COLUMN public.users.id IS 'ID';
COMMENT ON COLUMN public.users.name IS '名前';
COMMENT ON COLUMN public.users.password IS 'パスワード';
COMMENT ON COLUMN public.users.mailaddress IS 'メールアドレス';
COMMENT ON COLUMN public.users.created IS '作成日';
COMMENT ON COLUMN public.users.modified IS '更新日';

windows実行用にexeファイルとExcelテンプレートはGitHubにあげており,簡易的に使い方も記載している.

github.com

開発準備

Fyneの導入

これだけでOKだ.

go get fyne.io/fyne

Fyneの使い方

導入したところで使い方がわからなければどうしようも出来ない.私はFyne公式のTour of Fyneを一通り流してから始めた.しかしながら今回取り扱うUIは極小なので流さなくても問題はなかったのかもしれない.

これだけはやっておこう

Fyneのレイアウト配置については使う場合に理解がないと苦戦すると思うので使い方を最低限覚えておこう.
Tour of Fyneをこなさずに進めてしまうと悩むはず.面倒かもしれないが,LayoutWidgetについては必ず抑えておこう.

Tour of Fyneを終えたら以下のSampleもとても役に立つのでレイアウトが期待通りとならない場合は眺めてみるのが良い.

github.com

例として以下のようなレイアウトを組みたい時

f:id:tanabebe:20200419215946p:plain

雑ではあるが,以下で実現出来る.

package main

import (
    "fyne.io/fyne"
    "fyne.io/fyne/app"
    "fyne.io/fyne/layout"
    "fyne.io/fyne/widget"
)

func main() {
    ap := app.New()
    w := ap.NewWindow("Layout Sample")
    w.CenterOnScreen()
    lbl := widget.NewLabelWithStyle("0", fyne.TextAlignTrailing, fyne.TextStyle{Bold: true})
    w.SetContent(fyne.NewContainerWithLayout(layout.NewGridLayout(1), lbl,
        fyne.NewContainerWithLayout(layout.NewGridLayout(4),
            widget.NewButton("AC", func() {}),
            widget.NewButton("+-", func() {}),
            widget.NewButton("%", func() {}),
            widget.NewButton("÷", func() {}),
        ),
        fyne.NewContainerWithLayout(layout.NewGridLayout(4),
            widget.NewButton("7", func() {}),
            widget.NewButton("8", func() {}),
            widget.NewButton("9", func() {}),
            widget.NewButton("×", func() {}),
        ),
        fyne.NewContainerWithLayout(layout.NewGridLayout(4),
            widget.NewButton("4", func() {}),
            widget.NewButton("5", func() {}),
            widget.NewButton("6", func() {}),
            widget.NewButton("-", func() {}),
        ),
        fyne.NewContainerWithLayout(layout.NewGridLayout(4),
            widget.NewButton("1", func() {}),
            widget.NewButton("2", func() {}),
            widget.NewButton("3", func() {}),
            widget.NewButton("+", func() {}),
        ),
        fyne.NewContainerWithLayout(layout.NewGridLayout(4),
            widget.NewButton("0", func() {}),
            widget.NewButton(".", func() {}),
            widget.NewButton("C", func() {}),
            widget.NewButton("=", func() {}),
        ),
        fyne.NewContainerWithLayout(layout.NewGridLayout(3),
            widget.NewButton("left", func() {}),
            layout.NewSpacer(),
            widget.NewButton("right", func() {}),
        ),
        fyne.NewContainerWithLayout(layout.NewGridLayout(1),
            widget.NewButton("close", func() {
                w.Close()
            }),
        ),
    ))
    w.ShowAndRun()
}

流れとしては以下となる.

// 1. Fyneの起動
ap := app.New()
// 2. Fyneの新規windowsにHello World.を作成
w := ap.NewWindow("Hello World.")
// 3. 配置したいwidgetを作成
lbl := widget.NewLabel("Hello World.")
// 4. widgetをwindowに配置
w.SetContent(lbl)
// 5. アプリケーションの実行
w.ShowAndRun()

流れを覚えていくとレイアウトを組んでいくのが楽しくなるはずだ.

注意点

Fyneは日本語フォントに対応していない.日本語を入力すると文字化けする.
私はまだ試していないのだが以下で日本語に対応出来るとのこと.

qiita.com

Excelizeの導入

こちらもこれだけでOKだ.

go get github.com/360EntSecGroup-Skylar/excelize

Excelizeの使い方

Excelizeはドキュメントが充実しているので迷う事が少ない.こちらを読もう.私が使ったのはExcel文書を読むの処理くらいしかない.一部の処理(layout.go)から抜粋すると以下となる.

// 読み込みたいExcelファイル指定
readFile, err := excelize.OpenFile(importFile)

// 読み込むExcelシート名を保持するスライス
var targetList []string
// Excelの都合上,5スタート
for i := 5; i < len(idxRows); i++ {
    if idxRows[i][constant.SheetIndexTableName] != "" && idxRows[i][constant.Exclude] != "ON" {
                 // 対象とするシート名を格納
        targetList = append(targetList, idxRows[i][constant.TargetSheetName])
    }
}

// シート分の処理を行う
for _, list := range targetList {
    // シート毎にセル内の値を全取得
    rows, err := readFile.GetRows(list)
    // 処理略・・・
}

ビルド

FyneのREADMEにも明記されているが,Windowsアプリケーション実行時のコマンドプロンプトを抑止するため,以下のコマンドでexeファイルを生成している.

go build -o go-excel-export-ddl.exe -ldflags -H=windowsgui

大変だった点

  1. Excelのフォーマットを既存から大きく修正する事が出来なかった.
  2. Excel内で行削除をした場合とセル選択からセルをクリアした場合でGoに入ってくるデータが違った.(nil""
  3. Fyneの扱い方
  4. VBAで良く使うCells(x, y).End(xlDown)のようなセルの終点や始点操作がないため,セル読み込み時のポイントがこれで良いのか悩んだ.
  5. 業務でGoを使った経験がゼロなので,これで良いのかという悩みがとにかく多かった.

今後の改善点

データ定義を設計する際に編集者に自由入力させるのは止めたい.
例えば自動増分したい場合はデータ型をintergerとし,AutoIncrement列に「◯」を選択しているが PostgreSQL準拠ならintegerで自動増分する場合はserialとしたい.
使用しているButton内のfunctionが肥大化しすぎているのでなんかダサい.
自戒ではあるが,もっと勉強する.
PostgreSQL以外にも対応する.

参考

Fyne

Excelize Official Docs

GitHub - sqweek/dialog: Simple cross-platform dialog API for go-lang

FyneでFineな(文字化けしない✙可搬性あり)アプリを作る方法 - Qiita

T2-Wonderland: Go言語製のキレイなGUIアプリをFyneライブラリで作るよ!(*´ω`*)

GoでGUIを作るFyne というライブラリが楽しそうだよ、というご紹介 - Qiita

Go×agoutiで病院の予約戦争に勝利する

f:id:tanabebe:20191224155150p:plain

平日,休日問わず子供の通院時に困った事がある.かかりつけの病院のWeb予約は競争率が高く,予約開始時間に1分でも遅れてしまうと受付が終了してしまう.平日も午前中に休みを取って行かざるを得ない場合もあるのだが,予約を確実に取れる確証がない.
この不安が少しストレスとなっている.そこで今回は病院予約を確実に勝ち取るためにGo×agoutiでWeb予約受付を自動化していく.

実現したいこと

以下のオペレーションを病院予約当日の08:00に処理する.

  1. 病院のWeb予約受付サイトを開く
  2. 自身のアカウントでログインを行う
  3. 予約受付確認を行う
  4. 予約登録を行う

実際に動作させたプログラムはこちらにあげている.

github.com

環境

下環境で動作を確認した.

Name version
macOS Mojave 10.14.6
GoogleChrome 79.0.3945.79
ChromeDriver 79.0.3945.36
Go 1.12.9

導入準備

ChromeDriverのインストール

macなのでbrew installchromedriverをインストールする.

❯❯❯ brew install chromedriver

agoutiをインストール

go getagoutiをインストールする.

❯❯❯ go get github.com/sclevine/agouti

https://agouti.org/

処理の流れ

エラー時については割愛するが,プログラム全体の流れは以下とした.

  1. init関数を実行
  2. waitingファイルを確認し,処理継続可否の判断
  3. configファイルを読み込み
  4. ScrapingListconfigファイルの値を設定
  5. imgファイルの削除
  6. Google Chromeをヘッドレスモードで起動
  7. 予約受付サイトへ遷移
  8. スクリーンショットを取得
  9. 予約受付リンクをクリックし、ログイン画面へ遷移
  10. ログイン情報の入力
  11. スクリーンショットを取得
  12. ログインの実行
  13. スクリーンショットを取得
  14. 予約受付確認を実行
  15. スクリーンショットを取得
  16. 予約受付登録を実行
  17. スクリーンショットを取得する
  18. アクティブなウインドウを閉じる
  19. waitingファイルの削除
  20. Google Chromeを終了する

実装

今回,agoutiで使用した機能を以下に抜粋する.

WebDriver

Webブラウザを操作するためプロセスを制御する.

ChromeDriver

Google ChromeWebブラウザ操作を制御する.ここではヘッドレスとしてブラウザを非表示で起動を行う.オプションを付けずにdriver := agouti.ChromeDriver()とすることでブラウザの表示が行われる.

driver := agouti.ChromeDriver(
        agouti.ChromeOptions(
            "args", []string{
                "--headless", 
            }),
    )

Start

WebDriverプロセスを開始するために必要となる.

Stop

WebDriverプロセスを停止するために必要となる.main関数の最後でプロセスを停止したいのでdeferで処理を行う.

NewPage

WebDriverに対応したPageのレシーバが返ってくる.ここではGoogle Chromeが対象,以降のPage機能を使用するために必要となる.

target, err := driver.NewPage()

Page

Pageは開いているブラウザのセッションを指す.前提条件として以下のように指定したWebDriverとそれに対応するセッションを作成する事が必要となる.

driver := agouti.ChromeDriver()
target, err := driver.NewPage()

URLを渡して画面遷移を行う.

target.Navigate("targetUrl")

CloseWindow

アクティブなウィンドウを閉じる.

target.CloseWindow()

Screenshot

現在開いているページのスクリーンショットを指定したファイル名で保存する.相対パス,もしくは絶対パスで指定する.

target.Screenshot("filePath")

Selection

要素の取得,選択や実行を行う.

引数に指定したアンカー要素のテキストを検索する.

target.FindByLink("今すぐ受付")

Fill

要素を検索し,引数に指定したテキストで埋める.

target.FindByID("user_email").Fill("your mailadress")

Click

要素を検索し,クリックを行う.

target.FindById("sample").Click()

Submit

指定した要素を検索し,フォーム値を送信する.

target.FindByName("commit").Submit()

cronを設定する

maccronの設定を行っていく.実行結果を自分宛にメールで送信,毎日08:00の起動とするようcrontab -eで編集を行う.
※ここではcrontab -eとしているが,このコマンドはeの隣にあるキーがrなのでオプションに-rで大惨事となる可能性があるので,普段からこのオプションに慣れないよう注意して欲しい.

MAILTO = "example@example.com"
0 8 * * * cd /your project path}; bash -l -c 'go run /your project path/main.go'

実行結果

12月21日(土)に実行した結果は以下となった.これはアクセス先のシステムから送信されてきたメール内容を抜粋している.

日付:2019/12/21 8:00

◇ 受付完了のお知らせ

・診察日:2019年12月21日
・時間 :午前診察
・受付No:31

受付Noがニ桁だった事に驚きではあるが,予約受付が無事完了していることが確認出来た.これでストレスは抱えずともかかりつけの病院予約がスムーズに行える.
また,どのような動作となるかヘッドレスオプションを外し,掲載可能範囲の画面を以下に記す.ここではログインを失敗させているが,実際はログイン以降の処理も問題なく行われている事は確認済みだ.

f:id:tanabebe:20191224151445g:plain

課題

Web予約受付を自動化する.と題しプログラムを作成したが,蓋を開けると完全な自動化とは言い難い点があったため,下表に内容をまとめた.

# 詳細
1 macがスリープ状態だとcronが起動しないため,サーバー配置が必要.
3 waitingファイルを手動で作成する運用になっているため,スマートフォンからwaitingファイルを作成するように改善する.
4 スクリーンショットを取得しているものの,現状だと息をしていない.
5 予約受付順にムラがある.

まとめ

課題はあるものの,Web予約受付を自動操作することで確実な予約が可能となった.前日にwatingファイルを仕込んでいく事で当日の不安も解消され,いつもより惰眠も貪る事が出来る.これは良いことづくしと言える.他の展開としては,ブログに書いた内容を連携対象外のWebアプリに書き込んだり,手動のテストも自動化が出来るだろう.実装するにあたって特段難しい事はなかったので,今後も自動化出来る事は積極的に取り組む.

参考

agouti - GoDoc

Agouti

golang.hateblo.jp

program.okitama.org

UdemyでGoの配列とスライスの扱いを学んだので初心者ながらに少しだけ深堀りしてみた

f:id:tanabebe:20191118123120p:plain

現在Udemyで現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインシストレFintechアプリの開発を受講しています。講義の中でスライスのmakeとcapacityの内容があり、スライスと配列についての挙動が気になったのでまとめました。

はじめに

受講している内容

現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発

講師の酒井潤さんについて

twitter.com

シリエン戦隊JUN TVでYouTuberとして活動もされています。シリコンバレーのエンジニア事情などとても面白いです。 www.youtube.com

Splunkで働いている現役シリコンバレーエンジニアの方の講義を手軽に受けられるのは魅力的です。講義の内容や話のテンポも無駄な事を削ぎ落としていると感じたので、私にはとても心地良く感じました。シリコンバレーの風景なども動画におさめられているのはとても面白いと思いました。私はもはやファンになってしまっているので自身の学習内容にマッチしているのであれば、私は酒井潤さん推しです。気になる方は是非チェックしてみて下さい。

実践していく

講義から学んだGoの配列とスライスについては14〜16のレクチャーが該当となり、動画で言うと10分ほどです。プログラムは以下に公開しています。講義内容には影響ないように考慮しています。
リポジトリ内のreflectのケースは当記事では未記載

github.com

配列について

宣言方法

以下のような形で宣言する事が可能です。

var array1 [2]int
array2 := [3]int{1, 2, 3}
array3 := [...]int{1, 2, 3, 4}

配列の挙動を試す

以下のようにして、配列内の長さ、容量、値、アドレスを見てみます。

var array1 [2]int
fmt.Printf("array1 => length=%d capacity=%d value=%v address=%p \n", len(array1), cap(array1), array1, &array1)

array2 := [4]int{}
fmt.Printf("array2 => length=%d capacity=%d value=%v address=%p \n", len(array2), cap(array2), array2, &array2)

array3 := [4]int{1, 2, 3}
fmt.Printf("array3 => length=%d capacity=%d value=%v address=%p \n", len(array3), cap(array3), array3, &array3)
array3[3] = 4
fmt.Printf("array3 => length=%d capacity=%d value=%v address=%p \n", len(array3), cap(array3), array3, &array3)
実行結果

実行すると以下となります。

array1 => length=2 capacity=2 value=[0 0] address=0xc000016050 
array2 => length=4 capacity=4 value=[0 0 0 0] address=0xc00008e000 
array3 => length=4 capacity=4 value=[1 2 3 0] address=0xc0000180e0 
array3 => length=4 capacity=4 value=[1 2 3 4] address=0xc0000180e0

配列は要素数を宣言時に必ず決めるため、各変数での挙動が上記結果となります。アドレスを出力しているのはメモリ上に確保されるアドレスに変化が有るかどうかを見たかったためです。

配列の注意点

配列には要素を追加することは出来ないため、以下のようにするとエラーとなります。

// 配列にappendすることは出来ない
array3 = append(array3, 4)

配列の値の取り出し方

パターン1
// indexは不要なので値のみを出力する
for _, v := range array3 {
    fmt.Println(v)
}
パターン2
for i := 0; i < len(array3); i++ {
    fmt.Println(array3[i])
}

配列まとめ

  • 配列の要素数は固定長
  • 宣言時に要素数を決めるため、宣言の要素追加は不可能
  • メモリ上に容量を無駄に確保される事がない
  • appendは出来ないため、書き換える場合は要素位置を指定する

スライスについて

ここが今回のメインです。講義内でもスライスの宣言方法によってのメモリ上へどのように確保されるのかという点は触れているのですが、この時点では「まだ、気にすることはない」という事から最初はスルーしていました。深く考えると進まないのも事実です。ですが良く使いそうだなと感じたので、気になって仕方ない…という事で1度立ち止まって色々と試してみました。

宣言方法

スライスは以下のような形で宣言する事が可能です。

var slice1 []int
slice2 := []int{1, 2, 3, 4}
slice3 := make([]int, 10)

まずはスライスの挙動を試す

以下のようにして、スライスの長さ, 容量, 値, アドレスを見てみます。

var slice1 []int
fmt.Printf("slice1 => length=%d capacity=%d value=%v address=%p \n", len(slice1), cap(slice1), slice1, slice1)
slice2 := []int{1, 2, 3, 4}
fmt.Printf("slice2 => length=%d capacity=%d value=%v address=%p \n", len(slice2), cap(slice2), slice2, slice2)
slice3 := make([]int, 10)
fmt.Printf("slice3 => length=%d capacity=%d value=%v address=%p \n", len(slice3), cap(slice3), slice3, slice3)
実行結果

配列の時とは違い、宣言と同時に値を入れていない場合はnil扱いとなります。makeはここでは容量を10としていますが0でもnil扱いにはなりません。
また、var slice1 []intでは宣言したスライスの実体がメモリ上に確保されていますが(もしかしたら間違っているかもしれないです)どのアドレスも見ていないため0x0になります。

slice1 => length=0 capacity=0 value=[] address=0x0 
slice2 => length=4 capacity=4 value=[1 2 3 4] address=0xc000096080 
slice3 => length=10 capacity=10 value=[0 0 0 0 0 0 0 0 0 0] address=0xc00009e000

makeなしのスライスを試す

makeをせず、容量を確保しない場合のスライスを宣言します。

var slice4 []int
fmt.Printf("slice4 => length=%d capacity=%d value=%v address=%p \n", len(slice4), cap(slice4), slice4, slice4)
// appendするとどうなるか
slice4 = append(slice4, 2)
fmt.Printf("slice4 => length=%d capacity=%d value=%v address=%p \n", len(slice4), cap(slice4), slice4, slice4)
実行結果

宣言直後のslice4では容量も確保されていないため、address0x0となり、appendで値を追加すると容量が確保されています。次はmakeで宣言した場合のスライスを試していきます。

slice4 => length=0 capacity=0 value=[] address=0x0 
slice4 => length=1 capacity=1 value=[2] address=0xc0000160f8 

makeありのスライスを試す

ここではmakeした場合のスライスの挙動を見ていきます。確認したいポイントをPrintfで出力していきます。やっている事としては単純です。

// makeを使う。スライスの長さは容量との関係性が見たいので0とする
slice5 := make([]int, 0, 1)
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
slice5 = append(slice5, 1)
// この時点ではcapacityを超えてこないのでアドレスは変わらない
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
slice5 = append(slice5, 2)
// capacityを超えるのでアドレスが変わる
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
slice5 = append(slice5, 3)
// capacityを超えるのでまたアドレスが変わる、capacityは以前確保していたcapacity^2で増加していく
fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
実行結果

面白い結果となりました。2度目のslice5出力までは宣言時のスライス容量を超えないためaddressは同じです。 しかし、確保した領域を超えるとaddressに変化があり、スライスの容量も自動で増えています。また、最後の出力時には拡張される前の容量^2で増えています。ここではaddressが変わっているのでslice5の容量が拡張される度に、メモリ上に領域が確保されていきます。これは扱いを知らないと意識せずに書いてしまいそうです。

slice5 => length=0 capacity=1 value=[] address=0xc00008c160 
slice5 => length=1 capacity=1 value=[1] address=0xc00008c160 
slice5 => length=2 capacity=2 value=[1 2] address=0xc00008c190 
slice5 => length=3 capacity=4 value=[1 2 3] address=0xc0000940e0 

スライスの宣言パターンによるベンチマークを取る

スライスの宣言時に明示的に容量を確保しない場合、容量が拡張される度にメモリ上に新たな領域が確保され、無駄にメモリを喰い潰して行くという挙動だと思います。大量にループを回した場合、処理速度に大きく影響がありそうだと感じました。
Goの標準パッケージtestingを使用してベンチマークを取り、宣言パターンによって処理速度にどれほどの差があるか見ていきます。
_test.goと終わるファイルを作成することでtest対象と出来ます。また、ベンチマークを取りたいのでBenchmarkから始まるテスト関数を作成してgo test -bench . -benchmemで実行します。

// slice_test.go
package main

import "testing"

// sliceの容量を指定しない場合
func BenchmarkInitSliceVariable(b *testing.B) {
    var target []int
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target = append(target, i)
    }
}

// sliceでmakeで容量を確保しているがlength指定している場合(値が0で初期化されている場合)
func BenchmarkSliceCapacityNo(b *testing.B) {
    var target = make([]int, b.N)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target = append(target, i)
    }
}

// sliceのmake時にc容量を設定する場合
func BenchmarkSliceCapacityYes(b *testing.B) {
    var target = make([]int, 0, b.N)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target = append(target, i)
    }
}
実行結果

以下結果の通り、スライスの宣言方式で処理速度が大きく変わりました。

BenchmarkInitSliceVariable-4    100000000               22.3 ns/op            49 B/op          0 allocs/op
BenchmarkSliceCapacityNo-4      200000000               93.6 ns/op            57 B/op          0 allocs/op
BenchmarkSliceCapacityYes-4     2000000000               6.80 ns/op            0 B/op          0 allocs/op

実行結果についての見方は以下参考にさせていただきました。

qiita.com

// 関数の実行回数、有用な結果が得られるまで実行される
// 多ければ多いほど良い
2000000

// 1回の実行にかかった時間
// 少ないほど良い
815 ns/op

// 実行ごとに割り当てられたメモリのサイズ
// 少ないほど良い
336 B/op

// 1回の実行でメモリアロケーションが行われた回数
// 少ないほど良い
9 allocs/op

今回の結果に当てはめると以下ですね。

// 関数の実行回数、有用な結果が得られるまで実行される(多いほど良い)
100000000
200000000
2000000000

// 1回の実行にかかった時間(少ないほど良い)
22.3 ns/op
93.6 ns/op
6.80 ns/op

// 実行ごとに割り当てられたメモリのサイズ(少ないほど良い)
49 B/op
57 B/op
0 B/op

//1回の実行でメモリアロケーションが行われた回数(少ないほど良い)

0 allocs/op
0 allocs/op
0 allocs/op
スライスまとめ
  • 素数は可変長
  • スライスのmake時は要素数がわかっているなら宣言時にlenで確保
  • make([]int, 要素数)としてしまいそう注意が必要
  • 宣言時は長さが膨大だとスライスの中身は型による初期化が行われる のでlength0が良い

まとめ

講義の内容では約10分ほどの配列とスライスですが、とても考えさせられました。アロケーション0だったのが私の考えていた想定と違ったので、アロケーション回数が大きく増えてしまうようなバッドプラクティスなども今後学習を続ける中で深堀りしていきたいと思っています。

参考

testing - The Go Programming Language

Golangでベンチマークを取ってみた - Qiita

go - The Go Programming Language

@kakakakakkuさんのブログメンタリングを卒業しました

こちらにブログスタート時の初回記事がありますが、3か月間に及ぶブログメンタリングはあっという間でした。しかし終わってからが本当の始まりなので、どんな事を行ったのか、何を学んだのか振り返ってみました。

なぜブログメンタリングを受けたのか

  • 働き方が変わるので今後のセルフブランディングが必要と考えていた
  • アウトプットする場所としてブログを考えていた
  • 技術に好奇心があるものの、個人的には浅い所が多いと感じていた
  • 自分を叩き直す

このように思い悩んでいる時に「技術ブロガーを育てる!ブログメンタリングで何を教えているのか」という記事と出会いました。調べてみると、丁度メンティー募集のタイミングだったので即応募しました。同じように悩みを抱えている方、心が突き動かされるので是非チェックしてみて下さい。

kakakakakku.hatenablog.com

また、Twitterハッシュタグ#ブログメンタリングや、ブログメンタリングGoogle検索するとカックさんの記事やメンティーとして卒業された記事などがたくさん出てきますのでこちらもチェックしてみて下さい。

何を学んだか

技術文書を書く上での表記揺れですます調など、恥ずかしながら日本語の使い方については特に指摘をいただきました。もちろん他にもたくさんあるのですが、個人的に感じた事をまとめていきます。

自己管理は最重要

ブログメンタリング時はブログの更新本数のノルマがありますが、記事の平日公開をしたほうが読まれやすいのもあり、平日公開を狙った活動を行っていく必要があります。また、「コツコツと積み上げて経験値を稼ぐという感覚を如何に楽しめるか」というのもポイントです。私的な理由ですと「仕事、子供、ブログ」と並べた時にスイッチの切り替えが出来ていなかったです。子育てを優先する中でどうやって時間を捻出するかというのが2か月目に突入した時の課題でした。「22時以降はブログ」と掲げてはみたものの、ふとしたトラブルがあったりするとブレてきます。こういった想定外に耐え得るためにも計画性を持った自己管理が重要です。

ブログ記事を書くだけではダメ

「自分のためのブログ」でないのであれば「他人の目線」を常に意識すること。記事内の温度感他人が見た時何を思うか。こういった点は日常生活では出来ていたつもりでしたが、ブログ内でのアプローチ方法は全く違うモノでした。ブログ外の活動も含めて、今後も別目線での動きも積極的に取り組んでいきます。

モチベーションコントロール

毎記事で新しい取り組みを書くのは大変です。世に出ている同じ記事でも書くべきという点なども継続していく上で重要なポイントだと思いました。ブログを育てていくという意識をすることで「ブログ」を中心に考えた時、自己研鑽のモチベーションが断然変わりました。楽しんで取り組む姿勢、これ大事です。

3か月の実績

年月 書いた記事数
2019/07 5記事
2019/08 4記事
2019/09 6記事

これとは別で振り返ってみると平日に公開出来た記事は4記事、週2での記事公開はわずか1回でした。思っていたより書けていない!

まとめ

メンタリング中は学びが多くメンターの方がついてくれて何かを行うという経験が私はなかったので、ブログメンタリングに応募して本当に良かったです。書く楽しさと、その裏側にある厳しさを教えていただきました。不安はあれどやり遂げると決めていたので、ここをクリア出来たのは大きかったです。
また、カックさんの圧倒的なレスの速さなどから「いつ寝てるの?」という場面で良く驚いていましたが…数年前から私自身が抱えていた悩みにも気付いた上でアドバイスをくれたり、とにかくご自身で体現していることが本当にカッコ良く、凄かったです。

そしてブログそのものでした。

同時にもっとカックさんを使い倒す事が出来れば良かったという後悔もあります。今後の課題は山ほどありますが、良い意味での課題です。これからも1つずつステップを踏んでいきます。ありがとうございました!

同期メンティーの方々

最後になりますが、同時期にメンタリングを受けられていたメンティーの方々のご紹介です。メンタリング期間中も記事を良く拝見させていただきました。自分も頑張らないと!という良い刺激があったりモチベーションが上がります。今後の皆さんの活躍にも乞うご期待です!

lopburnyさん

riotz.works

y_zumi3さん

y-zumi.hatenablog.com

imaharuさん

www.imaharutech.work

期限切れとなってしまったLet’s EncryptのSSL証明書の新規発行と自動更新を行う

f:id:tanabebe:20191007143528p:plain

社内で使用しているknowledgeのWebアプリについてSSL証明書が切れてしまっており、自動更新の設定も当時はかけていなかったため、SSL証明書の新規発行と自動更新設定を行いました。

実行環境

SSL証明書の期限が切れているか確認する

rootユーザーへ切り替えます。

❯❯❯ sudo -i

期限が切れているのは明白なのですが、状況を確認します。 ドメイン名は伏せてあります

❯❯❯ certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Revocation status for /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem is unknown

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: xxxxx.xxxxxxx.com
    Domains: xxxxx.xxxxxxx.com
    Expiry Date: 2019-09-17 08:10:00+00:00 (INVALID: EXPIRED)
    Certificate Path: /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/xxxxx.xxxxxxx.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

INVALID: EXPIREDと出ているので期限が切れています。期限が切れている場合、SSL証明書の新規発行を行う必要があるため、再度発行していきます。

SSL証明書の新規発行を行う

ファイルは退避しておきます。

❯❯❯ mkdir /etc/letsencrypt/live/_bk
❯❯❯ mv /etc/letsencrypt/live/xxxxx.xxxxxxx.com /etc/letsencrypt/live/_bk

80, 443portが使われているとエラーとなるのでhttpdを停止しておきます。

❯❯❯ systemctl stop httpd

SSL証明書の新規発行を行います。

❯❯❯ certbot-auto certonly --standalone -d xxxxx.xxxxxxx.com

httpdを起動します。

❯❯❯ systemctl start httpd
Job for httpd.service failed because the control process exited with error code. See "systemctl status httpd.service" and "journalctl -xe" for details.

エラーとなってしまいました…ここは内容を見て探っていきます。

❯❯❯ systemctl status httpd 
Oct 06 13:03:31 xxxxx-instance httpd[10894]: AH00526: Syntax error on line 101 of /etc/httpd/conf.d/ssl.conf:
Oct 06 13:03:31 xxxxx-instance httpd[10894]: SSLCertificateFile: file '/etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem' does not exist or is empty
Oct 06 13:03:31 xxxxx-instance systemd[1]: httpd.service: main process exited, code=exited, status=1/FAILURE
Oct 06 13:03:31 xxxxx-instance kill[10895]: kill: cannot find process ""
Oct 06 13:03:31 xxxxx-instance systemd[1]: httpd.service: control process exited, code=exited status=1
Oct 06 13:03:31 xxxxx-instance systemd[1]: Failed to start The Apache HTTP Server.

エラーを見るとSyntax errorと言っており、更にfullchain.pemが存在しないと言っています。中身を見ていきます。

❯❯❯ vim /etc/httpd/conf.d/ssl.conf

該当箇所を見ると以下とありました。

SSLCertificateFile /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem

フォルダも移動したので問題ない、と思っていましたがどうやら違いました。確認してみます。

❯❯❯  ll /etc/letsencrypt/live/
total 4
drwxr-xr-x. 3 root root  38 Oct  6 12:47 _bk
drwxr-xr-x. 2 root root  93 Oct  6 19:59 xxxxx.xxxxxxx.com-0001
-rw-r--r--. 1 root root 740 Oct  6 12:56 README

フォルダ名がxxxxx.xxxxxxx.com-0001となっており、名前が拡張されていました。少し気持ち悪さは残りますが、ssl.conf内の以下で始まる定義のファイルパスに0001を付与し変更します。

SSLCertificateFile
SSLCertificateKeyFile
SSLCertificateChainFile

再度httpdの起動を行います。

❯❯❯ systemctl start httpd && systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2019-10-06 20:28:55 UTC; 13ms ago
     Docs: man:httpd(8)
           man:apachectl(8)
  Process: 11556 ExecStop=/bin/kill -WINCH ${MAINPID} (code=exited, status=0/SUCCESS)
  Process: 11515 ExecReload=/usr/sbin/httpd $OPTIONS -k graceful (code=exited, status=0/SUCCESS)
 Main PID: 11579 (httpd)
   Status: "Processing requests..."
   CGroup: /system.slice/httpd.service
           ├─11579 /usr/sbin/httpd -DFOREGROUND
           ├─11580 /usr/sbin/httpd -DFOREGROUND
           ├─11581 /usr/sbin/httpd -DFOREGROUND
           ├─11582 /usr/sbin/httpd -DFOREGROUND
           ├─11583 /usr/sbin/httpd -DFOREGROUND
           └─11584 /usr/sbin/httpd -DFOREGROUND

Oct 06 20:28:55 xxxxx-instance systemd[1]: Starting The Apache HTTP Server...
Oct 06 20:28:55 xxxxx-instance systemd[1]: Started The Apache HTTP Server.

修正箇所は正解ですね。これでhttpdが動作しました。

SSL証明書の自動更新を設定する

元を正せば更新を自動化していないのが問題ですね。期限が迫る度に手動で更新とか手間ですし、少し面倒な事を後回しにすると人間は大体忘れます。なので、このままSSL証明書の自動更新を行うように設定していきます。実際にSSL証明書の更新はかけずにチェックを行いたいので—dry-runをオプションにつけてrenewを実行します。

❯❯❯ cd /usr/local/certbot/
❯❯❯ ./certbot-auto renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Traceback (most recent call last):
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/renewal.py", line 64, in _reconstitute
    renewal_candidate = storage.RenewableCert(full_path, config)
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/storage.py", line 465, in __init__
    self._check_symlinks()
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/storage.py", line 523, in _check_symlinks
    "expected {0} to be a symlink".format(link))
CertStorageError: expected /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf is broken. Skipping.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)

Additionally, the following renewal configurations were invalid: 
  /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf (parsefail)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 renew failure(s), 1 parse failure(s)

またまたエラーです。ここでは以下のエラーが問題となっています。

CertStorageError: expected /etc/letsencrypt/live/xxx.xxx.com/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/xxx.xxx.com.conf is broken. Skipping.

シンボリックリンクについてファイルが壊れているという内容ですね。そもそもですが、前述したhttpdが起動しない問題と同じようにパスが違うだけと想定しますが、確認してみましょう。

どのファイル末尾に2prefixが振られてしまっているのは気になりますが、live内ではarchiveシンボリックリンクがはられているのでこちらについては問題なさそうです。

❯❯❯ ll /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/
total 4
lrwxrwxrwx. 1 root root  53 Oct  6 19:59 cert.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/cert2.pem
lrwxrwxrwx. 1 root root  54 Oct  6 19:59 chain.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/chain2.pem
lrwxrwxrwx. 1 root root  58 Oct  6 19:59 fullchain.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/fullchain2.pem
lrwxrwxrwx. 1 root root  56 Oct  6 19:59 privkey.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/privkey2.pem
-rw-r--r--. 1 root root 692 Oct  6 12:56 README

renewal内のファイルを確認します。

❯❯❯  vim /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf

中身を見ると存在しないパスを指定しているのが問題でした。0001を付与していきます。

# 変更前
archive_dir = /etc/letsencrypt/archive/xxxxx.xxxxxxx.com
cert = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem
privkey = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/privkey.pem
chain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/chain.pem
fullchain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem

# 変更後
archive_dir = /etc/letsencrypt/archive/xxxxx.xxxxxxx.com-0001
cert = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/cert.pem
privkey = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/privkey.pem
chain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/chain.pem
fullchain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem

再度、更新はかけずにチェックを行うコマンドを実行します。

❯❯❯  ./certbot-auto renew --dry-run

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

今度は成功しました。これでSSL証明書の更新についてテストが出来ました。

自動更新の設定を行う

renewの更新作業後はhttpdの起動を実行したいので、cronを使用します。念の為cronが動作しているかを確認します。

❯❯❯  systemctl status crond
● crond.service - Command Scheduler
   Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2019-10-06 18:41:14 UTC; 2h 42min ago
 Main PID: 462 (crond)
   CGroup: /system.slice/crond.service
           └─462 /usr/sbin/crond -n

Oct 06 18:41:14 knowledge-instance systemd[1]: Started Command Scheduler.
Oct 06 18:41:14 knowledge-instance crond[462]: (CRON) INFO (RANDOM_DELAY will be scaled with factor 36% if used.)
Oct 06 18:41:14 knowledge-instance crond[462]: (CRON) INFO (running with inotify support)

cronは動作しているので以下のように記述していきます。crontab -eで作成しようと思いましたがここは別ファイルでの管理にします。理由として間違ってキーボード隣のrをタイプしcrontab -rを実行すると無慈悲に全て消え失せますので要注意です。

❯❯❯  vim /etc/cron.d/letsencrypt

# 毎週日曜日の23:50に実行。/var/log/letsencrypt/result-renewal.logへ書き込むようにしておく。
50 23 * * 0 root /usr/local/certbot/certbot-auto renew --post-hook "systemctl reload httpd" > /var/log/letsencrypt/result-renewal.log

cronへ作成したファイルを反映します。

❯❯❯ crontab /etc/cron.d/letsencrypt

一度、毎分のcronが実行されるように変更を行い、実際に動いているかを確認します。

❯❯❯ vim /etc/cron.d/letsencrypt

# 毎週日曜日の23:50に実行。/var/log/letsencrypt/result-renewal.logへ書き込むようにしておく。
* * * * * root /usr/local/certbot/certbot-auto renew --post-hook "systemctl httpd reload" > /var/log/letsencrypt/result-renewal.log

❯❯❯ less /var/log/letsencrypt/result-renewal.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem expires on 2020-01-04 (skipped)
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem expires on 2020-01-04 (skipped)
No renewals were attempted.
No hooks were run.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                                                                         

確認が出来たら実行したいスケジュールに戻しておきます。これでSSL証明書の期限が迫っていたら自動更新が行われるようになります。

まとめ

今回については既にSSL証明書を導入済みという前提で進めてはいますが、Let’s Encryptの期限は3か月で切れるため、自動更新をしておくと後々かなり手間が省けます。「忘れんなよ」という話しではあるのですが…そこまで使われていないシステムだとSSL証明書の更新は忘れがちです。自動化のためにある程度の手間をかけることで後で楽が出来ます。SSL証明書の自動更新に関わらず、こういった考えは他の業務を遂行する上でも有用ではないでしょうか。

参考

qiita.com

イケてるcrontabのいじり方 - Qiita

ozuma.hatenablog.jp