GoでAESアルゴリズム(CBCモード)を使った実装をする
前回ではAESアルゴリズムを用いた暗号化・復号をGoで実装してみたの続きです。
今日の内容
ただAESアルゴリズムによる暗号のみだと、攻撃者は暗号文を操作 => 平文を操作することができます。これを解決するための1つのやり方が、SSL/TLSにも用いられているCBC(Cipher Block Chaining)モードです。モードとは、固定長以上の平文をブロックにわけ、各ブロックを暗号化アルゴリズムで暗号化する手法をさします。
CBCモードとはなんぞや
CBCモードは、1つ前の暗号文ブロックと平文ブロックをXORしたものを暗号化することを繰り返す手法になります。最初の暗号文ブロックを生成する際には、ランダムに生成した初期化ベクトル(IV)を平文ブロックとXORする形になります。このIVは復号時にも利用なので別途保存する必要があり、最終的に生成される暗号文にappendして保存するのが一般的らしいです。
また、復号時には1つ前の暗号文ブロックと、復号された暗号文ブロックをXORしたものが平文ブロックになります。ここでもIVが必要なので、保存した暗号文からIVを取得します。
これにより、攻撃者による暗号文の操作したとしても目的通りの結果にさせないことが可能になります。
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に絞った点で異なる。
- 作者: サイモンシン,Simon Singh,青木薫
- 出版社/メーカー: 新潮社
- 発売日: 2007/06/28
- メディア: 文庫
- 購入: 30人 クリック: 216回
- この商品を含むブログ (233件) を見る
- 作者: サイモンシン,Simon Singh,青木薫
- 出版社/メーカー: 新潮社
- 発売日: 2007/06/28
- メディア: 文庫
- 購入: 23人 クリック: 70回
- この商品を含むブログ (161件) を見る
- 作者: 結城浩
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/08/26
- メディア: 大型本
- この商品を含むブログ (14件) を見る
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種類のリソースから構成される
- 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>.
ブラウザ側からも大丈夫
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は手動でやらなきゃいけないからメンドクサイ。
[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にかかった費用と考えられる。内訳は以下の通り。
でも、あれれ?GCEって5node/clusterまで無料じゃなかったっけ?って思ったら、それはCluster管理の価格だった。ノード料金はそのままかかる。個人だと結構いたいかなあ。こまめに管理しないと。
Google Container Engine Pricing and Quotas | Container Engine Documentation | Google Cloud Platform
下記で計算もできる。 cloud.google.com
次は
- 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
% 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をリリースした例。こういう形で変更管理を楽にできそう。
- 以上。
その他
- 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をリリースした例。こういう形で変更管理を楽にできそう。
- 以上。
その他
- 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ボックスを取得
% vagrant box add centos6-7 https://github.com/CommanderK5/packer-centos-template/releases/download/0.6.7/vagrant-centos-6.7.box
- Vagrant初期化
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 VMにpingが通らない
- 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 }
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
以上。ヨサそう