Got Some \W+ech?

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

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で半年運用してみた * お金を調べてるうちにみつけた...最初からこれをみつけていたかった...

Github + CircleCI + DockerでGCEを動かす - レジストリ編

  • 2017/08/31: コメントでの指摘を受け、一部コマンドを変更

背景:

  • 今、(ほぼ)1人チームで継続性が強く必要な仕事をしているので、あらゆる面で運用を楽にしていきたい。なのでコードで楽に管理でき、デプロイまで自動でやってくれて、オフィスアカウント(googleアカウント)との連携もできるGCPに強く興味がある。特にContainer周りやGCEは深くほっていきたいエリア。
  • 最終目標はDockerイメージをGCPで動かすところだが、今回はイメージをGithub + CircleCIを用いてレジストリにPushするところまでが目的
  • コードはこちらです。

Cloud SDKインストール & セットアップ

https://cloud.google.com/sdk/docs/quickstart-mac-os-x

% tar xvzf google-cloud-sdk-121.0.0-darwin-x86_64.tar.gz
% ./google-cloud-sdk/install.sh
% gcloud init
% gcloud components update kubectl // Instal kubectl

GKE setup

  • Clusterの作成をGUIで。gcloudかRESTでも作成可能
    • gophish-clusterをasia-east1-a上に作った
  • コマンド環境の設定
% export PROJECT_ID=gophish-150317
% gcloud config set project ${PROJECT_ID}
% gcloud config set compute/zone asia-east1-a
% gcloud config set container/cluster gophish-cluster
% gcloud container clusters get-credentials gophish-cluster
  • Dockerイメージのビルド
% docker build -t asia.gcr.io/${PROJECT_ID}/gophish:v1 .
  • ビルドしたイメージをGKEのContainer RegistryにPush
% gcloud docker -- push asia.gcr.io/${PROJECT_ID}/gophish:v1
% gcloud docker -- search asia.gcr.io/${PROJECT_ID}
  • 問題なくできていれば下記のようにイメージがタグ付きで表示される

自動ビルドをする

Github + CircleCIの構成で。

事前準備

  • Githubリポジトリを作る。ローカルで作ったファイルをPushしておく
  • GithubとCircleCIを連携する。circle.ymlはない状態だがとりあえずビルド一回ぐらい回す
  • GCPプロジェクトのサービスアカウントキー(JSON)を取得する
    • プロジェクト > API Manager > 認証情報 > 認証情報を作成 > サービスアカウントキー
    • 役割は「編集者」の役割 をもっているメンバーだけが可能みたいだ。とりあえずCompute Engine default service accountを使っといた。
    • https://cloud.google.com/container-registry/docs/access-control

circle.yml作成

  • Githubフローに似た流れのジョブを作る。つまり…

ブランチを切ってMaster向けPRを作成。 ↓ Dockerイメージをビルド + テスト ↓ レビュー ↓ マージ && テスト環境デプロイ ↓ リリースtagをきる on Github ↓ 本番環境デプロイ

  • 真のGithubフローだとprを送った時点でデプロイされる、といったことをどこかで読んだ事が、切り戻しまでの時間を考えるとアグレッシブだな、と思う。切り戻しまでの時間 < Maximum Tolerant Downtimeであればいい、という発想なのだろうか。組織と個人のレベルが高ければ実現できそうだな。

  • このフローを作るには、Github > Settings > Webhooksのイベントで以下を設定しておけばいい

    • Push, Pull request, Deployment, Release

どう動くか

  • BranchをきってPRを作った時にはテストまで実行されてデプロイはされない

  • MergeするとMasterブランチでのビルドが走る。デプロイまでされる. GCPでみるとlatestタグのついたイメージが更新されていることがわかる

  • GithubでReleaseをすると、WebHookしてくれて、やはりデプロイまでしてくれる。ただdeploy.shにリリースタグがある場合の追加処理があり、それによりリリースタグが付与されたDockerイメージがレジストリにpushされる。以下は v1.0.1をリリースした例。こういう形で変更管理を楽にできそう。

gist.github.com

  • 以上。

その他

  • CircleCI内でgcloudをupdateしてるけど、デフォルトで最新化しててくれませんかね…15秒もかかっとんぞ
  • Dockerイメージをビルドするのが遅い。これはCircleCIのコンテナ内でビルドしてるので、キャッシュされないから。対策としてはdependenciesで、ジョブ開始時点での最新イメージをGKEからpullしてくること。これをすると30秒くらい早くなる。
    • CircleCI 2.0でなんとかするらしい。
  • deploy.shって, deploymentのタイプ毎に分けた方がいいのかな〜

DockerイメージをGKEのレジストリにアップロードする

背景:

  • 今、(ほぼ)1人チームで継続性が強く必要な仕事をしているので、あらゆる面で運用を楽にしていきたい。なのでコードで楽に管理でき、デプロイまで自動でやってくれて、オフィスアカウント(googleアカウント)との連携もできるGCPに強く興味がある。特にContainer周りやGCEは深くほっていきたいエリア。
  • 最終目標はDockerイメージをGCPで動かすところだが、今回はイメージをGithub + CircleCIを用いてアップロードさせるところまで
  • コードはこちらです。

Cloud SDKインストール & セットアップ

https://cloud.google.com/sdk/docs/quickstart-mac-os-x

% tar xvzf google-cloud-sdk-121.0.0-darwin-x86_64.tar.gz
% ./google-cloud-sdk/install.sh
% gcloud init
% gcloud components update kubectl // Instal kubectl

GKE setup

  • Clusterの作成をGUIで。gcloudかRESTでも作成可能
    • gophish-clusterをasia-east1-a上に作った
  • コマンド環境の設定
% export PROJECT_ID=gophish-150317
% gcloud config set project ${PROJECT_ID}
% gcloud config set compute/zone asia-east1-a
% gcloud config set container/cluster gophish-cluster
% gcloud container clusters get-credentials gophish-cluster
  • Dockerイメージのビルド
% docker build -t asia.gcr.io/${PROJECT_ID}/gophish:v1 .
  • ビルドしたイメージをGKEのContainer RegistryにPush
% gcloud docker push asia.gcr.io/${PROJECT_ID}/gophish:v1
% gcloud docker search asia.gcr.io/${PROJECT_ID}
  • 問題なくできていれば下記のようにイメージがタグ付きで表示される

自動ビルドをする

Github + CircleCIの構成で。

事前準備

  • Githubリポジトリを作る。ローカルで作ったファイルをPushしておく
  • GithubとCircleCIを連携する。circle.ymlはない状態だがとりあえずビルド一回ぐらい回す
  • GCPプロジェクトのサービスアカウントキー(JSON)を取得する
    • プロジェクト > API Manager > 認証情報 > 認証情報を作成 > サービスアカウントキー
    • 役割は「編集者」の役割 をもっているメンバーだけが可能みたいだ。とりあえずCompute Engine default service accountを使っといた。
    • https://cloud.google.com/container-registry/docs/access-control

circle.yml作成

  • Githubフローに似た流れのジョブを作る。つまり...

ブランチを切ってMaster向けPRを作成。 ↓ Dockerイメージをビルド + テスト ↓ レビュー ↓ マージ && テスト環境デプロイ ↓ リリースtagをきる on Github ↓ 本番環境デプロイ

  • 真のGithubフローだとprを送った時点でデプロイされる、といったことをどこかで読んだ事が、切り戻しまでの時間を考えるとアグレッシブだな、と思う。切り戻しまでの時間 < Maximum Tolerant Downtimeであればいい、という発想なのだろうか。組織と個人のレベルが高ければ実現できそうだな。

  • このフローを作るには、Github > Settings > Webhooksのイベントで以下を設定しておけばいい

    • Push, Pull request, Deployment, Release

どう動くか

  • BranchをきってPRを作った時にはテストまで実行されてデプロイはされない

  • MergeするとMasterブランチでのビルドが走る。デプロイまでされる. GCPでみるとlatestタグのついたイメージが更新されていることがわかる

  • GithubでReleaseをすると、WebHookしてくれて、やはりデプロイまでしてくれる。ただdeploy.shにリリースタグがある場合の追加処理があり、それによりリリースタグが付与されたDockerイメージがレジストリにpushされる。以下は v1.0.1をリリースした例。こういう形で変更管理を楽にできそう。

gist.github.com

  • 以上。

その他

  • CircleCI内でgcloudをupdateしてるけど、デフォルトで最新化しててくれませんかね...15秒もかかっとんぞ
  • Dockerイメージをビルドするのが遅い。これはCircleCIのコンテナ内でビルドしてるので、キャッシュされないから。対策としてはdependenciesで、ジョブ開始時点での最新イメージをGKEからpullしてくること。これをすると30秒くらい早くなる。
    • CircleCI 2.0でなんとかするらしい。
  • deploy.shって, deploymentのタイプ毎に分けた方がいいのかな〜

VagrantとAnsibleを触ってみたのでメモ

構成管理に手を出してみたので、メモ。 AnsibleとVagrantで。 本ドキュメントでは手順とハマリポイントを紹介しますが、手順は他の本やブログでもさんざん上がってるので、最小限にとどめます。

目的

実践的な意味での構成管理、プロビジョニングの勘所を探したい

Ansibleを選んだ理由

最初、Itamae かAnsible で迷ってたが、以下の観点からAnsibleを選択

記述方法

ItamaeじゃなくてAnsibleを選択したのは、rubyに詳しくなく、多少離れてるyaml記述の方が学習コストが少なそうだったから。docker fileもyml記述だし。複雑なことをするとツライっぽいけど、そこまで複雑じゃないからいいかな、とも。ここらへんは感覚値です。

ドキュメント

ドキュメントの総数やSOFの記事数も多かったのがポイント。公式ドキュメントの充実度もAnsibleが上だった。

後、moduleがAnsibleのが充実してたし、Andsible-Galaxyもよさげだった。

とか、色々言ったけど、結局決定打はない。というか決定できるほど知らないので、Ansible暫くやってつまったらitamaeでゴニョゴニョしに行く方針。

構成

Mac + VM on VirtualBox + Vagrant + Ansible

構築

Vagrant構築

mkdir ~/workspace/ansibleDemo
cd ~/workspace/ansibleDemo
% vagrant box add centos6-7 https://github.com/CommanderK5/packer-centos-template/releases/download/0.6.7/vagrant-centos-6.7.box
vagrant init centos6-7
  • Vagrantfile作成(下記を追加)
# vim Vagrantfile実行
config.vm.box = "centos6-7
config.vm.network "private_network", ip: "192.168.33.10"
  • VM起動
vagrant up
  • Ansibleインストール
brew install ansible
  • Inventory登録
echo "[TestServer]" > hosts                                                                       
echo "192.168.33.10" >> hosts                                                                       
  • ssh接続先設定
vagrant ssh-config --host 192.168.33.10 >> ~/.ssh/config                                           
% ansible -i hosts 192.168.33.10 -m ping
192.168.33.10 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
  • playbook作成
# vim playbook.yml
- hosts: TestServer
  become: yes
  tasks:
      - ping:
  • playbook実行
% ansible-playbook -i hosts playbook.yml 

とりあえず、出来たところはここまで

ハマりポイント

Guest VMpingが通らない

  • ansible経由どころか、hostマシン(Mac)からもできなかった
  • vagrantのVagrantFileにprivate_networkを記述すれば、staticにIPを決定出来るのかと思ったが、vagrant upしても接続できない。VM内でifconfigうつも、private_networkに記述したIPは表示されず
  • VirtualBoxからみるとVMのネットワークのアダプタ1にhost_onlyのインターフェースがあるはず。
  • 仕方ないので /etc/sysconfig/network-scripts/ifcfg-eth1を直接編集して、ネットワーク再起動
BOOTPROTO=none
ADDR=192.168.33.10
NETMASK=255.255.255.0
ONBOOT=yes
DEVICE=eth1
/etc/init.d/networks restart
  • 上記で解決

Vagrant up時にWarning: Authentication failure. Retrying...が出る

  • sshの設定を調査
% vagrant ssh-config --host 192.168.33.10
Host 192.168.33.10
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/Kengo/workspace/ansibleDemo/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL
  • IdentifyFile内のprivate_keyにマッチするpublic_keyがゲスト側にいなければいけないので、作る
% ssh-keygen -yf .vagrant/machines/default/virtualbox/private_key > public_key
% cat public_key
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCg4qWCq3mPom9TzcAdDoDHt61s7tjsmeHaD0pLePYW0p4+qZCr2u8cLk0gJElB7BQc3c3mMuo5iCsYoxrjXbVHpNNUDmRHNLdSxNVO3Z7qLY7VzyQWwoqyqrCcbN3fG/MV+6cs+8d0dcvHGrEYnR9O6Q81VS4sdBucM0J8jQ2oyzumr8QZdXFEEeQbG9SKOVIFpuPHBiOFV+skc22VfgZNlxANBizFV7uguCxhEoQs74L1XMvC1ae7TjVhnIZbZ1063QyXtPgO25TwFqZUsCltqFxgBA032YKZFAtFXyF5vu0pootG91biS9f43dccp8cFuizgLc5FkUYUhjDtNRLV
  • ゲスト側にpublic_keyを登録....けど、すでに存在した。
% vagrant ssh
Last login: Sun Aug  7 18:41:24 2016 from 10.0.2.2
[vagrant@localhost ~]$ cat .ssh/authorized_keys 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCg4qWCq3mPom9TzcAdDoDHt61s7tjsmeHaD0pLePYW0p4+qZCr2u8cLk0gJElB7BQc3c3mMuo5iCsYoxrjXbVHpNNUDmRHNLdSxNVO3Z7qLY7VzyQWwoqyqrCcbN3fG/MV+6cs+8d0dcvHGrEYnR9O6Q81VS4sdBucM0J8jQ2oyzumr8QZdXFEEeQbG9SKOVIFpuPHBiOFV+skc22VfgZNlxANBizFV7uguCxhEoQs74L1XMvC1ae7TjVhnIZbZ1063QyXtPgO25TwFqZUsCltqFxgBA032YKZFAtFXyF5vu0pootG91biS9f43dccp8cFuizgLc5FkUYUhjDtNRLV vagrant <- ★あれこれはssh-keygenして作ったものと同じ...
  • ぐぐったら.sshの設定がいけなかったらしい
[vagrant@localhost ~]$ chmod 0700 /home/vagrant/.ssh/
[vagrant@localhost ~]$ chmod 0600 /home/vagrant/.ssh/authorized_keys 

これでおk

ansibleでpingができない

% ansible -i hosts 192.168.33.10 -m ping
192.168.33.10 | UNREACHABLE! => {
    "changed": false, 
    "msg": "SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue", 
    "unreachable": true
}
  • どうもssh実行時の送信先の情報を保存していなかった
vagrant ssh-config --host 192.168.33.10 >> ~/.ssh/config                    
  • これでpingができた
% ansible-playbook -i hosts playbook.yml 

PLAY ***************************************************************************

TASK [setup] *******************************************************************
ok: [192.168.33.10]

TASK [ping] ********************************************************************
ok: [192.168.33.10]

PLAY RECAP *********************************************************************
192.168.33.10              : ok=2    changed=0    unreachable=0    failed=0   

以上。ヨサそう