2019年12月29日日曜日

golang でファイルの最後の一行を取得する方法

概要

タイトルの通りです
もう少し良い方法はないだろうか

環境

  • macOS 10.15.2
  • golang 1.12.9

読み込むファイル

  • vim test.txt
aaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbb
ccccccccccccccccccc
ddddddddddddddddddd

サンプルコード1: bufio.NewScanner

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    fmt.Println(lastLine())
}

func lastLine() string {
    line := ""
    f, _ := os.Open("./test.txt")
    if err != nil {
        fmt.Println(err)
        return line
    }
    defer f.Close()
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        line = scanner.Text()
    }
    return line
}

サンプルコード2: file.Seek

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    fmt.Print(lastLine())
}

func lastLine() string {
    line := ""
    file, err := os.Open("./test.txt")
    if err != nil {
        fmt.Println(err)
        return line
    }
    defer file.Close()
    // 1 行が 19 バイト以上であればさらに大きくする
    byteSize := int64(19)
    // カーソルを最後の行にする
    file.Seek(-1*(byteSize+1), 2)
    b := make([]byte, (byteSize + 1))
    for {
        _, err := file.Read(b)
        if err != nil {
            if err != io.EOF {
                fmt.Println(err)
                return line
            }
            break
        }
        line = string(b)
    }
    return line
}

最後に

個人的には 1 が簡単なので良いのですがファイルのサイズが大きくなると大変なことなります
2 は 1 行ごとにバイトサイズが異なる場合だと考えることが増えます

参考サイト

2019年12月28日土曜日

nginx でクライアント証明書認証してみる (キーチェーンアクセス編)

概要

クライアント証明書認証は https のサイトにアクセスするのに指定の証明書がないとアクセスできない認証です
IP だけではアクセス制御できない場合に使えます
今回は証明書の発行をキーチェーンアクセスで行い、そこで発行した証明書を使って nginx でクライアント証明書認証を試してみました

環境

  • macOS 10.15.2
  • nginx 1.15.2

認証局の作成

まずはキーチェーンアクセスを使って認証局を作成します
証明書は認証局がないと発行できません

ユーザ証明書を「SSL クライアント」にします
名前は好きな名前にしましょう

以下のように認証局が作成できれば OK です

キーチェーンアクセスの一覧にも認証局が存在することを確認します

クライアント証明書の発行

次に作成した認証局からクライアント証明書を書き出します

作成した認証局を右クリックし書き出しを選択します

鍵は p12 形式で書き出します

鍵にはパスワードを設定することもできます
今回はテストなのでパスワードなしにします

鍵が書き出せたら同様に認証局を右クリックして書き出しを選択します
そして .cer 形式で証明書を書き出します

証明書と鍵の変換

nginx で使うためには証明書と鍵ファイルをそれぞれ「.crt」「.key 」形式に変換する必要があります

  • openssl x509 -in ca.cer -inform DER -out ca.crt -outform PEM
  • openssl pkcs12 -in ca.p12 -nocerts -nodes -out ca.key

鍵を変換する際にパスワードを求められますが今回は設定していないので空で OK です

nginx の設定

あとは作成したクライアント証明書を nginx に設定して起動します
クライアント証明書認証は https でなければ動作しないのでサーバ証明書を適当に用意します
また以下の設定は最低限の設定のみになっています

  • cd /usr/local/etc/nginx/
  • vim nginx.conf
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       443 ssl;
        server_name  localhost;

        ssl_certificate      /etc/letsencrypt/live/hoge.fuga.com/fullchain.pem;
        ssl_certificate_key  /etc/letsencrypt/live/hoge.fuga.com/privkey.pem;
        ssl_verify_client on;
        ssl_client_certificate /path/to/ca.crt;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    include servers/*;
}

ssl_verify_client on;ssl_client_certificate /path/to/ca.crt; がクライアント証明書認証を有効にしている部分です
それ以外のディレクティブは SSL に関する設定になります

/path/to/ca.crt は先程変換した証明書のパスをフルパスで指定しましょう
鍵は nginx には指定せずクライアントがアクセスする際に指定します

動作確認

nginx を再起動しましょう

  • sudo nginx -s stop
  • sudo nginx

まずは curl を使って確認してみます
クライアント証明書を指定するオプションは --cert でクライアント証明書の鍵を指定するオプションは --key になります

  • curl -k --cert ./ca.crt --key ./ca.key https://localhost

これでアクセスできると思います
もしクライアント証明書と鍵が指定されていない場合は nginx が「400 No required SSL certificate was sent」というエラーを返します
またクライアント証明書と鍵の指定が誤っている場合は「400 The SSL certificate error」が返ります

こんな感じでクライアント証明書と鍵の指定が合っていないと正常にアクセスできないのでこの仕組を使ってアクセス制御を実現できるます

ちなみに Chrome などのブラウザでアクセスしたい場合はキーチェーンアクセスなどの証明書を管理するアプリに鍵をインポートしましょう
基本は p12 ファイルをそのまま配布してダブルクリックなどすれば各 OS に付属の証明書管理アプリが立ち上がってインポートできると思います

今回は curl とブラウザのクライアント証明書を使ったアクセス方法を紹介しましたがクライアントのソフトウェアによって証明書の設定は様々なのでそれぞれに合った設定をしてください

最後に

nginx でクライアント証明書認証を試してみました
クライアント証明書の発行は MacOS 付属のキーチェーンアクセスで行いました
キーチェーンアクセスに慣れている人はこの方法でも良いと思います
クライアント証明書の管理も簡単になります

openssl コマンドを使ってすべての作業をクライアント証明書の発行を行うことも可能です
openssl の手順はネットを検索すればいろいろ出てくると思います

また混乱しそうなので説明しておくとサーバ証明書とクライアント証明書は全くの別物です
同じ認証局から発行されている必要もないのでご注意ください

2019年12月27日金曜日

go-workers でワーカーをテストする方法を考える、その2

概要

前回 go-workers のテスト戦略を考えてみました
更に良い方法が見つかったのでコードを紹介します

環境

  • macOS 10.15.2
  • golang 1.12.9

main.go

変更点は Worker 構造体の関数として Perform を登録したのでそれを workers.Process に渡しています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/main/main.go
package main

import (
    "github.com/jrallison/go-workers"
    "github.com/hawksnowlog/a/worker"
)

func main() {
    workers.Configure(map[string]string{
        "server":  worker.GetRedisURL(),
        "process": "1",
    })
    // ここにワーカーのメイン処理を指定します
    w := worker.NewWorker()
    // w が持つ Perform 関数をワーカーのメイン関数として設定します
    workers.Process("default", w.Perform, 1)
    workers.Run()
}

worker.go

先程紹介したように Worker 構造体の関数として Perform を定義しています
Perform 内で NewWorker を呼ばなくなり直接 Worker 構造体のフィールドにアクセスできるようになっています
こうすることで Worker 構造体のフィールドをモックにすることで Perform を直接呼び出してテストできるようにしています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/worker.go
package worker

import (
    "fmt"
    "github.com/jrallison/go-workers"
)

// Worker 構造体は外部リソースを管理する構造体を持っています
// テスト時にモックしたいリソースは Interface として定義します (重要)
type Worker struct {
    logger *Logger
    Redis  RedisInterface
}

// Worker 構造体を生成します
func NewWorker() *Worker {
    return &Worker{
        logger: NewLogger(),
        Redis:  NewRedis(),
    }
}

// ワーカーのメイン処理になります
// ここで外部リソースにアクセスしたりします
// Worker 構造体の関数として定義することでテストしやすいコードにしています
func (w *Worker) Perform(msg *workers.Msg) {
    err := w.Redis.Open()
    if err != nil {
        w.logger.Info(err.Error())
        return
    }
    defer w.Redis.Close()
    a, _ := msg.Args().Array()
    w.logger.Info(fmt.Sprintf("%s", a[0]))
    err = w.Redis.Set(msg.Jid())
    if err != nil {
        w.logger.Info(err.Error())
        return
    }
    data, err := w.Redis.Get()
    if err != nil {
        w.logger.Info(err.Error())
        return
    }
    w.logger.Info(string(data))
}

redis.go

Perform の終了後に defer を使って Redis とのコネクションを Close しているので冒頭で Open できるように関数を追加しています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/redis.go
package worker

import (
    "github.com/gomodule/redigo/redis"
    "os"
)

// モックするために Interface を定義します
type RedisInterface interface {
    Set(data string) error
    Get() ([]byte, error)
    Close()
    Open() error
}

type Redis struct {
    client redis.Conn
}

const keyName = "data"
const defaultRedisURL = "localhost:6379"

func NewRedis() *Redis {
    client, _ := redis.Dial("tcp", GetRedisURL())
    return &Redis{
        client: client,
    }
}

func GetRedisURL() string {
    if url, ok := os.LookupEnv("REDIS_URL"); ok {
        return url
    }
    return defaultRedisURL
}

func (r *Redis) Open() error {
    client, err := redis.Dial("tcp", GetRedisURL())
    if err != nil {
        return err
    }
    r.client = client
    return nil
}

func (r *Redis) Set(data string) error {
    _, err := r.client.Do("set", keyName, data)
    if err != nil {
        return err
    }
    return nil
}

func (r *Redis) Get() ([]byte, error) {
    data, err := redis.Bytes(r.client.Do("get", keyName))
    if err != nil {
        return nil, err
    }
    return data, nil
}

func (r *Redis) Close() {
    r.client.Close()
}

fake_redis.go

redis.go 同様に Open を追加しています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/fake_redis.go
package worker

type FakeRedis struct {
}

const FakeJid = "054f5c637bc26dbd7491a758"

func NewFakeRedis() *FakeRedis {
    return &FakeRedis{}
}

func (r *FakeRedis) Open() error {
    return nil
}

func (r *FakeRedis) Set(data string) error {
    return nil
}

func (r *FakeRedis) Get() ([]byte, error) {
    return []byte(FakeJid), nil
}

func (r *FakeRedis) Close() {
}

worker_test.go

そしてテストを変更します
NewWorkerWorker を作成したらそのまま Perform を呼び出しています
その前に FakeRedis のモックを Worker フィールドに設定します
こうすることで Redis に直接アクセスすることなく Perform 関数内でモックを使ってくれます

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/worker_test.go
package worker

import (
    "fmt"
    "github.com/jrallison/go-workers"
    "testing"
)

func TestWorker_Perform(t *testing.T) {
    // キューイングされたメッセージを擬似的に生成
    msg, _ := workers.NewMsg(fmt.Sprintf(`{"jid":"%s","args":["1","2"]}`, FakeJid))
    mainWorker := NewWorker()
    // Redis には実際にアクセスしない FakeRedis を設定
    mainWorker.Redis = NewFakeRedis()
    mainWorker.Perform(msg)
}

最後に

おそらくこの方法が一番良い方法だと思います
テストのなので良い悪いとかはないと思うのですがソースコードもキレイでかつテストも書きやすいコードになるのでは思います

参考までに

2019年12月26日木曜日

go-workers でワーカーのメイン処理をテストする方法を考えてみた

概要

go-workers のメイン処理は実行される関数を workers.Process に渡すことで実行されます
ワーカー内で使われている構造体などは別途テストすれば良いのですがワーカー全体のテストはいろいろな外部リソース (DB や外部の HTTP サーバーなど) に依存しているケースもあり難しいです
今回は指定したワーカーのメインの処理全体をテストする方法をリファクタリングしながら考えてみました

環境

  • macOS 10.15.2
  • go 1.12.9
    • go-workers
    • redigo
    • kitlog
  • redis-server 5.0.5

ファイル構成、成果物

今回作成したソースファイルは以下の通りです
worker.go がワーカーのメイン処理を実装しているソースになります
ワーカーは Redis とロギング機能を使うことを想定して作っています
もちろん外部の HTTP サーバやシェルコマンドなどを扱う場合はそれに合わせたソースファイルを作成すれば OK です
そしてワーカーのテストファイルは worker_test.go になります

. ├── fake_redis.go ├── logger.go ├── main │ └── main.go ├── redis.go ├── worker.go └── worker_test.go   1 directory, 6 files

main.go 以外はすべて worker パッケージとして作成します

main.go

main.go は特に何もしていません
ワーカーのメイン処理である関数を workers.Process を使って指定するくらいです

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/main/main.go
package main

import (
    "github.com/jrallison/go-workers"
    "github.com/hawksnowlog/a/worker"
)

func main() {
    workers.Configure(map[string]string{
        "server":  worker.GetRedisURL(),
        "process": "1",
    })
    // ここにワーカーのメイン処理を指定します
    workers.Process("default", worker.Perform, 1)
    workers.Run()
}

worker.Perform がワーカーのメイン処理になります
また基本的にこのファイルは実行するだめだけのファイルなのでテストは不要かなと思います

worker.go

ここにワーカーのメイン処理を実装していきます
設計としてはワーカーが参照する外部リソースは別のファイルで構造体として管理します
そして、それら別ファイルで管理している構造体をワーカー内のメイン処理で使うようにします

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/worker.go
package worker

import (
    "fmt"
    "github.com/jrallison/go-workers"
)

// Worker 構造体は外部リソースを管理する構造体を持っています
// テスト時にモックしたいリソースは Interface として定義します (重要)
type Worker struct {
    logger *Logger
    Redis  RedisInterface
}

// Worker 構造体を生成します
func NewWorker() *Worker {
    return &Worker{
        logger: NewLogger(),
        Redis:  NewRedis(),
    }
}

// ワーカーのメイン処理になります
// ここで外部リソースにアクセスしたりします
func Perform(msg *workers.Msg) {
    mainWorker := NewWorker()
    defer mainWorker.Redis.Close()
    a, _ := msg.Args().Array()
    mainWorker.logger.Info(fmt.Sprintf("%s", a[0]))
    err := mainWorker.Redis.Set(msg.Jid())
    if err != nil {
        mainWorker.logger.Info(err.Error())
        return
    }
    data, err := mainWorker.Redis.Get()
    if err != nil {
        mainWorker.logger.Info(err.Error())
        return
    }
    mainWorker.logger.Info(string(data))
}

Perform が main.go でも指定したワーカーのメイン処理になります
あとで紹介しますがこの Perform 関数をうまくテストするための設計になっています

Worker 構造体で外部リソースの構造体を管理しているのがわかります
NewWorker を呼ぶことで Worker 構造体を初期化できます

redis.go

今回はワーカーが Redis にデータを格納したり取得したりするケースを想定しています
Redis へのアクセスは redis.go で構造体として管理します
また RedisInterface を定義しておきます
こうすることでテスト時に Redis 用のモックを作成できるようにしておきます (重要)

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/redis.go
package worker

import (
    "github.com/gomodule/redigo/redis"
    "os"
)

// モックするために Interface を定義します
type RedisInterface interface {
    Set(data string) error
    Get() ([]byte, error)
    Close()
}

type Redis struct {
    client redis.Conn
}

const keyName = "data"
const defaultRedisURL = "localhost:6379"

func NewRedis() *Redis {
    client, _ := redis.Dial("tcp", GetRedisURL())
    return &Redis{
        client: client,
    }
}

func GetRedisURL() string {
    if url, ok := os.LookupEnv("REDIS_URL"); ok {
        return url
    }
    return defaultRedisURL
}

func (r *Redis) Set(data string) error {
    _, err := r.client.Do("set", keyName, data)
    if err != nil {
        return err
    }
    return nil
}

func (r *Redis) Get() ([]byte, error) {
    data, err := redis.Bytes(r.client.Do("get", keyName))
    if err != nil {
        return nil, err
    }
    return data, nil
}

func (r *Redis) Close() {
    r.client.Close()
}

Redis 構造体は RedisInterface が持つ Get, Set, Close 関数を実装しています
今回はテスト用のアプリなので格納するデータは適当に設定しています

また golang にはすでに Interface から便利なモックを作成することができるツールがたくさんあります (参考)
今回は理解するために自分でモックを作りましたがより柔軟なモックを使いたい場合は素直にツールを使っても良いと思います

fake_redis.go

外部リソースは基本的にモックを作成する必要があります
そのために先程 RedisInterface を作成しています
モックの作り方は簡単で先程作成した RedisInterface が持つ関数を実装した構造を作成すれば OK です

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/fake_redis.go
package worker

type FakeRedis struct {
}

const FakeJid = "054f5c637bc26dbd7491a758"

func NewFakeRedis() *FakeRedis {
    return &FakeRedis{}
}

func (r *FakeRedis) Set(data string) error {
    return nil
}

func (r *FakeRedis) Get() ([]byte, error) {
    return []byte(FakeJid), nil
}

func (r *FakeRedis) Close() {
}

この FakeRedisRedisInterface を実装しているため Worker 構造体が持つ Redis フィールドに設定することができます
ワーカーのテストをするときにはこの FakeRedis を設定することで実際に Redis にアクセスすることなくテストを進めることができます

logger.go

今回はロガーも外部リソースとして管理しています
標準出力に JSON ログを出力する機能を持っています
今回は logger.go のモックは作っていませんがファイルなどにログを出力するようなケースではモックするなりテスト用のログファイルに出力するような工夫が必要かなと思います

またロギングに kitlog を使っていますが特に深い意味はないのでお好きなロギングパッケージを使って OK です

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/logger.go
package worker

import (
    kitlog "github.com/go-kit/kit/log"
    "os"
)

type Logger struct {
    logger kitlog.Logger
}

func NewLogger() *Logger {
    w := kitlog.NewSyncWriter(os.Stdout)
    logger := kitlog.NewJSONLogger(w)
    return &Logger{
        logger: logger,
    }
}

func (l *Logger) Info(msg string) {
    l.logger.Log("msg", msg)
}

worker_test.go

ここまで作成できてようやくワーカーのメイン処理である Perform 関数のテストが書けます
今回のテスト作成の方針は「Perform 関数と同じ流れをテストでも実装する」という方針にしたいと思います
本当は worker.Perform を呼び出してその結果エラーになるかどうかをチェックするべきだと思います
なぜなら Perform 関数の実装が変更される度にテストの実装も修正する必要があるためです

メインが変わればテストを書き換えるという流れ自体はそこまでおかしなことではないと思いますが、個人的にはあまりよろしくないかなとは思っています

また Perform に渡されるキューイングされたデータもテスト用に手動で作成します
これは go-workers にメッセージデータを作成する関数が用意されているのでそれを使えば OK です

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/worker_test.go
package worker

import (
    "fmt"
    "github.com/jrallison/go-workers"
    "testing"
)

func TestWorker_Perform(t *testing.T) {
    // キューイングされたメッセージを擬似的に生成
    msg, _ := workers.NewMsg(fmt.Sprintf(`{"jid":"%s","args":["1","2"]}`, FakeJid))
    mainWorker := NewWorker()
    // Redis には実際にアクセスしない FakeRedis を設定
    mainWorker.Redis = NewFakeRedis()
    // これ以降は worker.Perform と同じ処理を書くことでテストする
    defer mainWorker.Redis.Close()
    a, _ := msg.Args().Array()
    mainWorker.logger.Info(fmt.Sprintf("%s", a[0]))
    err := mainWorker.Redis.Set(msg.Jid())
    if err != nil {
        mainWorker.logger.Info(err.Error())
        return
    }
    data, err := mainWorker.Redis.Get()
    if err != nil {
        mainWorker.logger.Info(err.Error())
        return
    }
    mainWorker.logger.Info(string(data))
}

これで redis-server が動作していない状態でもモックがあるためテストできるようになります

動作確認

  • go fmt ./ ./main && go test -v ./

エラーなくテストできると思います
また Redis には実際にデータなどが格納されていないのも確認できると思います

最後に

go-workers でメインの処理となる perform 関数をテストする方法を考えてみました
ポイントは

  • ワーカーが使用する外部リソースは別ファイルとして構造体として管理する
  • テスト時に外部リソースに実際にアクセスしたくない場合は外部リソースの構造体をモックする
  • 外部リソースの構造体をモックするために必ず Interface を用意する
  • テスト時にワーカーのフィールドで管理している外部リソースのフィールドにモックを設定する

という感じです
複雑なことをやっているような気がしますが golang のテスト的には自然な流れかなと思います
むしろ golang でモックありのテストをする場合にはほぼ必須のパターンなので慣れておくことをオススメします

2019年12月25日水曜日

golang で標準出力のテストをする方法

概要

golang で fmt や log で標準出力に出したログの文字列をテストする方法を紹介します
少しポイントがあるのでそこも紹介します

環境

  • macOS 10.15.2
  • golang 1.12.9

メインコード

今回はロギングに kitlog を使っています
標準の log パッケージでも問題ないですが kitlog を使う場合は事前にインストールしておきましょう

  • go get github.com/go-kit/kit/log
  • vim $GOPATH/github.com/hawksnowlog/hoge/hoge.go
package hoge

import (
    "fmt"
    "github.com/go-kit/kit/log"
    "os"
)

type Hoge struct {
    logger log.Logger
}

func NewHoge() *Hoge {
    w := log.NewSyncWriter(os.Stdout)
    logger := log.NewLogfmtLogger(w)
    return &Hoge{logger}
}

func (h *Hoge) LoggingByKitlog() {
    name := "hawksnowlog"
    age := 99
    h.logger.Log("name", name, "age", age)
}

func (h *Hoge) LoggingByFmt() {
    fmt.Println("name=hawksnowlog age=88")
}

テストコード

  • vim $GOPATH/github.com/hawksnowlog/hoge/hoge_test.go
package hoge

import (
    "bytes"
    "io"
    stdlog "log"
    "os"
    "sync"
    "testing"
)

func captureOutput(f func()) string {
    reader, writer, err := os.Pipe()
    if err != nil {
        panic(err)
    }
    stdout := os.Stdout
    stderr := os.Stderr
    defer func() {
        os.Stdout = stdout
        os.Stderr = stderr
        stdlog.SetOutput(os.Stderr)
    }()
    os.Stdout = writer
    os.Stderr = writer
    stdlog.SetOutput(writer)
    out := make(chan string)
    wg := new(sync.WaitGroup)
    wg.Add(1)
    go func() {
        var buf bytes.Buffer
        wg.Done()
        io.Copy(&buf, reader)
        out <- buf.String()
    }()
    wg.Wait()
    f()
    writer.Close()
    return <-out
}

func TestHoge_Logging(t *testing.T) {
    // h := NewHoge() ここだとエラーになる
    output := captureOutput(func() {
        h := NewHoge()
        //h.LoggingByFmt()
        h.LoggingByKitlog()
    })
    if output != "name=hawksnowlog age=99\n" {
        t.Errorf("error: %s", output)
    }
}

実行

  • go fmt github.com/hawksnowlog/hoge && go test -v github.com/hawksnowlog/hoge

ポイント

captureOutput という関数に標準出力する処理を渡します
渡した処理内で出力されるログが文字列として返ってきます

ポイントは Hoge 構造体を作成するのも captureOutput の中で行う点です
もし captureOutput の外で h := NewHoge() すると Hoge 構造体でロギングしている情報がうまく取得できません

また比較する際に改行コードなどのエスケープ文字にも注意してください
タブ区切りでログを出力している場合にはタブ文字 (\t) が入ります

標準出力をキャプチャする関数はこれ以外にもたくさん紹介されていますがどれも仕組み的には同じです

参考サイト

2019年12月24日火曜日

MacOS でクライアント証明書を発行してみる

概要

キーチェーンアクセスを使ってクライアント証明書を発行する方法を紹介します
ちなみにサーバ証明書の発行ではないのでご注意ください
サーバ証明書は Let’sEncrypt などを使い正式な認証局を通してから発行しましょう

環境

  • macOS 10.15.2
  • openssl 2.8.3

キーチェーンアクセスでクライアント証明書を発行する

キーチェーンアクセスを開きます

そしてメニューから「証明書アシスタント」->「証明書を作成」を選択します

証明書のタイプを「SSL クライアント」にします

自己証明書のため警告が表示されますが「続ける」を選択します

以下のように作成されれば OK です
キーチェーンアクセスに追加したクライアント証明書があることを確認しましょう

.cer ファイルと .p12 ファイルを書き出す

証明書ファイルと鍵ファイルをキーチェーンアクセスで書き出します
作成したクライアント証明書を右クリックし書き出しを選択します

フォーマットを「証明書 (.cer)」にします

.cer ファイルが作成されたら続けて「個人情報交換 (.p12)」を選択します

パスワードを設定することができますが今回は空のまま作成しました

以下のように書き出しできれば OK です

.crt ファイルと .key ファイルに書き換える

MacOS で使う場合はこのままでも使えますが nginx や Apache Httpd Server などのメジャーな Web サーバで使う場合には変換が必要です
変換には openssl コマンドを使います

  • openssl x509 -in my_client_cert.cer -inform DER -out my_client_cert.crt -outform PEM
  • openssl pkcs12 -in my_client_cert.p12 -nocerts -nodes -out my_client_cert.key

鍵を変換するときにパスワードを求められますが今回は空にしているのでそのまま Enter で OK です

中間 CA 証明書を作成する

作成した鍵情報から自己認証局を作成することもできます
認証局が作成できれば中間 CA 証明書も発行できます
まずは認証局を作成します

先程作成したクライアント証明書の鍵を選択し「認証局を作成」を選択します

固有名のタイプは「自己署名ルート CA」にします
ユーザ証明書は「SSL クライアント」にします

これで認証局が作成できました
これはルート認証局になります

続けて中間認証局を作成します
同じ用に「認証局を作成」を選択します
そして今度は固有名のタイプに「中間CA」を選択します

先ほど作成したルート認証局が表示されるので確認します
今回は信頼されていないルート認証局になるので警告が出ますが作成します

中間 CA が作成されれば OK です

キーチェーンアクセスの一覧に作成対 CA と中間 CA が存在できると思います
あとは中間 CA を選択して再度書き出しを行えば中間 CA 証明書が発行できます
クライアント証明書を作成した手順と同じで .cer ファイルを書き出して .crt に変換すれば OK です

  • openssl x509 -in my_client_ca_cert.cer -inform DER -out my_client_ca_cert.crt -outform PEM

Tips: その名前の認証局構成ファイルはすでに存在します。別の名前を選んでください。と表示される場合は

バグなのかもしれませんが作成した認証局をキーチェーンアクセスの一覧から削除して再度作成しようとすると「その名前の認証局構成ファイルはすでに存在します。別の名前を選んでください。」と表示され認証局が作成できなくなりました
その場合は以下のディレクトリにある認証局のディレクトリを削除すれば再び作成できるようになります

/Users/username/Library/Application Support/Certificate Authority

最後に

MacOS で SSL クライアント証明書を発行する方法を紹介しました
結局 openssl コマンドを使うのであれば初めの手順から openssl コマンドだけで完結することも可能です
キーチェーンアクセスに慣れている人はキーチェーンアクセスを使っても OK です

参考サイト

2019年12月23日月曜日

Gitlab でガントチャートを markdown で書く方法

概要

Gitlab 10.3 上の場合 marmaid.js が使えるようになっており Gitlab 上の Issue などで markdown を使ってガントチャートを描くことが可能です

環境

  • Gitlab 11.11.3

方法

gantt + ```marmaid を使って記載します

サンプルは以下のとおりです

```mermaid
gantt
  title 開発スケジュール
  section v1
    設計 :done, design, 2018-01-25 12:00:00, 2d
    開発 :active, coding, after design, 3d
    テスト :test, after coding, 2d
  section v2
    設計 :design2, after coding, 3d
    開発 :coding2, after design2, 3d
    テスト :test2, after coding2, 3d
    リリース :crit, deploy,  after test2, 2d
```

ちゃんとデザインが適用されれば以下のように表示されます

Tips

調べてみたのですが Issue の開始と終了を使ってガントチャートを作ることはまだできないようです
Gitlab のエンハンス項目としては上がっているのでもしかしたらすでにできるかもしれませんが自分の環境ではできませんでした
Epic を使う ? (参考)

最後に

Gitlab でガントチャートを作成する方法を紹介しました
他にも使い方はあるので詳細は公式のドキュメントを御覧ください