やる気がストロングZERO

やる気のストロングスタイル

kubernetesクラスタで永続化データを扱うのは難易度が高いのでやめたほうがいい

仕事でkubernetesの本番運用を検討して思ったことをまとめる。

あくまで「kubernetesをとりあえず動かせる」程度の理解レベルにての話で、知識豊富な専門家が取り組むのであればこの限りではないと思う。

kubernetesクラスタで永続化データを扱うのは難易度が高いのでやめたほうがいい

kubernetesで永続化データを扱おうとすると途端に不安要素が増える。

「volumeを使えば永続化できる」ということはすぐに解るのだが本番運用を考えると

  • 容量が設定値を超えそうになったらどうすればいいのか?
  • 複数コンテナから同時にread/writeを行いたいけど、どう設定するのがよいのか?
  • マウントできなくなったらどのように対応すればいいのか?
  • 実ファイルはどこに存在しているのか

とか不安に思えることが湧いてくるけど、ちょっと調べてもなかなか情報が出てこない。

上記はパッと思いついただけなので、実際に運用するともっと意味不明な状態に陥る事はあると思う。
そのとき、kubernetes自体の仕組みがわかってないと問題解決のための方針すら決められずお手上げになってしまいそうで怖い。

実際に検証中に「なぜかvolumeに急にマウントできなくなりコンテナが立ち上がらない状態」になってしまった。
マウントできないので中のデータをサルベージもできない。
検証環境だからどうでもいいデータしか入ってなかったけど、本番だと絶対にどうにかしないといけない。
ぱっと調べたところ「クラスターの設定をどうのこうのする」というような英語の情報しか出てこず、これは「ちょっと勉強してkubernetesがとりあえず使えるようになった」程度ではどうにもならないと感じた。

vmやオンプレミスでもよくわからん現象の問題解決は難易度が高いと感じるのに、kubernetesでそうなるともはや手出しできない。さらにkubernetesは専門家も少ないので人に頼ることもできない。)

僕レベルの知識だと、このあたりの情報が簡単に出てくるくらいになってないと本番で運用するのは厳しいと感じた。

「使える」程度の知識でkubernetesを本番運用するならデータはクラスタ外に任せたほうが良さそう

永続データを持たないコンテナだけならkubernetesを使うのはメリットも多く、操作や挙動もシンプルになるので、良さそうと感じている。

「データはPaaS等の外部に任せてしまう」というのが「ちょっとkubernetesをかじった」程度の知識で扱える最大値なんではないかなと思った

YouTubeで公開システム構築をやってみている

「俺が考える最強の契約管理システムを作る」というタイトルのYoutube動画を投稿している。

[再生リスト]
俺が考える最強の契約管理システムを作る - YouTube

こんなん。

youtu.be

作業リポジトリも公開してます。

GitHub - mixmaru/my_contracts: 俺が考える最強の契約管理システム

なんでそんなことをやってるか?

作りたいシステムがあったので、作るついでに録画して公開することで「作業量は同じでアウトプットが2倍になってお得」みたいな考え。

どうやってるか?

Ubuntuデスクトップに「SimpleScreenRecorder」をインストールして録画してる。 ショートカットキーで録画スタートストップ出来る。
最初は見られたくないところ(簡単な英単語を翻訳にかけてるところとか。。)は録画ストップしてたけど、最近はあまり気にしなくなってきた。長々と調べものする感じになったらストップしたりしてる。

見てる側からすると急に時間が飛ぶ感じになるので僕の中では「ザ・ワールド」と呼ぶことにしてる。

mp4で録画して、細切れになったらffmpegで連結して1つの動画にして投稿する。
参考)ffmpegでMP4ファイルを結合する - Qiita
ただし、最近は1動画が長いのであまり連結はしなくなった。

何を作っているか?

契約管理システムを作ってる。
今、業務で既存の契約システムの面倒を見ていて、常々「俺ならこうしたい!」っていうのが臨界点に達したため。

完成するの?

わからない。
しないかもしれない。

気づき

  • YouTube投稿することがモチベーションになってる感がある。
  • 動画が長すぎて自分さえも見ない(ポイントを押さえて編集したい気もするけどめんどくさい)

【Go】nilチェックは value == nilだとダメな場合がある

Goのnilチェックで躓いたのでメモ。

nilチェックは value == nilだとだめな場合がある

こんな挙動になる

type Nameable interface {
    GetName() string
}

type Human struct {
    name string
}

func (h *Human) GetName() string {
    return h.name
}

func IsNilHuman(human Nameable) bool {
    if human == nil {
        return true
    } else {
        return false
    }
}

func main() {
    if IsNilHuman(nil) {
        println("nilだ")              // こっちにはいる(イメージ通り)
    } else {
        println("nilではない")
    }

    var nilHuman *Human
    nilHuman = nil
    if nilHuman == nil {
        println("nilだ")             // こっちにはいる(イメージ通り)
    } else {
        println("nilではない")
    }

    if IsNilHuman(nilHuman) {
        println("nilだ")
    } else {
        println("nilではない")    // こっちにはいる!(おもてたんとちゃう)
    }
}

なんで?

ここを読んでなんとなく理解した。
絶対ハマる、不思議なnil - Qiita

value == nilは型も揃っていなければ trueにならない。
意図通りにならなかったパターンでは [Nameable interface型のnil] == [ただのnil]という比較になっているのでfalseになる、と理解した。
[*Humanに代入されたnil] == [ただのnil]は、代入されたnilは[ただのnil]だから同じ型ということ??ちょっと理解しきれてない。。

確実にチェックするには?

こうするらしい。

func IsNilHuman(human Nameable) bool {
    if  (human == nil) || reflect.ValueOf(human).IsNil() {
        return true
    } else {
        return false
    }
}

【テーブル設計】フルネームで保持しているカラムを後から「名字・名前」に分けたい時

フルネームで保持しているカラムを後から「名字・名前」に分けたい時、どうすればいいか考えてみた。
出来たと思うけど、脳内シミュレーションしただけで実際にシステムに対して行ったことがあるわけではないので、もしかしたら穴があるかも。

サンプルケース

既存実装では「名字・名前」を分けてなかった

ユーザー情報を保持するuserテーブルがあって、名前を保持するnameカラムが定義されている。

userテーブル

id name
1 山田 太郎
2 岡田次郎

データベースからデータを読み出して、Userクラスとして使っている

// Userクラス
class User {
    id int
    name string

    construct(id int, name string){
        user_id = id
        name = name
    }

    function getName(){
        return name
    }
}


// dbから取得したデータをセット
user = new User(1, "山田 太郎")

// 名前取得
user.getName()  // "山田 太郎"が取得できる

今後は「名字・名前」を分けて保持してほしいという要件が発生した

ある時、名字と名前を使い分けたいという要望が発生した為、名字と名前を分けて保持することにした。
ただし、既に登録されているユーザーの名前を変換することができない。
(空白部分で「名字・名前」を分けてデータをコンバートすればいいという案が出たが、「岡田次郎」のように空白を入力していないユーザーもいたため不可能だった)
どうすればいいか?

名前保持用のテーブルに分割する

旧名前用データテーブル(フルネームしか持ってない)と新名前用データテーブル(「名字・名前」データで持っている)を用意して使い分ける。

userテーブル

id
1
2
3

user_nameテーブル(旧名前用データテーブル。userテーブルからnameを移動させる)

user_id name
1 山田 太郎
2 岡田次郎

user_separate_nameテーブル(新名前用データテーブル。以降はこのテーブルに名前データを入れていく)

user_id first_name last_name
3 三郎 川上

データ取得時はjoinして取る

select 
    user.id, 
    user_name.name, 
    user_separate_name.first_name, 
    user_separate_name.last_name 
from user
left outer join user_name on user_name.user_id = user.id
left outer join user_separate_name on user_separate_name.user_id = user.id

こんな感じで取れる

id name first_name last_name
1 山田 太郎 null null
2 岡田次郎 null null
3 null 三郎 川上

プログラム側で扱うデータ構造はこんな感じ

// インターフェース
interfacd IName{
    getFullName() string
    getFirstName() string
    getLastName() string
}

// 旧名前データ用クラス。INameインターフェースを実装する
class UserName < IName {
    user_id int
    name string

    construct(id int, name string){
        user_id = id
        name = name
    }

    function getFullName(){
        return name
    }

    function getFirstName(){
        return null
    }

    function getLastName(){
        return null
    }
}

// 新名前データ用クラス。INameインターフェースを実装する
class UserSeparateName < IName {
    user_id int
    first_name string
    last_name string

    construct(id int, first_name string, last_name string){
        user_id = id
        first_name= first_name
        last_name = last_name
    }

    function getFullName(){
        return last_name + " " + first_name
    }

    function getFirstName(){
        return first_name
    }

    function getLastName(){
        return last_name
    }
}

// Userクラス。INameインターフェースを実装したクラス(旧 or 新名前用クラス)を内部に持って、名前情報取得処理を委譲する。
class User{
    id int
    name IName
    
    construct(id int, name IName){
        user_id = id
        name= name
    }

    // getFullNameにメソッド名変更してもいいかも。
    function getName(){
        return name.getFullName()
    }

    function getFirstName(){
        return name.first_name
    }

    function getLastName(){
        return name.last_name
    }
}

// 使う時こんな感じ

// user_nameテーブルのデータを読み込む
user_name = new UserName(1, "山田 太郎")

// userテーブルデータを読み込む
user = new User(1, user_name)

// 使う
user.getName() // 山田 太郎
user.getFirstName() // null  (旧データなのでfirst_nameデータがない)
user.getLastName() // null  (旧データなのでlast_nameデータがない)


// user_separate_nameテーブルデータを読み込む
user_separate_name = new UserSeparateName(3, "三郎", "川上")

// userテーブルデータを読み込む
user = new User(3, user_separate_name)

// 使う
user.getName() // 川上 三郎
user.getFirstName() // 三郎(新データなのでfirst_nameデータがある)
user.getLastName() // 川上(新データなのでlast_nameデータがある)

(他案)userテーブルにfirst_nameとlast_nameカラムを追加するだけでいいのでは?=> 良くない

テーブル分割してややこしくしなくても、first_name, last_nameカラムをuserテーブルに追加すれば良い気がしたりするけど、個人的にはNG。

↓こんな感じ?

id name first_name last_name
1 山田 太郎 null null
2 岡田次郎 null null
3 川上 三郎 三郎 川上

※1,2が旧データ、3が新データ

なぜNGか?

データを冗長に保持している感じが嫌。
こんなデータを入れられてしまう可能性がある。

id name first_name last_name
4 岡本 太郎 武蔵 宮本

この場合、フルネームを取得すると「岡本 太郎」が取得できるけど、名字を取得すると「宮本」となる。
データ不整合が発生する可能性のあるデータ構造は、アプリケーション側やオペレーター側に整合性担保の責任を押し付けてくるので良くないと思ってる。

SynologyのNAS設定が楽しい

去年くらいにSynologyのNASを購入した。

GWに入って改めて設定を触ってて「便利だなー楽しいなー買ってよかったなー」と思ったので書いてみる。

なにが便利か?楽しいか?

NASだけどサーバーとして色々機能をもたせられる。
VPNサーバーとしての機能をもたせたり、Dropbox的な機能をもたせられたりもする。
これらの設定は、それぞれアプリとして提供されていて、GUIでポチポチと簡単に設定できるようになっている。
(synologyのnasしか触ったことないので、NASってのがそういうものなのか、synologyのNASだけがそうなのかはよくわからない。)

ホームネットワーク内でdropboxのようなディレクトリ同期機能を再現できる

nasと家の各デバイスnasの機能の「Cloud Station Server」で同期すれば、Dropbox的なディレクトリ同期機能を何台でも実現できる。
さらに、nas内の同期ディレクトリをDropBoxと同期しておけば、実質「無料版は3台までしか同期連携できない」というDropBoxの制限を超えて同期することができるようになる。
(synologyサーバーからのdropbox連携は端末件数にカウントされない(API連携だから?))

VPNサーバー設定して出張・旅行先から安全にネットできる

VPNサーバーとして設定すると、外からVPN接続でネット接続できるようになる。
ビジネスホテルの無料wifi使うときとか、VPNで安全にネットすることができる。

堅牢にデータを保管できる

macのtimemachineやubuntuのバックアップ保存先に指定して自動的に定期的にバックアップを取っていれば、NAS内で勝手に冗長化してくれる。
より安全にしたければこれをどこかのパブリッククラウドに連携して自動的にアップロードされるようにしておけば、より安全に保管することができそう。

ほかにも、、、

振り返ってみると「色々」というほどなんかしたわけでもないけど、サーバーとしてできることなら何でもできそうな感じなので「こんなんできないだろうか?」と考えるの自体が楽しい。

scpコマンドの挙動でハマった(ちょっとだけGo)

Goを書いていてsshでファイル転送したかったが、Goでのいい感じのクライアントが見つからなかったので普通にscpコマンドを実行させることにしたが、

scpコマンドの挙動でハマった。

雑にメモ。

やりたいこと

remoteの/var/data/ディレクトリの中身を localの/download/以下にダウンロードしたい。

remote
└── var
    └── data <- ここ以下のファイル群を
        ├── a
        │   └── 1.txt
        ├── b
        │   └── 2.txt
        └── c
            └── 3.txt

local
└── download
    └── 20200401 <-こんな感じでダウンロードしたい(ディレクトリ名はダウンロード日)
        ├── a
        │   └── 1.txt
        ├── b
        │   └── 2.txt
        └── c
            └── 3.txt

scpコマンド検討

/downloads/20200401ディレクトリを作っていない状態で

以下のように、事前に20200401ディレクトリを作ってない状態で

local
└── download

以下のコマンドだと、、

scp -r remote:/var/data/ /downloads/20200401

bashで実行した場合、意図通りになる

local
└── download
    └── 20200401
        ├── a
        │   └── 1.txt
        ├── b
        │   └── 2.txt
        └── c
            └── 3.txt

Goで実行した場合、意図通りになる

exec.Command(
   "scp",
   "-r",
   "remote:/var/data/",
   /downloads/20200401,
).CombinedOutput()

local
└── download
    └── 20200401
        ├── a
        │   └── 1.txt
        ├── b
        │   └── 2.txt
        └── c
            └── 3.txt

/downloads/20200401ディレクトリを作った状態で

以下のように、事前に20200401ディレクトリを作った状態で

local
└── download
    └── 20200401 <- こいつを事前に用意しておく

以下のコマンドだと、、

scp -r remote:/var/data/ /downloads/20200401

bashで実行した場合、意図通りにならない

local
└── download
    └── 20200401
        └── data <- これいらない
            ├── a
            │   └── 1.txt
            ├── b
            │   └── 2.txt
            └── c
                └── 3.txt

Goで実行した場合、意図通りにならない

out, err := exec.Command(
   "scp",
   "-r",
   "remote:/var/data/",
   /downloads/20200401,
).CombinedOutput()

local
└── download
    └── 20200401
        └── data <- これいらない
            ├── a
            │   └── 1.txt
            ├── b
            │   └── 2.txt
            └── c
                └── 3.txt

*を使った場合

scp -r remote:/var/data/* /downloads/20200401/

bash実行だとエラーになる

no matches found: remote:/var/data/*

Go実行だと意図通りになる

out, err := exec.Command(
   "scp",
   "-r",
   "remote:/var/data/*",
   /downloads/20200401/,
).CombinedOutput()

local
└── download
    └── 20200401
        ├── a
        │   └── 1.txt
        ├── b
        │   └── 2.txt
        └── c
            └── 3.txt

【Go】gorpにdecimal型を認識させる

経緯

データベースライブラリにgorpを使ってみている。
gorpでは(gorpというより、database/sqlの範疇かも)Goのint型はDBのintと、GoのstringはDBのtextやvarcharと、みたいに自動的にマッピングされて意識しなくてもデータやり取りができる。

Goには標準でdecimal型がなかったので外部ライブラリを使うことにした。
decimal - GoDoc

これをそのまま使った場合は、DBのdecimal型と勝手にマッピングしてくれた。

type Human struct {
   Id               uint64
   Money       decimal.Decimal 
}

外部ライブラリを裸でそのまま使うのが嫌だったので、内部でラップして使うようににしたら勝手にはマッピングしてくれなくなった。

type Human struct {
   Id               uint64
   Money       MyDecimal 
}

どうすればマッピングしてくれるか?

DBとのやり取りに使うには、database/sqlパッケージの「Scannerインターフェース」とdatabase/sql/driverパッケージの「Valuerインターフェース」が実装されている必要があるようだった。

decimalライブラリにはそれが定義されていたので、それをそのまま実行してやるようにすればOKだった。

type MyDecimal struct {
    decimal decimal.Decimal
}

func (d MyDecimal) Value() (driver.Value, error) {
   return d.decimal.Value()
}


func (d *MyDecimal) Scan(value interface{}) error {
   return d.decimal.Scan(value)
}

...