Got Some \W+ech?

Could be Japanese. Could be English. Android, セキュリティ, 機械学習などをメインに、たまにポエムったり雑感記載したりします。

GoでAESアルゴリズム(GCMモード)を使った実装をする

GoでAESアルゴリズム(CBCモード)+PKCS7パディング+HMACを使った実装をするの続きです

さて、前回はHMACを組み合わせることで、暗号化における機密性(Confidentiality)だけでなく、完全性(Integrity)からなる認証まで実現することができました。しかし、これには1つ問題があります。面倒くさい! 面倒くさければ、手間を省こうと実装しないかもしれないし、よしんばしても実装ミスが発生するおそれがあります。その問題に対応するために出てきたのが、認証付き暗号(Authentication Encryption)です。

GCMはその1つで、アメリカ国立標準技術研究所(NIST)にも標準として認められています。また、暗号処理を並列化することで高速な処理を可能としています。そのためなのか、最新ブラウザ <--> サーバ間通信であれば、デフォルトでAES-GCMのTLS通信として選択されています。

image

また、GCMはパディングが不要なストリーム型の暗号です。

GoにおけるAES-GMCの実装

なんと、Goの"crypto/cipher"パッケージにデフォルトで入ってました。hmacみたいに独立してないし、Paddingのように自前実装が必要ないので、暗に「お前ら、うだうだ言わずGCM使っとけ?」と言われている気がします。勿論、私はそうさせて頂きますよ、Google様。

// GCM encryption
func EncryptByGCM(key []byte, plainText string) ([]byte, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block); if err != nil {
        return nil, err
    }

    nonce := make([]byte, gcm.NonceSize())// Unique nonce is required(NonceSize 12byte)
    _, err = rand.Read(nonce); if err != nil {
        return nil, err
    }

    cipherText := gcm.Seal(nil, nonce, []byte(plainText), nil)
    cipherText = append(nonce, cipherText...)

    return cipherText, nil
}

// Decrypt by GCM
func DecryptByGCM(key []byte, cipherText []byte) (string, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }
    gcm, err := cipher.NewGCM(block); if err != nil {
        return "", err
    }

    nonce := cipherText[:gcm.NonceSize()]
    plainByte, err := gcm.Open(nil, nonce, cipherText[gcm.NonceSize():], nil); if err != nil {
        return "", err
    }

    return string(plainByte), nil
}

func main() {
    cipherText, _ = EncryptByGCM(key, "12345")
    decryptedText, _ = DecryptByGCM(key, cipherText)
    fmt.Printf("Decrypted Text: %v\n ", decryptedText)
        // Decrypted Text: 12345
}

以上。取り急ぎ、これでGoにおけるAESアルゴリズムを用いた実装は一通り紹介出来たかと思います。

NONCE, 初期化ベクトルについて

これらは各データで一度きりしか使われないことを担保しなければいけません。元々の存在理由がリプレイ攻撃への対策です。 リプレイ攻撃は、例えばEC2サイトに注文した暗号通信を盗聴し、再度おなじ暗号通信を再送(リプレイ)することで2重の発注をさせてしまう様な攻撃です。もしくは、自分の口座に対する送金をする通信を盗聴し、それをリプレイすることで、何回も振り込ませてしまうような攻撃です。

これを使い捨てのナンスを使い暗号文を検証することで防御できるので、使いまわしてはいけません。なので、毎回ランダムに作り直しましょう。

GoでAESアルゴリズム(CBCモード)+PKCS7パディング+HMACを使った実装をする

GoでAESアルゴリズム(CBCモード)+PKCS7パディング+HMACを使った実装をするの続きです。

さて、前回はAES+CBC+PKCSパディングを使った実装例を紹介した。パディングを使って、任意の平文をブロック型暗号で暗号化することが可能になったが、一方で脆弱性がうまれてしまった。暗号文+パディングを繰り返し送ることで平文を一部推測できてしまうような脆弱性だ。この脆弱性の根本原因は、誰もが復号できてしまう部分にある。従って、復号処理のための暗号文を入力するものを認証することで解決できる。その仕組がMAC(Message Authentication Code)であり、今回はHMAC(keyed-hash MAC)を使った実装例が今回のものになる。

AES暗号化によるHMACとは

暗号文をHMAC用の共有鍵とハッシュ関数で導きだされるもの。復号をするまえにHMACを検証することで、共有鍵を持っているサブジェクトを認証することができる。

GoにおけるHMACは?

標準パッケージ内のcrypt/hmacで実装されている。パディングは実装されていなかったのに、なぜなのか。謎である。実装自体はシンプルになる。

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    paddedPlaintext := PadByPkcs7([]byte(plainText))
    cipherText := make([]byte, len(paddedPlaintext)) // cipher text must be larger than plaintext
    iv := make([]byte, aes.BlockSize)// Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText, paddedPlaintext)
    cipherText = append(iv, cipherText...)

        // MAC作成
    mac := hmac.New(sha256.New, []byte("12345678912345678912345678912345")) // sha256のhmac_key(32 byte)
    mac.Write(cipherText)
    cipherText = mac.Sum(cipherText)
    return []byte(cipherText), nil
func DecryptByCBCMode(key []byte, cipherText []byte) (string, error) {
    if len(cipherText) < aes.BlockSize + sha256.Size {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }

    // macの取り出し
    macSize := len(cipherText) - sha256.Size
    macMessage := cipherText[macSize:]

        // 暗号文から想定macを計算
    mac := hmac.New(sha256.New, []byte("12345678912345678912345678912345")) // sha256のhmac_key(32 byte)
    mac.Write(cipherText[:macSize])
    expectedMAC := mac.Sum(nil)

        // MACによる認証
    if !hmac.Equal(macMessage, expectedMAC) {
        return "", errors.New("Failed Decrypting")
    }

    iv := cipherText[:aes.BlockSize]
    plainText := make([]byte, len(cipherText[aes.BlockSize:macSize]))
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }
    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText[aes.BlockSize:macSize])

    return string(UnPadByPkcs7(plainText)), nil
}

その他

AES +CBC +パディング + hmacでためしたが、 AES +GCMで全てやりたかったことが出来るみたいなので、次回はそちらを試してみる.

GoでAESアルゴリズム(CBCモード)+PKCS7パディングを使った実装をする 編集

GoでAESアルゴリズム(CBCモード)を使った実装をするの続き

AESはブロック暗号であるため、16の倍数バイトの平文にしか適用できない。そうでない平文をAESで暗号化するには追加のデータ(パディング)を付与して16の倍数バイトにしてやる必要がある。AESを含めた共通鍵方式のパディングにはいくつか種類があって、Wikiによると以下の通りである。 * Bit Padding, Byte Padding, ANSI X.923, ISO 10126, PKCS#7, ISO/IEC 7816-4, Zero padding

白状すると、各々が何に適しているかはわからないし、調べる気力もなかった。ただPKCS #7は、RSA公開鍵方式で使われていたはずなので、信頼と実績でそれを使ってみようと思う。ただ、Goの場合は、Padding関係のパッケージが容易されてないので、自前実装することになる。メンドクサイ

PKCS7による実装

PKCS7のパディングは、RFC5652にある通り、追加したいパディング長を値にしたバイトを追加する形になる。数式にすると、以下のとおり。

  • k-(lth mod k)

kはブロックサイズであり、AESの場合は16になる。lthは平文(バイト)の長さになる。上記で導きだしたものが、パディングの値になる。例えば(lth mod k)=1だとしたら、01がパディングになり、(lth mod k)=2だとしたら、02 02がパディングになる。ただ、(lth mod k)=0の場合、16 16.....16がパディングになる。これをGoで実装すると以下の通り。

func PadByPkcs7(data []byte) []byte {
    padSize := aes.BlockSize
    if len(data) % aes.BlockSize != 0 {
        padSize = aes.BlockSize - (len(data)) % aes.BlockSize
    }

    pad := bytes.Repeat([]byte{byte(padSize)}, padSize)
    return append(data, pad...) // Dots represent it unpack Slice(pad) into individual bytes
}

func UnPadByPkcs7(data []byte) []byte {
    padSize := int(data[len(data) - 1])
    return data[:len(data) - padSize]
}

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    //if len(plainText) % aes.BlockSize != 0 { <-いらなくなった
    //  panic("Plain text must be multiple of 128bit")
    //}

    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    paddedPlaintext := PadByPkcs7([]byte(plainText))
    fmt.Printf("Padded Plain Text in byte format: %v\n", paddedPlaintext)
    cipherText := make([]byte, aes.BlockSize + len(paddedPlaintext)) // cipher text must be larger than plaintext
    iv := cipherText[:aes.BlockSize] // Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText[aes.BlockSize:], paddedPlaintext)
    cipherTextBase64 := base64.StdEncoding.EncodeToString(cipherText)
    return []byte(cipherTextBase64), nil
}

func DecryptByCBCMode(key []byte, cipherTextBase64 []byte) (string, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }

    cipherText, _ := base64.StdEncoding.DecodeString(string(cipherTextBase64))

    if len(cipherText) < aes.BlockSize {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }
    iv := cipherText[:aes.BlockSize] // assuming iv is stored in the first block of ciphertext
    cipherText = cipherText[aes.BlockSize:]
    plainText := make([]byte, len(cipherText))

    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText)
    return string(UnPadByPkcs7(plainText)), nil
}
func main() {
    plainText = "12345" // Paddingしないとエラーになる平文
    cipherText, _ = EncryptByCBCMode(key, plainText)
    fmt.Printf("Plaintext %v is encrypted into %v:\n", plainText, cipherText)
    decryptedText,_ = DecryptByCBCMode(key, cipherText)
    fmt.Printf("Decrypted Text: %v\n ", decryptedText)
}
>> 
                                                  |-> こっからパディング
Padded Plain Text in byte format: [49 50 51 52 53 11 11 11 11 11 11 11 11 11 11 11]
Plaintext 12345 is encrypted into [116 51 109 107 87 53 103 43 86 65 114 73 104 99 75 97 85 76 121 71 109 90 122 70 83 122 90 120 98 102 112 72 43 74 111 69 48 76 79 97 98 69 56 61]:
Decrypted Text: 12345

以上。

おまけ - パディングに対する攻撃

暗号文+パディングを繰り返し送ることで平文を一部推測できてしまう攻撃手法(パディングオラクル攻撃)がある。SSL3.0における脆弱性(POODLE)はこれを利用したものになる。この攻撃を防ぐためには、復号しようとしている暗号文が正しいサブジェクトによって生成されたものかを認証ればよい(MAC)。MACは又今後書くつもり。

GoでAESアルゴリズム(CBCモード)を使った実装をする

前回ではAESアルゴリズムを用いた暗号化・復号をGoで実装してみたの続きです。

今日の内容

ただAESアルゴリズムによる暗号のみだと、攻撃者は暗号文を操作 => 平文を操作することができます。これを解決するための1つのやり方が、SSL/TLSにも用いられているCBC(Cipher Block Chaining)モードです。モードとは、固定長以上の平文をブロックにわけ、各ブロックを暗号化アルゴリズムで暗号化する手法をさします。

CBCモードとはなんぞや

CBCモードは、1つ前の暗号文ブロックと平文ブロックをXORしたものを暗号化することを繰り返す手法になります。最初の暗号文ブロックを生成する際には、ランダムに生成した初期化ベクトル(IV)を平文ブロックとXORする形になります。このIVは復号時にも利用なので別途保存する必要があり、最終的に生成される暗号文にappendして保存するのが一般的らしいです。

image

また、復号時には1つ前の暗号文ブロックと、復号された暗号文ブロックをXORしたものが平文ブロックになります。ここでもIVが必要なので、保存した暗号文からIVを取得します。

image

これにより、攻撃者による暗号文の操作したとしても目的通りの結果にさせないことが可能になります。

CBCモードにおける暗号文ブロックの変化に対する動き

  • 破損時(暗号文ブロックの値が何らかの理由により変わった場合): 破損した暗号文ブロックにより復号される平文ブロックと、その後に復号される平文ブロックに影響します。
  • ビット欠落: 欠落した暗号文ブロックにより復号される平文ブロックとその後の平文ブロックのすべて

GoによるAES CBCモードの実装

ではGoでの実装方法を見てみましょう.

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    if len(plainText) % aes.BlockSize != 0 {
        panic("Plain text must be multiple of 128bit")
    }

    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    cipherText := make([]byte,  aes.BlockSize + len(plainText)) // 初期化ベクトルを保存するためにaes.BlockSizeを加えている
    iv := cipherText[:aes.BlockSize] // Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))

    return cipherText, nil
}
func DecryptByCBCMode(key []byte, cipherText []byte) (string ,error) {
    block , err := aes.NewCipher(key); if err != nil {
        return "", err
    }

    if len(cipherText) < aes.BlockSize {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }
    iv := cipherText[:aes.BlockSize] // assuming iv is stored in the first block of ciphertext
    cipherText = cipherText[aes.BlockSize:]
    plainText := make([]byte, len(cipherText))

    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText)
    fmt.Println(plainText)
    return string(plainText), nil
}
func main() {
    cipherText, _ = EncryptByCBCMode(key, "1234567891234567") // 16bye
    fmt.Println(cipherText)
    cipherText, _ = EncryptByCBCMode(key, "12345678912345671234123412341234") // 32byte
    fmt.Println(cipherText)
        // iv(32 byte) + 16byte

    plainText, _ = DecryptByCBCMode(key, cipherText)
    fmt.Println(plainText)
        // 12345678912345671234123412341234
}

以上。ただ、結局16の倍数 Byteの固定長平文しか暗号化できない。ブロック暗号の仕様上、しょうがないのでが、任意の長さの平文を暗号化したいのは当然の欲求だ。その場合、(16 - 平文%16)バイト文のパディングを追加する必要がある。次回は、それをかく。

GoでAESアルゴリズムを使った実装をする (と暗号歴史を一部紹介)

実はCISSPを受験しようと思ってて、最近その勉強をしている。(結構長く勉強してるはずなんだけど、業務が忙しくて実質時間が書けられてなくて進捗だめであああああああああああああああああああああああああああああああ)。まあ、こういうこと書いてるからなのかもしれないんだけど、しかたがない(しかたなくない)

CISSP自体に暗号はドメインとして含まれていないのだが、ドメインをまたがる問題として出題されているので、結城先生の「暗号技術入門」を読書している。

せっかくなので、そのアウトプットとしてGoでAES・CBCモードのSecret Keyを実装してみることにした。 余談だが、共通鍵方式はSecret Key, Common Key, Shared Key...など色々あるようだが、CISSP(の問題)ではSecret Keyに統一されているので、本投稿もそれに合わせる。

尚、@ken5scalは暗号技術の専門家ではないので、間違いはあると思う。何か見つけた場合は是非連絡を頂きたい。

AESアルゴリズム

さて、AESアルゴリズムの実装に入る前に、その説明だけしておこう。 これは別名Rijndaelアルゴリズムともいい、DESが機械の性能向上により物理的敗北を喫したのをきっかけに、NIST(米国立標準技術研究所)によりFIPS(連邦情報処理標準規格)と認定された。これにより保管データ暗号化時の推奨アルゴリズムとして、デファクトとなる。@ken5scalの使うMBPのディスク暗号化機能「FileVault」もAESアルゴリズムを利用している。

面白いのは、このアルゴリズムがNISTによって秘密に作成・運用されているのではなく、DESに変わるSecret Keyアルゴリズムを選定するコンペで採用されたものであり、公開されていることだ。wikiにも結城先生本にも記載されてるし、NISTもがっつり公開している。*1

実は米国は、AESが選定されるまで暗号に関しては非常にクローズな立場をとっていた。1992年の時点では暗号化ソフト(含むアルゴリズム)をAuxillary(補助的?)軍需品と指定し、輸出規制の対象としていたぐらいである。具体的にはアルゴリズム・鍵長(40bit以上)で規制をかけてた*2。当然、通信の暗号化などプライバシーの強化に大活躍したが、それと同時に犯罪者によっても悪用される。従って、法執行当局のシギント的捜査が難航するからコントロールしようとするのは当然のことだろう。包丁をコンビニに置くと...みたいな話に感じそうだが、当時の背景(第2次世界対戦後と冷戦後)を考慮すると、「せやな」と思えなくもない。当局による取り組みの1例としては、1994年に「鍵預託」(key escrow)というシステムを組み込んだ「預託暗号標準(EES)」があるだろう。ホワイトハウスはこれを承認し、電話通信・コンピューター通信に個人の秘密鍵を埋め、同時に鍵のコピーを連邦当局に保存する方向に持っていこうとしてた*3。日本のマイナンバーの前身みたいなものだろうか。

でも、結局のところ、規制を米国ほどかけたくないヨーロッパや市場からの反応により、管轄が1996年に国務省(Department of State)から商務省(Department of Commerce)に移管され、それとともに軍需品の対象ではなくなった。そして、その後色々あって(知らない)、2000年のAES選定につながる。ただし、現在でも輸出規制が完全になくなったかというとそうではなく、Secrete Keyアルゴリズムは鍵長64bit超のもの且つマスマーケットになっていないものが輸出規制対象となっている。*4

なお、米国に暗号の輸入出規制があると書いたが、別に米国に限ったものではない。武器輸出を規制するワッセナー・アレンジメントに調印した国や、独自の基準をもつ中国など、様々な規制があって面白いので、別途調べてみたい。*5

AESアルゴリズムとは(リトライ)

AESルゴリズムはブロック暗号アルゴリズムで、固定長(128bit)のブロックを、128/192/256bitの鍵で暗号化するもの。この暗号化は複数回行われ、各回(ラウンド)ではSubBytes -> ShiftRows -> MixColumns -> AddRoundKeyするもの。それぞれの解説は図が絡むのでしないw 結城先生の「暗号技術入門」を買ってください。実装といっても、そんな難しくはなく、すでにGoでは標準パッケージ「crypto/aes」で容易されているので、それを使ってやればおk。

// AESによる暗号化
func EncryptByBlockSecretKey(key []byte, plainText string) ([]byte, error) {
    c, err := aes.NewCipher(key); if err != nil { // NewCipherで暗号オブジェクトを作る。
        return nil, err
    }
    cipherText := make([]byte, aes.BlockSize)

    c.Encrypt(cipherText, []byte(plainText)) // Input/Output must be 16bits large
    return cipherText, nil
}

func DecryptByBlockSecretKey(key []byte, cipherText []byte) string {
    c, err := aes.NewCipher(key); if err != nil { // NewCipherで暗号オブジェクトを作る。
        fmt.Println(err.Error())
        return ""
    }

    plainText := make([]byte, aes.BlockSize)
    c.Decrypt(plainText, cipherText)

    return string(plainText)
}

func main() {
    plainText := "1234123412341234"

    key := []byte("1234123412341234123412341234123") // あえてAES規格ではない鍵長を使う
    fmt.Println(EncryptByBlockSecretKey(key, plainText) 
        # crypto/aes: invalid key size 31                                       <- 暗号オブジェクト作成時にKeySizeErrorが発生

        key = []byte("1234123412341234") // AES-128
    fmt.Println(EncryptByBlockSecretKey(key, "12341234123412345")) // Longer than 16 byte
        # [196 235 186 96 98 151 252 89 132 220 117 226 229 247 4 48] <nil> <- 1ブロック以上は処理対象にならない

    fmt.Println(EncryptByBlockSecretKey(key, "123412341234123")) // Shorter than 16 byte
        # panic: crypto/aes: input not full block

    cipherText,_ := EncryptByBlockSecretKey(key, plainText)
    fmt.Println(cipherText)                     
        # [196 235 186 96 98 151 252 89 132 220 117 226 229 247 4 48]

        plainText = DecryptByBlockSecretKey(key, cipherText)
    fmt.Println(plainText)
        # 1234123412341234 <- 復号できた。
}

しかし、これでは固定長の平文しか処理できない。固定長以上の平文を暗号化したい場合、16byteのブロック長にきって暗号化を繰り返せばいいのだが、これをブロック暗号のモードを実装しなければいけない。これを次回は実装してみる。

追記( @bata_24さん ありがとうございます)_

  • 復号化 vs 復号 前職のパイセンにもよく言われていたが、本来"復号化”は国語的におかしいみたいだ。なぜなら「復号」は暗号文を元に戻すという意味の動詞だからそうだ。暗号は名詞以外の定義はない。よって、暗号化の対義語は復号になる。ただし、結城本は復号化だが...って思ってたら、字面の対称性を重視しているらしい*6。ま、職場でもよく復号化...と言われてるが、とりあえず本投稿は復号に寄せてる。怒られそうだけど、正直どっちでもいい

  • Rijndael vs AES Rijndaelは厳密には128, 160, 192, 224, and 256bitsの鍵長をサポートしている。また、128, 160, 192, 224, and 256bitsのブロックサイズもサポートしている。AESは鍵長を128,192 and 256bits、ブロックサイズを128bitsに絞った点で異なる。

暗号解読〈上〉 (新潮文庫)

暗号解読〈上〉 (新潮文庫)

暗号解読 下巻 (新潮文庫 シ 37-3)

暗号解読 下巻 (新潮文庫 シ 37-3)

暗号技術入門 第3版 秘密の国のアリス

暗号技術入門 第3版 秘密の国のアリス

Github + CircleCI + DockerでGCEを動かす - ファイル暗号化 on Github編

  • 先に断っておくと本編はあまり、DockerとGCE関係ない。
  • 見られたくないファイルをGithubにあげたい場合、Github上では暗号化してCircleCIのビルドのタイミングで複合化したいニーズがあると思う。
  • 例えば 証明書をNGINXイメージに埋め込むとか。
  • なのでGithubにprするときにはAESアルゴリズムで暗号化すればどうカナ〜と思った。
  • せっかくなのでGoで
  • 単純で。こうやればおk。パッケージがあるのが素晴らしい。っていうか自力ではじめて暗号化するコード書いたな。
  • CBCモード使わないと、出力も入力も16byte固定なんすね。普段意識しないから新鮮だった。やはり自分で実装するのはいい。必ず発見があって面白い <- 一番いいたかったこと
func main() {
    // 
    key := []byte("1234123412341234123412341234123") // Not AES-128, 256, 512(bit)
    c, err:= aes.NewCipher(key); if err != nil {
        fmt.Println(err.Error()) // Erro発動
    }

    key = []byte("1234123412341234") // AES-128
    c, err = aes.NewCipher(key); if err != nil {
        fmt.Println(err.Error())
    }

    cipherText := make([]byte, aes.BlockSize)
    plainText := []byte("1234123412341234")

    c.Encrypt(cipherText, plainText) // Input/Output must be 16bits large
    fmt.Println(cipherText)

    c.Decrypt(plainText, cipherText)
    fmt.Println(string(plainText))
}

追記

  • よく考えてらIAMのkey management 使えばいいわ

Github + CircleCI + DockerでGCEを動かす - デプロイ編

  • 今日はデプロイするところまで.
  • 基本的にはKubernetesのチュートリアルを参考にしつつ作った.
  • 説明はしないスタイルで。

Kubernetes関連用語

  • 説明はしないと言ったな。アレは嘘だ。
  • Kubernetesはコンテナ化されたアプリをClusterにデプロイしたり、配布をスケジュール化したりする。今風に言うとオーケストレーションツール?
  • Cluster -> 複数のリソース群が1つのユニットとして接続されたもの。2種類のリソースから構成される 
    • Master: クラスター内部を管理する(coordinates). 監視したり、スケールしたり、更新したり
    • Nodes: アプリを走らせるVM。MasterとはKubernetes APIで通信する。Physical Host
  • Pod: 正直捉えづらかった。現在進行系で捉えきれてない。デプロイのタイミングで作られるコンテナの集合体 + 共通リソースの集合体。Logical Host. PodはNodeの上で走る。
  • Services: Podsの集合。Podsの外部ネットワークへ公開、ロードバランス、サービスディスカバリを出来るようにする
  • Labels: そのまんま。PodやServiceにラベリングできる。環境、バージョン、サービスタイプ(front, backend,db)などの情報を負荷できる

Gophishのデプロイ

> kubectl run gophish-deploy --image=asia.gcr.io/${PROJCET_ID}/gophish:latest --port=3333 # gophishアプリをClusterにデプロイ(ポート3333ではしらせる)。デプロイノードは1つ
> export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}') # Podの名前を取得
> kubectl expose deployment/gophish-deploy --type="NodePort" --port 3333 # 今回は1インスタンスしか立てない
> export NODE_PORT=$(kubectl get services/gophish-deploy -
ate='{{(index .spec.ports 0).nodePort}}') # Portを取得

さて、ここまでやれば外部公開されているはずなのだが、curl "$NODE_IP":"$NODE_PORT"してもtime outする。nmapしたところfilterとの結果が帰ってきたのでFirewall系とあたりをつける。

Firewallの設定

なのでコンピュートサービス > ネットワーキング > ファイアウォールルールで見ると、任意のIPから対象のノード:$NODE_PORTへのアクセス許可設定がされていなかったので、以下で設定

> gcloud compute --project "gophish-150317" firewall-rules create "external2gophish" --allow tcp:NODE_PORT --network "default" --source-ranges "0.0.0.0/0" --target-tags "gke-gophish-cluster-bc6565eb-node"

これで、curl "$NODE_IP":"$NODE_PORT"したら無事アクセスできた。でも、毎回NODE_PORT指定するのダルいなぁ...と思ったので、kubectl exposeでオプションがないか探したが見つからなかった(targetPortオプションは使ってみたが駄目だった)。ただ、yamlファイルでの方法なら指定が可能らしいのでyamlファイルを作成。

http://kubernetes.io/docs/user-guide/services/#type-nodeport

apiVersion: v1
kind: Service
metadata:
  labels:
    name: gophish
  name: gophish
spec:
  ports:
  - port: 3333
    #targetPort: 3333
    protocol: TCP
    nodePort: 30333
  selector:
    name: gophish
  type: NodePort

一旦、過去のサービスを消して再設定. 又、今回は指定したポートを指定してFirewallに穴を開ける。無事通る

% kubectl delete svc/gophish
% kubectl create -f gophish-service.yaml
% gcloud compute --project "gophish-150317" firewall-rules create "external2gophish" --allow tcp:30333 --network "default" --source-ranges "0.0.0.0/0" --target-tags "gke-gophish-cluster-bc6565eb-node"
% curl 104.199.208.112:30333                                                                                  
<a href="/login">Found</a>.

ブラウザ側からも大丈夫 f:id:kengoscal:20161127180234p:plain

NodePortじゃなくてLoadBalancingも試す

yamlを書き換えるだけ

apiVersion: v1
kind: Service
metadata:
  labels:
    name: gophish
  name: gophish
spec:
  ports:
  - port: 3333
    targetPort: 3333 <- ★
    protocol: TCP
  selector:
    name: gophish
  type: LoadBalancer <- ★

ロードバランサーが作成される過程でFirewallの穴あけもやってくれるらしいので、ロードバランサーに公開IPが付与され次第、アクセスできる。NodePortは手動でやらなきゃいけないからメンドクサイ。 f:id:kengoscal:20161127180846p:plain

[Kengo@Mac] ~/workspace/docker_gophish
% kubectl get svc                                                                                             
NAME         CLUSTER-IP     EXTERNAL-IP       PORT(S)    AGE
gophish      10.3.249.184   104.155.214.128   3333/TCP   3m
kubernetes   10.3.240.1     <none>            443/TCP    4d
[Kengo@Mac] ~/workspace/docker_gophish
% curl 104.155.214.128:3333                                                                                   
<a href="/login">Found</a>.

スケールアップ

折角ロードバランサー形態にしたので、スケールアップしてみる。おk。

# Before
% kubectl get deploy
NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
gophish-deployment   1         1         1            1           1h

% kubectl scale deployment gophish-deployment --replicas=2

# After
NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
gophish-deployment   2         2         2            2           1h

余談 - お金の話

24日の記事を含めて、このコンピュートエンジンを立ち上げたのが約1週間前。それから立ち上げっぱなしで、利用料金はトータルで1102円。為替とかはもろもろ無視すると、ロードバランサー・ストレージもあるものの、90%がCompute Engineにかかった費用と考えられる。内訳は以下の通り。

f:id:kengoscal:20161127183644p:plain

でも、あれれ?GCEって5node/clusterまで無料じゃなかったっけ?って思ったら、それはCluster管理の価格だった。ノード料金はそのままかかる。個人だと結構いたいかなあ。こまめに管理しないと。

Google Container Engine Pricing and Quotas  |  Container Engine Documentation  |  Google Cloud Platform

下記で計算もできる。 cloud.google.comf:id:kengoscal:20161127184716p:plain

次は

  • CircleCIでデプロイしようと思う。一旦サービスはおとそう。

デバッグ

> kubectl cluster-info #
> kubectl get nodes # Cluster内でアプリをホストできるノード
> kubectl get deployment
> kubectl proxy & # ローカルでいながらClusterのAPIに接続できる -> ブラウザからhttp://localhost:8001/uiで管理コンソールを開ける。便利。
> kubectl get pods # 存在しているpodsをリスト
> export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}') # Podの名前を取得
> curl http://localhost:8001/api/v1/proxy/namespaces/default/pods/$POD_NAME # 接続テスト
> kubectl describe pods # podsの情報(構成情報・イベント)を取得
> kubectl logs ${POD_NAME} # 対象Podのログを取得
> % kubectl exec -it $POD_NAME COMMAND #対象Pod上でCommandを実行。docker execみたいなもん。
> kubectl describe svc/gophish-deploy # サービスの情報

参考資料

Kubernetes - kubectl Overview * 安定の本家ドキュメント GKEで半年運用してみた * お金を調べてるうちにみつけた...最初からこれをみつけていたかった...