AWSのEC2でRetire Notificationというのがきた
まとめ
突然EC2インスタンスが故障して、1時間後ぐらいにAWSからそのインスタンス修復不可能だからretireさせるよという通知がメールで来ました。 stopさせようにもstoppingでなかなか進捗しなかったので、開発環境だったからterminateしたけど、terminateも30分弱かかった。
基本、Datadog監視で気づくけど、こういうことがあるんだなぁということと、AWSはちゃんとしてるなーと思ったという話です。
詳細
AWSからメール(超意訳)
[Retirement Notification] Amazon EC2 Instance scheduled for retirement.
あなたが使ってるインスタンスがハードウェア的な修復不可能な感じで壊れました。多分、もう既につながらなくなってるかもしれないし、つながるかもしれない。指定日時に完全にterminateするからそれまでにAMI作って起動し直してね。
見てみたらこのメールよりも前に該当のEC2インスタンスが死亡していた
- Datadogからalertがあがっていた
- dashbordのstatus checkが 0/2 に(初めて見た)
- AWS Consoleを見るとこういう表示が
rebootしろと書いてあったが・・・
GUIからrebootしても、何も起きない。。
とりあえずstopもstopping...
のまま10分ぐらい終わらない。
もしかしたら、これはもっと待てばstoppingしたかもしれない。
開発環境だったので、さくっとterraformとかで作り直しできるようになってたのでterminateした。でも、これも30分弱かかった。
Terraform実践入門 #3
入門三部作ラスト
ここまでで、簡単にTerraformの機能をおおざっぱに説明してきました。 今回は、そこそこ現実に即したインフラを作るまでの流れを追いながら、徐々にリファクタリングしていくことで実践的な考え方を身につけていきましょう。
すごく簡略化した一般的なAWSの構成を題材にします
本番に完全に即してる訳ではないけども簡略化した構成を例として、terraformでの構築、リファクタリングの過程をやっていきます。 ディレクトリ構成などに焦点を当てていきたいので、セキュリティグループなど細かいことは省いています。また、あくまでも例なのでそのまま動くコードにはなっていません。 下図のような構成です。
ロードバランサーにEC2インスタンスがそれぞれ2つずつぶらさがってるような構成。各インスタンスにはnginxとRailsアプリケーションがいるようなすごく一般的な構成デス。
で、admin画面とapp画面があって、admin.example.com
でアクセスするとadmin画面に、app.example.com
にいくと通常のユーザー用のweb画面に行くみたいな構成があったとします。
これをTerraformで書いていきます。
第一形態
私の戦闘力は53万です
ディレクトリ構成
最初のTerraformはこんな構成で各ファイルの役割は以下のようなものが考えつきます。シンプルにディレクトリ配下に全てtfファイルがある状態です。
. ├── ec2.tf // EC2インスタンスに関するresourceを書く ├── elb.tf // ELBに関するresourceを書く └── main.tf // provider情報などを書く
各ファイルはコンポーネントごとにある程度分けていきます。AWSコンソールと同じような考え方ですね。
main.tfというのは、別にcommon.tfとか、core.tfとかbase.tfとか基本となるやつみたいな意味であればまぁ名前は自由でいいかなと思います。main.tfとしてるのはプログラムのスタートってmain関数だからそれにならってるぐらいの感じです。
ファイルの中身
各ファイルの中身はこういう構成です。
main.tf
providerを書くだけです。
provider "aws" { region = "ap-northeast-1" }
ec2, elb.tf
EC2instanceはcount
を使って簡単に複数台立てられます。また、ELBでinstanceを指定する際も、*.id
とすることでcountで作成したresourceはterraformがlistに展開してくれます。
instanceやelbを指定する際にどのsubnetに属するかも書いておきます。
ec2.tf
resource "aws_instance" "admin" { count = 2 ami = "ami-999999" instance_type = "t2.micro" subnet_id = "subnet1" } resource "aws_instance" "app" { count = 2 ami = "ami-999999" instance_type = "t2.micro" subnet_id = "subnet1" }
elb.tf
こちらがELBのresource。instancesのところがポイントかと思います。
resource "aws_elb" "admin" { name = "admin-elb" instances = ["${aws_instance.admin.*.id}"] subnets = ["subnet1"] // その他、lister、healthcheckの設定など } resource "aws_elb" "app" { name = "app-elb" instances = ["${aws_instance.app.*.id}"] subnets = ["subnet1"] // その他、lister、healthcheckの設定など }
ここまでは、単純なケースなので特に問題は起きていないように思えます。 では、次のようなケースはどうでしょうか。
ここまでの環境をterraformで作ったあなたに、 開発メンバーから同じ構成で開発環境とステージング環境を作って欲しいといわれました。 別VPCで全く同じ構成でたてて欲しいと。つまりこういう構成です。
🤔 この場合、あなたならどのように追加するでしょうか?
環境ごとのファイルをコピペしてprefixをつける?
. ├── dev-ec2.tf ├── dev-elb.tf ├── stg-ec2.tf ├── stg-elb.tf ├── prod-ec2.tf ├── prod-elb.tf └── main.tf
このやり方には問題点があります。
フラット構造の問題点
terraformは実行するディレクトリ配下の全てのtfファイルを読み込むので、開発環境、ステージング環境、本番環境を全て同じ状態ファイル(tfstate)で保存します。 そのため、開発環境だけの変更をapplyしたいだけなのに、もし本番の差分があれば本番への差分も変更が入ってしまうということです。
また、単に今はファイル数が少ないから見分けがつきますが、多くなってくると管理が難しくなってきます。
そこでディレクトリを分ける次の方法です。
第二形態
くっくっく・・・ちなみに戦闘力にしたら100万以上は確実か・・・
先ほどのようにフラットではなく、環境ごとにディレクトリに分けて、同じようにresourceを書いていきます。 たいていのモデルケースではインフラコードはディレクトリごとに分かれて作られていると思います。
なぜ環境ごとに分けているかというと、見やすさもありますが、terraformのtfstateを分けたいというのが一番のポイントでした。
ディレクトリ構成
では、実際にディレクトリ構成を見てみましょう。
. ├── dev │ ├── ec2.tf │ ├── elb.tf │ └── main.tf ├── prod │ ├── ec2.tf │ ├── elb.tf │ └── main.tf └── stg ├── ec2.tf ├── elb.tf └── main.tf
このように、ディレクトリに分けることによってtfstateが環境ごとに作られるようになります。そのため、各環境ごとに変更を適用できて管理しやすくなります。また、それぞれのリソースファイルも見つけやすくなると思います。
パラメータ化する
ディレクトリを分けるのは理解しましたが、この場合にファイルの中身はどう書くのが正しいのでしょうか?
先ほどのresourceの設定をそのままコピペしただけだと、subnetを指定していたsubnet1
の部分が各環境のVPCごとに別のものになるので、そこだけを切り替えないといけません。
ここで登場するのがterraformのvariables
という変数の仕組みです。変数の宣言を書くために各環境にvariables.tf
というファイルを用意します。
Tips: このvariables.tfという名前はTerraformのコミュニティモジュールと呼ばれる有志が作っているモジュールのベストプラクティス的なところで紹介されている名前の付け方です。terraform-community-modules
variables.tfを追加した実際のディレクトリ構成です。
├── dev │ ├── ec2.tf │ ├── elb.tf │ ├── main.tf │ └── variables.tf ├── prod │ ├── ec2.tf │ ├── elb.tf │ ├── main.tf │ └── variables.tf └── stg ├── ec2.tf ├── elb.tf ├── main.tf └── variables.tf
各variables.tfでは、以下のように宣言して値は環境ごとに変更しておきます。
variable "subnet_id" { default = "subnet1" // stgだったらsubnet2にするみたいな感じ }
これを宣言しておくと、先ほどaws_instance
やaws_elb
のresource内で指定していたsubnetの部分は変数で書くことができるます。そのため、各環境の記述内容は同じままで、variables.tfの中身だけ書き換えればよくなります。
例としてadminのinstanceで指定していた部分です。このように直すことができます。
resource "aws_instance" "admin" { count = 2 ami = "ami-999999" instance_type = "t2.micro" - subnet_id = "subnet1" + subnet_id = "${var.subnet_id}" }
また、今回の例では触れていませんが、インスタンスの台数やインスタンスタイプなども環境ごとに変えたくなる値だと思います。そういったものも変数化しておくと対応しやすくなりますね。
これでたいがいのことはクリアできたような気がしますが、もう一歩先のリファクタリングがあります。例として次のようなケースはどうでしょうか?
企画担当者からランディングページ用のサーバーも作ってくれと言われました。 今まではadminとappだけでしたが、追加でadminとかappと同じ構成でLP用のサーバーも作って欲しいと言われました。 🤔この場合にあなたならどうするでしょうか?
resourceを増やす
これぐらいの規模なら、各環境のec2.tf、elb.tfにadminやappをパクってresourceをコピペすればいいだけだ🙋
そう思ってこんな風に各環境のファイルにコピペして回ります。
resource "aws_instance" "lp" { count = 2 ami = "ami-999999" instance_type = "t2.micro" subnet_id = "${var.subnet_id}" }
これでも問題ないといえば問題ありませんが、これからLP以外にも違う役割のサーバーを増やして欲しいとかQA環境も欲しいなどの要望が来るかもしれません。 また、新しい役割のサーバーを増やすには今現在のあなたはelbとinstanceを追加すればいいと分かっていますが、1ヶ月後に自分自身がこのことを覚えていられるでしょうか?
第三形態
たいした自信だねベジータ。
こういったときに使えるのがterraformのmoduleという機能です。
module化
今回の構成の場合、LBがあって、EC2がぶらさがっているという部分はどのサーバー(admin、app、lp)でも同じということに気づいたと思います。moduleを使うことでLBとEC2をぶら下げるという部分をグループ化することができます。
module、moduleと言いましたがやり方は簡単で、単にこのように別ディレクトリにまとめるだけです。ここでは、module/web_app
という名前でまとめました。
そして、各環境にあったec2.tfとelb.tfはweb_app.tfという一つのファイルにまとめています。
├── dev │ ├── web_app.tf │ ├── main.tf │ └── variables.tf ├── module │ └── web_app │ ├── ec2.tf │ └── elb.tf ├── prod │ ├── web_app.tf │ ├── main.tf │ └── variables.tf └── stg ├── web_app.tf ├── main.tf └── variables.tf
このようにmoduleにまとめたら、利用する側のweb_app.tfでは、以下のようにsourceとしてmoduleのディレクトリを指定するだけです。
module "admin" { source = "../module/web_app" } module "app" { source = "../module/web_app" } module "lp" { source = "../module/web_app" }
そして、moduleを利用する場合は、terraform planを打つ前に、terraform getというコマンドを打つ必要があります。 getは単にmoduleのファイルを実行ディレクトリ配下にコピーをしてきているだけです。 普通は意識する必要はありませんが、.terraform/modulesというディレクトリが自動的に作成されています。
moduleのスコープ
ここまでで、moduleの利用はできているのですが、この状態でterraform planをすると、実は失敗してしまいます。
terraformからは、moduleの中に書いてあるvar.subnet_id
なんてものはない!!って怒られます。
なぜかというと、terraformの第1原則である実行ディレクトリ配下に存在するtfファイルのみ同じtfstateで管理されるからです。 dev、stg、prodと、分けたら別のスコープになったのと同じで、moduleに対しても同じことが言えるのです。 dev/variables.tfに変数が宣言されていたとしても、moduleから見るとスコープ外なので読み取れないのです。
では、どうやってmoduleに変数を渡せばいいのでしょうか?
moduleへ変数を渡す
moduleに変数を渡す方法はきちんと用意されているので安心してください。
まず、module/web_app/variables.tf
というファイルを用意して以下のように書きます。
variable "web_app_subnet_id" {}
ここでは、web_app_subnet_id
という値を宣言しました。
そして、module内でvar.subnet_idとしていたところをvar.web_app_subnet_idとします。
moduleの変更が終わったら、次は呼び出し側で変数を渡してあげます。 先ほどのmoduleをimportしているところで、dev/variables.tfで宣言しているsubnet_idを渡してあげるように変更します。
module "admin" { source = "../module/web_app" + web_app_subnet_id = "${var.subnet_id}" } module "app" { source = "../module/web_app" + web_app_subnet_id = "${var.subnet_id}" } module "lp" { source = "../module/web_app" + web_app_subnet_id = "${var.subnet_id}" }
これでweb_appモジュールがたった4行で増やせるようになりました。 最終的なディレクトリ構成がこちら。
├── dev │ ├── web_app.tf │ ├── main.tf │ └── variables.tf ├── module │ └── web_app │ ├── variables.tf │ ├── ec2.tf │ └── elb.tf ├── prod │ ├── web_app.tf │ ├── main.tf │ └── variables.tf └── stg ├── web_app.tf ├── main.tf └── variables.tf
module内のresourceを調整
今までは各resourceで、aws_instance "admin"
のadminの部分のように、resourceの識別子がかぶらないように調整をしていました。
moduleにしたことで、もうadminだけのものではないのでinstanceなどのような一般的な識別子に変えてしまいます。moduleにしたことでスコープがかぶらなくなるので、どれだけimportされようと識別子がかぶることがなくなるのです!
このように、moduleの中はscopeが閉じるので、外からは変数を与えたり、今回は触れていませんが逆に値を取り出すにはoutput
っていう機能を使ったりして取り出します。
エンジニアのみなさんが大好きなカプセル化ができる訳です😍
outputはこちら: https://www.terraform.io/intro/getting-started/outputs.html
とはいえ、やりすぎ注意
ここまでくるともっと共通化できるじゃん、全部moduleにしちゃえばいいじゃんという発想もでてきます。 ただ現実は厳しいので、例えば開発にはLPいらないよとかそういったことが起きてきます。 なので、やり過ぎは禁物で適度に柔軟性を持たせつつmodule化することが重要です。
まとめ
Terraform入門を通してある程度理解したあなたは、既存プロジェクトのソースコードが読めるようになっているはずです!!自分のプロジェクトのインフラコードを見てみましょう。
Google Container Engine+Deis環境にDatadog監視をいれる
概要
- Google Container Engine(kubernetes)の監視環境の動向 - まーぽんって誰がつけたの?を受けてDatadogでやる
- Google Cloud Platform Integrationをいれる
- GCPのメトリクスをAPI経由でとってきてくれる
- kubernetes Integrationをいれる
- kubernetesのリソースとしてdd-agentとkube-state-metricsをいれる
kubernetes Integrationについて🕸
dd-agent
- おなじみのdockerで起動するタイプのdd-agent。dockerのmetricsを収集してくれる。
- 環境変数に
KUBERNETES = "yes"
をつけるだけでついでにkubernetesのmetricsもとってくれるようになるので基本的なものであればconfig設定など不要 - DeamonSetとしてdeployされるので各ノードに1agentいることになる
kube-state-metrics
- kube-state-metricsをpodとして起動して、ClusterIP型の内部用のエンドポイントを持つ
- clusterにつき1つ立ち上がる
- dd-agentがそのエンドポイントをたたいてmetricsを収集する
- kube-state-metricsは何をする君かというと、k8sの残リソースなどをAPIサーバーに問い合わせをする
Datadogの設定について🐶
- cluster-nameでフィルタリングできるので、同じAPI KEYを使ったとしてもグラフやモニターも思いのままにできる。最高☺️💯
Google Container Engine(kubernetes)の監視環境の動向
まずはk8sの監視環境について理解する
DatadogやStackdriverなどの外部サービスを使うと簡単にできそうなのではあるが、そもそも何を監視しようとしているのかよく分からない。なので、まずは外部サービスを使わない場合どうなっているか調べてみた。 ちなみにStackdriverはGoogleが買収したDatadogみたいなやつ。
外部サービスを使わない場合
- kubernetes上にメトリクス収集サービスをたてることになる
- まずはcAdvisorというGoogle製のコンテナのリソースメトリクス収集君。ただしこれはkubeletにすでに組み込まれている
- HeapsterというこれまたGoogle製のクラスターのメトリクス収集君がcAdvisorのAPIを呼んで収集してformatして保存先に流す
- Heapsterがしているのは、収集したメトリクスをinfluxDBやGoogle Cloud Monitoringに送るのが役割
- 収集したメトリクスはinfluxDBなどの時系列DBを自前でたててそこに保存しておく
- これだけだと各リソースのメトリクスは収集できてもk8s全体のobject単位の監視ができていない
- そこで、kube-state-metricsという全体で見て容量に空きはあるかなどの観点でk8sのapi-serverに問い合わせして収集してくれるadd-onをさらにたてることになる
- そしてこれらをまとめてGrafanaなどのグラフツールも自前でたててやっとこさグラフが見えるという感じ
Datadogを利用する場合
- とりあえず上記のことをがっと全部やってくれる💯
- k8sでdd-agent入れるには、clusterの各nodeにcontainerとしてagent動かしたい🤔
- しかしpodで入れようとすると同一nodeで動く可能性がある
- そこで
deamon sets
という各nodeに1つcontainerを動かせるk8sの仕組みを利用して起動する
- Pro planは10container/host なので、
$18/month * ノード数
- Docker integrationでより詳細なmetricsもいける
- GCP integrationも各種揃っている
Stackdriverを利用する場合
- とりあえず上記のことをがっと全部やってくれる💯
- GCPのダッシュボードで見れるような内容なら無料!
- alertも作れるけど、無料プランだと メール通知のみ
- Slack通知したいとなると1Resourceごとに
$8/month * GCP全体のリソース数
- 計算対象のリソースは、GKEのinstance数、GCEのinstance数、CloudSQLのinstance数など全体にかかってしまう
- GKE関係ないけどStackdriverよさそうなところ
- 外形監視が無料で作れて全世界からつっついてくれる
- GAEとの相性がよさそう(agentなしでログが見れたりデバッグできたりするようだ)
まとめ
機能と価格はそう変わらないのでDatadogに軍配が上がる気がする。 alertの柔軟性やグラフの見やすさ、使い勝手的にも。 これを調べたのは半年前ぐらいだから今はどうなんだろうか。
Google Container EngineとLocustを使って負荷テスト環境をさくっと作る
GKE+Locustの負荷テスト環境を作ろうと思った背景
GKEとLocustを選んでみた理由としては以下のようなポイントでした。
- インスタンスは秒課金なので気軽に利用できる
- GKE(k8s)なので動的ポートマッピングの設定などが簡単
- Locustは単なるpythonのスクリプトのため、urlでアクセスする以外にbotoを使ったKinesisへのアクセスなどの負荷テストも再現できる
- Node数10ぐらいにすれば
1000req/s
も再現できた
負荷テストのキモはシナリオを作るところにあるので、どうせなら、他の人も気軽に環境を作れるようにしとこうと思って作りました。
kinesisに対して実行するLocustとGKE上の構成図
- Locustはバッタやイナゴという意味で大量に虫の大群が襲ってくるイメージだと思う
- MasterとSlave構成をとることができてZeroMQを使って協調動作する
- k8sのReplica SetでSlaveの増減は簡単
- k8sだから動的ポートマッピング、サービスディスカバリなど自動的にやってくれる
- ClusterのNode数も簡単に増やせるし、実行してすぐに消せば課金も$1いかないぐらい
- k8sのyamlを書いてhelm(k8sのパッケージマネージャ的なやつ)で簡単にinstallできるようにしといた
Locustの実行中のイメージ
使い方
コードなどはこちらにあります。Rakeタスクで色々セットアップできるようになっています。 GKE前提になってますが、その他はいろいろ読みかえれば使えるはず・・!
ざっくり言うと、こんな流れです。
1. シナリオを実行するlocustのtaskを作成
2. rake cluster:create
でGKE作成
3. rake helm:install
でデプロイ
4. web UIから実行
シナリオを作るところは、pythonでできた負荷テストツールであるLocustを参考になんでも書けます。
ただのpythonのスクリプトなので、webアクセス以外の負荷テストのシナリオを書くことも可能です。例として、kinesisにアクセスするexampleのtaskもおいてあります。
まとめ
k8sとhelm便利
ログ収集基盤に求められる要素技術を理解する
○○○じゃダメなのか
単なるアプリケーションサーバー
- 秒間数百とか大量のデータを受け付ける場合に向いてない。
SQSじゃダメなのか
- キューイングが目的だけど、SQSは大量のデータを受け付けることは目的としてない。
- SQSは命令をつっこむ、別のworkerがそれを取得して処理をキックするみたいな使い方に向いている。
参考: Amazon SQS と Kinesis はどう違うのか?~ユーザが求めるキュー(queue)の姿~|AWSを使い倒せ | GiXo Ltd.
ストリームデータ処理
こういう、大量にリクエストがきて、低レイテンシで処理をしなきゃいけない場合は、ストリームデータ処理というジャンルの話になってくる。
ただし、単にストリームデータ処理といっても以下のような要素で構成されている。
- 大量のデータを受け付ける口
- メッセージキューイング、ユニークIDなどの付与
- データ処理
AWSでストリームデータ処理するには
AWSであれば
- 大量のデータを受け付ける -> kinesis
- メッセージキューイング、ユニークIDなどの付与 -> kinesis
- データ処理 -> EC2上のkinesis application とか Lambda
Amazon kinesisで広がるリアルタイムデータプロセッシングとその未来
GCPでストリームデータ処理するには
GCPであれば
- 大量のデータを受け付ける -> pub/sub
- メッセージキューイング、ユニークIDなどの付与 -> pub/sub
- データ処理 -> Dataflow
Terraform入門 #2 Terraformはこわくない!!
Terraform入門 #1の続きです。
Terraformを始めるのに一番の障壁
- 既存のインフラコードで試してみたいけどアクセスキーの準備とかアカウント申請が面倒だし、なんか壊しそうで怖い:scream:
- 自分でやるにしても、AWS契約しないと試せないんでしょと思って面倒になる:frowning2:
- もし自分のアカウントでやったときに高額請求されたらどうしよう:worried:
この怖さがTerraformへの障壁をあげてしまってる気がします。少なくともぼくはそうでした。
簡単に試せるやつあるよ
terraformはAWSのものじゃないと、Terraform入門 #1で言いましたが、他にも適用できるものが用意されてます。もちろん、GCPやAzureなどもそうですが、GitHubなどのサービスに対しても用意されてます。気になった人は、他にはどんなProviderがあるか確認してみましょう。 HTTPリクエストしたときのresponse bodyを取得するなんてものもあります!!
要はAPI呼び出して何か作成するということをするサムシングがあればそれはterraform化することができると言っても過言ではありません。 その中で簡単に試せるのがDockerです。Macなら簡単にdocker環境は入れられます。みなさんもちろん入ってますよね:eye::eye:?
DockerでTerraformを試してみよう!
Dockerの初歩的な知識と、Docker for Macはinstallされてる前提で始めます。terraformもhomebrewでさくっと入れておいてください。
それでは早速設定を書いて試していきましょう。
AWSや、GCPの場合でもそうですが、バックエンドにAPI呼び出しをするための設定情報を書きます。これはprovider
というディレクティブに書いていきます。
providerさえ書いたらあなたはterraformの世界に入ったと思ってください:tada:
Dockerの場合の書き方はTerraformのDocsをみればのっています。ここは最低限のMacのDockerに対して行う設定を書いてみます。
provider "docker" { host = "unix:///var/run/docker.sock" }
Docker for Macの場合、unixドメインソケットで接続してあげるというわけですね。
:point_up:Tips: 最初につまづくのがファイルの命名規則。たいてい、main.tfとか、core.tf、common.tfとかあるんだけど、どうすりゃいいんだと悩みますが、正解はtfがついてればなんでもオーケー。試しにファイル名を好きなものにしてみましょう。
terraform plan
さぁ、あとは好きなだけterraform planコマンドを打ってみましょう! planというのが要はdry runです。絶対に環境を壊すことはないので安心してください。
terraformは現在の状態を保持するといいましたが、planを打つことでその差分を確認できます。 今の状態でterraform planを打ってみましょう。とりあえず何かメッセージが出てきたと思います。providerを書いただけなので、何も差分は出ていないはずです。
$ terraform plan Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. No changes. Infrastructure is up-to-date. This means that Terraform did not detect any differences between your configuration and real physical resources that exist. As a result, Terraform doesn't need to do anything.
Terraformでnginxをたててみる
localhostで8080でdockerでnginxを動かしたい場合、こんな感じでdocker runで実行しますよね。
$ docker run -d --name my-nginx -p 8080:80 nginx:latest a2c05579e456b51227d7878e6e05754fd76d13b8f7345f39ff97cdeb68458f12
runしたあとはlocalhost:8080にアクセスするとnginxのページが見れます。
$ curl localhost:8080 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
terraform化
このdockerのコマンドの内容をTerraformで表現するとこうなります。先ほどのコマンドがどんなresourceに対応してるかは、公式Docを見れば分かります。
# 最新のnginxイメージ resource "docker_image" "nginx" { name = "nginx:latest" } # コンテナ起動 resource "docker_container" "nginx" { name = "my-nginx" image = "${docker_image.nginx.latest}" ports { internal = 80 external = 8080 } }
早速planしてみましょう
こんな感じの出力になったと思います。
$ terraform plan . . + docker_container.nginx bridge: "<computed>" gateway: "<computed>" image: "${docker_image.nginx.latest}" ip_address: "<computed>" ip_prefix_length: "<computed>" log_driver: "json-file" must_run: "true" name: "my-nginx" ports.#: "1" ports.3862886908.external: "8080" ports.3862886908.internal: "80" ports.3862886908.ip: "" ports.3862886908.protocol: "tcp" restart: "no" + docker_image.nginx latest: "<computed>" name: "nginx:latest" Plan: 2 to add, 0 to change, 0 to destroy.
それぞれ、+
がついてるresourceは新しく追加されるreourceを表してます。他には、-
が削除、~
が変更、-/+
というのは削除して新しく作り直すという記号があります。
最後の1行にも、2つのresourceが追加されるよと書いてありますね。
terraform applyしてみよう
実行すると、Applyが成功した出力がでてきたと思います。 ※ apply前に、さっきdocker runしたコンテナはkillしてrm -fしておきましょう。
$ terraform apply docker_image.nginx: Creating... latest: "" => "<computed>" name: "" => "nginx:latest" docker_image.nginx: Creation complete (ID: sha256:c246cd3dd41d35f9deda43609cdeaa9a...658f9c5e38aad25c4ea5efee10nginx:latest) docker_container.nginx: Creating... bridge: "" => "<computed>" gateway: "" => "<computed>" image: "" => "sha256:c246cd3dd41d35f9deda43609cdeaa9aaf04d3658f9c5e38aad25c4ea5efee10" ip_address: "" => "<computed>" ip_prefix_length: "" => "<computed>" log_driver: "" => "json-file" must_run: "" => "true" name: "" => "my-nginx" ports.#: "" => "1" ports.3862886908.external: "" => "8080" ports.3862886908.internal: "" => "80" ports.3862886908.ip: "" => "" ports.3862886908.protocol: "" => "tcp" restart: "" => "no" docker_container.nginx: Creation complete (ID: 7d5ccd716c3de242af221588ea303b4382dd3ab789f4841f888e2b9b549eda76) Apply complete! Resources: 2 added, 0 changed, 0 destroyed. ...
実際にコンテナがたってるかみてみましょう。動いてますね。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7d5ccd716c3d c246cd3dd41d "nginx -g 'daemon ..." 7 seconds ago Up 6 seconds 0.0.0.0:8080->80/tcp my-nginx $ curl localhost:8080 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
tfstateができています
applyすると、terraform.tfstate
というファイルがterraformによって作成されます。その中身がどうなってるか確認してみましょう。jsonファイルにいろんな情報が追加されてますね。この中に自分で指定したresourceの名前などが入ってることを確認してみましょう。
{ "version": 3, "terraform_version": "0.9.9", "serial": 0, "lineage": "5ecb5960-880c-4d99-88b8-6495ad61c6ea", "modules": [ { "path": [ "root" ], "outputs": {}, "resources": { "docker_container.nginx": { "type": "docker_container", "depends_on": [ "docker_image.nginx" ], "primary": { "id": "7d5ccd716c3de242af221588ea303b4382dd3ab789f4841f888e2b9b549eda76", "attributes": { "bridge": "", "gateway": "172.17.0.1", ...
差分を発生さてさるみる
先ほどの状態から、8080から8081にportを変更してみましょう。以下の変更を加えて
- external = 8080 + external = 8081
planをしてみましょう。
$ terraform plan -/+ docker_container.nginx bridge: "" => "<computed>" gateway: "172.17.0.1" => "<computed>" image: "sha256:c246cd3dd41d35f9deda43609cdeaa9aaf04d3658f9c5e38aad25c4ea5efee10" => "sha256:c246cd3dd41d35f9deda43609cdeaa9aaf04d3658f9c5e38aad25c4ea5efee10" ip_address: "172.17.0.2" => "<computed>" ip_prefix_length: "16" => "<computed>" log_driver: "json-file" => "json-file" must_run: "true" => "true" name: "my-nginx" => "my-nginx" ports.#: "1" => "1" ports.1078587976.external: "" => "8081" (forces new resource) ports.1078587976.internal: "" => "80" (forces new resource) ports.1078587976.ip: "" => "" ports.1078587976.protocol: "" => "tcp" (forces new resource) ports.3862886908.external: "8080" => "0" (forces new resource) ports.3862886908.internal: "80" => "0" (forces new resource) ports.3862886908.ip: "" => "" ports.3862886908.protocol: "tcp" => "" (forces new resource) restart: "no" => "no" Plan: 1 to add, 0 to change, 1 to destroy.
赤い表示で、forces new resource
というものが出てきたと思います。これはどういうことかというと、該当のリソース(ここではコンテナ)を動かしたままportを変更することはできないため、作り直すよってことを言っています。
最後の1行にも英語で1つ追加して、1つ削除しますと書いてあります。
今回は手元のDockerなので作り直しばっちこいですが、もし、これがAWS上のデータベースだとしたら・・:scream:
Terraformを使っていれば、こんな感じで危険を察知できたり、次に何が起きるかが分かるようになります。
applyしてみよう
docker psでCONTAINER IDが変わっているので、再作成されたことが分かります。curlでも8081でちゃんとつながってますね。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1bf5b57aa7a4 c246cd3dd41d "nginx -g 'daemon ..." 3 seconds ago Up 2 seconds 0.0.0.0:8081->80/tcp my-nginx $ curl localhost:8081 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title>
tfstate.backup
新しくterraform.tfstate.backup
というファイルができていると思います。これは、前回のものをbackupしたものです。このように前回の状態も自動的に保存しておいてくれます。
applyでどうしようもない誤った変更をしてしまった(いい例が思いつきませんが、、)場合に、そのtfstate.backupファイルからまたresourceを作り直すことができます。やったことないけど、完全に復元とはいかないです。
変数とインターポレーション
先ほどは、1つのコンテナでしたが複数コンテナをたてたいとします。 その場合に、同じ文字列を設定する部分は変数にしたいという思いがおきてくると思います。
例えば、各コンテナの名前は別の名前を設定する必要がありますが、my-nginx
というところは同じにして、後ろに1とか2とかをつけることで別名にしたいとします。
その場合は、まずvariable
というディレクティブを使って、このように変数を宣言することができます。細かい文法はここは無視してこういう風に宣言すると思ってください。
variable "name" { default = "my-nginx" }
そして、containerのリソース側で呼び出すときは、var.name
でアクセスできて、${}
を使って変数展開できます。shの変数展開の書き方とそっくりですね。
resource "docker_container" "nginx_1" { name = "${var.name}-1" image = "${docker_image.nginx.latest}" ports { internal = 80 external = 8080 } } resource "docker_container" "nginx_2" { name = "${var.name}-2" image = "${docker_image.nginx.latest}" ports { internal = 80 external = 8081 } }
この状態でplanをしてみましょう。nameの部分が変数展開されていることが分かるかと思います。
+ docker_container.nginx_1 . name: "my-nginx-1" . . + docker_container.nginx_2 . name: "my-nginx-2" .
ちょっと待って:raised_hand:
このコードを見て、プログラマのみなさんならうずうずしてしまう部分があるかと思います。
そう、port指定の8080
や8081
、nameのsuffixの-1
や-2
のようなベタ打ちはなるべく避けたいのではないのでしょうか。変数に配列、そしてforやifが使えれば・・・・
terraformでもfor文やif文のような機能がある
手続き型プログラミング言語ではないので素直にforやifと素直にはいかないですが、それと同じことを実現する手段は用意されています。
実際にコードを見てみた方が早いと思います。こちらをご覧ください。
variable "ports" { default = [8080, 8081, 8082, 8083] } variable "name" { default = "my-nginx" } resource "docker_container" "nginx" { count = "${length(var.ports)}" name = "${count.index % 2 == 0 ? "${var.name}-a-${count.index + 1}" : "${var.name}-b-${count.index + 1}"}" image = "${docker_image.nginx.latest}" ports { internal = 80 external = "${var.ports[count.index]}" } }
resource "docker_container" "nginx"
の下に新たにcount
と書かれたところがあります。これは、countに渡した数だけresourceを作ってくれ、これがfor文相当です。
length
というビルトイン関数を利用して、ports
という配列の長さをとってきています。
そして、count.index
で該当のindexにアクセスできます。forのindex相当です。
nameのところで書いているのがif文相当です。if文は書けないので三項演算子が用意されています。この処理の場合は、偶数の場合は、aという文字を、奇数の場合はbという文字を追加しています。
この状態でplanをしてみましょう
出力された結果はnameやportが想定した値となっているでしょうか。このように共通した処理はforやifなどを使ってまとめて書くことができます。
実際は、interpolationというところにもっとビルトイン関数が用意されてます。また、変数のタイプもmapやbooleanなどがあり、variablesにまとまっています。手元のdockerなので、ぶっ壊れても安心なのでいろいろと試してみましょう!
まとめ
Dokerを通してTerraformを気軽に試せる環境について紹介しました。
Providersを見て、Terraformで管理したら便利そうだなと思うものがあれば、自分の遊びプロジェクトでで導入してみるといいかもしれませんね:heart_eyes:
次回は、実践的な環境でTerraformのコードをリファクタリングしていく内容を紹介していこうと思います。リファクタリングの過程を通して実践的な知識を学んでもらえればと思います。題材は検討中・・
Terraform入門 #1 Terraformはいいぞ
前書き
人が書いたterraformのメンテぐらいは見よう見まねでやってるけど、一からやれと言われたらよく分からない。そう思ってた1年前ぐらいの自分を思い出しながら気軽に入門してもらうにはどうすればいいか考えてみました。 Terraformはこわくないしほんとに便利なので、その辺を感じ取ってもらえればと思います。
Terraformは何であって何でないのか、そしてなぜ必要なのか
そもそも、学習するにあたってTerraformって最高便利というところが分かってないと、なんでやらなあかんねんとなりそうだなと思ったので、まずはなぜ必要かを説明していきます。
Terraformってインフラのコードで書くやつでしょ?
terraformはインフラのリソースをコードで書けて実際に作成まで行ってくれます。この辺はだいたい知ってると思います。 ただ、Terraformはchefとかansibleといったプロビジョニングツールとはちょっとレイヤーが違います。
terraformはインフラの土台を作るもの、chefとかはそれに味付けをしていくという役割になります。雑に示すとこんな感じです。
Terraformは何をしてるのか
terraformは内部的には単にユーザーが書いたインフラの設定を見てAPIを呼んでるだけです。AWSならAWSのインフラの設定を書いて、単に裏側でAWSのAPIを呼んでるだけです。
じゃあなんでわざわざterraformなるものをかませないといけないのか。AWSのAPIを呼べばいいじゃんとなりますよね。次からは、実際にインフラを管理していくときに起きる問題点をあげながら、terraformのいいところを見ていきます。
Terraformのいいところ
terraformのいいところは、API呼び出しをしてくれるだけでなく、その状態を保持してくれます。また、リソース同士の依存関係も表現してくれます。それぞれみていきましょう。
インフラの状態を保持する
AWSのGUIだと現在の状態が想定しているものなのか、誰かが手動で変更したかなど分かる術はありません。操作ログを見れば分かることは分かりますが現実的じゃありません。
この問題をterraformがどう解決しているかというと、tfstate
というjsonファイルに、テキストでインフラのリソースの情報を保存することで状態を記憶するというアプローチをとっています。結構単純です。
リソースを新しく追加したり、削除したり、パラメータを少し変更したい場合は、このtfstateファイルと実際の状況をAPIを呼び出して確認し、必要最低限の差分のみ反映(API呼び出し)をしてくれます。もちろん差分がなければ何もしません。
依存関係
依存関係については、この辺はAWSとかでいざAPIをたたいてみないと分からないとは思うんですが、何かリソースを作るときには必ず他のリソースの情報が必要になると言えます。
例えば、EC2のインスタンスのIPアドレスを使ってDNSレコードを作りたいという場合があったとき、これをAPIでやるとこんな感じで呼び出しが必要です。
- EC2作成APIたたく
- 作り終わるのをしばし待つ
- 2のIDとかを指定して、EC2の詳細情報取得APIを呼ぶ
- IPアドレスをjsonから見つけてくる
- 4のIPアドレスをRoute53のレコード作成APIたたく
これが複数台ともなるとやってられなくなるでしょう、また、実際はもっと複雑な依存関係を持っています。
terraformなら、こんな感じで書いて1回実行するだけで、terraformが賢くAPI呼び出しをしてくれます。
resource "aws_instance" "web" { ami_id = "ami-123456" instance_type = "t2.micro" } resource "aws_route53_record" "www" { zone_id = "Z12345678" name = "abc.example.com" type = "A" ttl = "300" records = ["${aws_instance.web.public_ip}"] }
上の例で、records
のところに、aws_instance.web.public_ip
という自分で書いたまだ実際に作成されていないリソースのIPアドレスを設定することができています!ここがすごいところで依存関係が記載できていることになります。
ソースの詳細はあまり気にせずここでは雰囲気を感じ取ってください。
複数プロバイダにまたがれる
さっきの例で、例えばだけど、AWSで色々作ってるんだけど、なんかDNSだけは会社の制度で、別のサービスで管理しないといけない。そんなことがあった場合は、依存関係はもっと複雑になります。AWSのAPIの知識に加え、その別のサービスのAPIの知識も必要です。 また、呼び出し方も独特の違いがあるでしょう。
こんなときもTerraformは便利で、Terraformにはプロバイダと呼ばれる概念があり、これはAWSやGCPなどクラウドプラットフォームなどが主なもので色々用意されています。AWSは用意されているプロバイダの一つに過ぎないのです。 プロバイダは自作することもできますが、公式にあるもので基本的には困らないと思います。
先ほどの例で、DNSだけは、DNSimpleというサービスを使うことになったとしましょう。そんなときは、先ほどのaws_route53_record
の部分の設定をdnsimple_record
に変えて、providerの情報を追加してあげるだけでいいのです。
DNSimpleのAPI呼び出しってどうやるんだろうって調べる必要がないのがすごいところです。
resource "aws_instance" "web" { ami_id = "ami-123456" instance_type = "t2.micro" } provider "dnsimple" { token = "hogehogetoken" account = "piyopiyo" } resource "dnsimple_record" "www" { domain = "example.com" name = "abc" value = "${aws_instance.web.public_ip}" type = "A" ttl = 300 }
まとめ
どうでしょうか、Terraformのありがたみが分かったでしょうか?他のツールじゃなぜダメなんだとか、あれじゃダメなのとか疑問が湧いたら調べたり、質問してみたりしてください。きっと理解が深まると思います。 次回は、Terraformをもっと身近に感じるために気軽にできる環境で説明していこうと思います。
GoogleAnalyticsのiOS SDKをpodspec側のdependencyにすると相性が悪い
GoogleAnalyticsのSDKのラッパーライブラリを作りたい
そのためには、podspec側にdependencyを書いておく必要がある。こんな感じ。
Pod::Spec.new do |s| s.name = 'Example' s.version = '0.1.0' s.summary = 'A short description of Example.' 略 s.dependency 'Google/Analytics' end
これでpod installをするとtransitive dependencyのエラーが出てpod installができなくなってしまう。
原因は、GoogleAnalyticsが提供しているライブラリがFramework形式ではなく、.a
のstatic libraryのため。Google様がどうにかしてくれないとどうしようもない。
swiftの場合、use_frameworks!
が必要なため、ここで詰まってしまう。
回避策、そしてまた別の問題
Podfile側に下記のworkaroundを入れれば回避可能。 https://github.com/CocoaPods/CocoaPods/issues/3289
pre_install do |installer| def installer.verify_no_static_framework_transitive_dependencies; end end
しかし、pod installはうまくいくけどライブラリはリンクされないため結局ライブラリ側ではGAのSDKが利用できない。
リンクさせるためには、下記のようにpodspec側のxcconfigにFRAMEWORK_SEARCH_PATHS
やLIBRARY_SEARCH_PATHS
などを追記していかないといけない。実際は、他にも必要だが何が必要かを探して自分で書いていかないといけない。
s.pod_target_xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '"${PODS_ROOT}/Google/Frameworks"' }
言語のコミュニティがどうとかどうでもいい
当方、iOSをobjcからやってて、Androidも少し。フロントエンドは、jqueryの頃から、typescriptあたりまでなんとなく。 サーバーは、Rails、Scala Play frameworkあたりを少々。 今はインフラ周り、AWS、GCP、コンテナ周りは、docker、k8s、ECS、Deisなどを、少々かじっております。
で、
Scalaの話でなんかコミュニティがどうこうで揉めてるのを見て、なんかほんと、どうでもいいことで争ってる。言語なんてなんだっていいんだよ。ある言語をわかってる人は、いざやってみたら多分どのパラダイムでも、すぐ理解できる。分からないってなら、多分何やっても分からない。
なんていうか、外野のあーだこーだ、コミュニティがどうこうなんてどうでもいい。第一線の人たちは一般人なんかシカトでいいです。
ほんと、みんなすごい人たちなんだから、一般人がどうこうとか、コミュニティ怖いとかどうでもいい。みなさんは、ソフトウェアを作って欲しい。ぼくたちみたいな一般人なんて相手にしなくていいよ。
日本で有名な人たちは、一般人なんて相手にしなくていいから、日本発、すごいソフトウェアをみんな作ってて欲しい><
外国では次から次へと有名なソフトウェアが生まれて、何も生み出さない自分を悔しく思って。 こんな何もできない自分に比べてもっとすごい人たちが、なんか、しょうもないあーだこーだに巻き込まれてるのはもったいないし、損失だなと。
Objective-Cのallocは何をしてるのか
allocはNSObjectのクラスメソッド
alloc - NSObject | Apple Developer Documentation
+ (instancetype)alloc;
The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.
初期化前にメモリを確保。なのでこの時点で一応インスタンスはできている。でも初期化はされてない。サイズは確保されてない状態ってことだと思う。initして初めてサイズが確保される。なので、allocの返り値の型とinitの返り値の型は同じ。
Swiftの場合、 Hoge() ってやると、[[Hoge alloc] init]; まで呼ばれるのでallocを意識することはない。
allocationの説明
isaとは
http://algorithm.com.au/downloads/talks/objective-c-internals/objective-c-internals.pdf
In other words, an NSObject is simply a struct with a single field, named isa (“is a”, as in “a car is a vehicle”) that points to some sort of Class type.
AnyClassの正体
AnyObjectの型
typealias AnyClass = AnyObject.Type
何か分からないものの型を表現してる。
参考
kubernetesでserviceのselectorはnamespace内に閉じてるのか検証
まとめ
- 同じlabel名がつけられていてもnamespace内でのみ振り分けられる
検証の構成
- GKE上で試した
- Deploymentでただ単にnginxかapacheが動いてるだけ
- sandbox1ではnginx、sandbox2ではapacheが動いてる
- Service(LoadBalancer)で接続
- labelは同名にする
namespace: sandbox1 | Service | | | app: sandbox-webserver ▼ | nginx | ------------------------------ namespace: sandbox2 | Service | | | app: sandbox-webserver ▼ | apache |
ディレクトリ構成
├── sandbox1 │ ├── Chart.yaml │ └── templates │ ├── deployment.yaml │ ├── namespace.yaml │ └── service.yaml ├── sandbox2 │ ├── Chart.yaml │ └── templates │ ├── deployment.yaml │ ├── namespace.yaml │ └── service.yaml
検証コード(sandbox1)
- deployment.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: web namespace: sandbox1 spec: replicas: 1 template: metadata: labels: app: sandbox-webserver spec: containers: - name: web image: bitnami/nginx:latest ports: - containerPort: 80
- service.yaml
apiVersion: v1 kind: Service metadata: name: web namespace: sandbox1 spec: ports: - name: http port: 80 targetPort: 80 protocol: TCP type: LoadBalancer selector: app: sandbox-webserver
検証コード(sandbox2)
- deployment.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: web namespace: sandbox2 spec: replicas: 1 template: metadata: labels: app: sandbox-webserver spec: containers: - name: web image: bitnami/apache:latest ports: - containerPort: 80
- service.yaml
apiVersion: v1 kind: Service metadata: name: web namespace: sandbox2 spec: ports: - name: http port: 80 targetPort: 80 protocol: TCP type: LoadBalancer selector: app: sandbox-webserver
Swiftで動的にクラス作ってメソッド呼ぶ。しかも全く型情報がない状態で。要はjavascriptみたいなことがしたい
まとめ
- NSSClassFromString使う
- 引数が3つ以上だとperformSelectorは使えない
- SwiftだとNSInvocationは使えないので代わりにIMPを使う
- Class methodとInstance methodで呼び方ちょっと違う
- performSelectorの返り値の型は
Unmanaged<AnyObject>
でtakeUnretainedValue
を使う
引数が多いときのやり方で参考にしたobjcのコード
+ (id)initGraphRequestWithGraphPath:(NSString *)graphPath{ Class FBSDKGraphRequestClass = NSClassFromString (@"FBSDKGraphRequest"); id graph = [FBSDKGraphRequestClass alloc]; SEL aSelector = NSSelectorFromString(@"initWithGraphPath:parameters:"); IMP imp = [graph methodForSelector:aSelector]; id (*func)(id, SEL, id, id) = (void *)imp; graph = func(graph, aSelector, graphPath, @{}); return graph; }
Unmanaged<AnyObject>
の取り出し方
var unmanagedObject: Unmanaged
= someFunctionThatReturnsAnUnmanagedObject() var newValue: Type = unmanagedObject.takeRetainedValue as Type
takeRetainedValue
ってやつを使うみたい。でも、アプリがEXC_BAD_ACCESSかなんかだったかで落ちた。
takeUnretainedValue
だったら大丈夫だった。
ふーむ。allocしたのに、release漏れてるからとかなんだろうか。Swiftでは普通allocなんて書かないからあんまり普段考えないことだけど。
SwiftでIMPを呼ぶ
unsafeBitCastを使う。
NSClassFromStringでClass methodを呼ぶ
kubernetes meetup Tokyo #4に参加してLT発表してきた #k8sjp
今日はこれで発表してきます Kubernetes Meetup Tokyo #4 https://t.co/2rxxAnKyAB #k8sjp
— オオシママサト (@_mpon) 2017年4月20日
ECSからGKEに乗り換えたい
感想
- Google Japanの食堂でながーい会場で発表した。人めっちゃいた。
- kubernetesのserviceまわり、OnlyLocalのことが少し理解できた
- RBACっていうIAMみたいな機能の話がためになった
- ServiceAccountもなんなのか分かった
- ただ、歓談の時間がほとんどなくてあんまり人と話すチャンスがなかった
- 歓談のときに、kubernetesにしようと思ってたけど当時は難しそうすぎて、docker swarmにしたけど、そろそろkubernetesにしようかなーみたいなこと話せてよかった
- あと、openshift使ってる人の話も聞けて参考になった
- etcdが落ちた時、復旧する時にどれを神にすればいいか辛そうという話をしました
- meetupうちでもやりたいなーって話しかけに行ったんですが400人ぐらい毎回集まっちゃうからでかい会場じゃないと厳しいとのこと、残念><
- オーケストレーションはkubernetesがデファクトではありますが、みたいな雰囲気をなんとなく感じました
- AWS上にkubernetesたてるにしても、etcdとかmasterのapi-serverも冗長化するみたいな風にしてやれば意外といけそうではって雰囲気。kube-awsってやつで H/A etcdになるように更新してるみたい