Terraformで配列をloopする時はfor_eachを使った方がいい

f:id:cloudfish:20200217183335p:plain

AWSのパラメータストアをTerraformで作成したのですが、for_eachを知らずにcountを使って作成したところ、追加や削除の際に色々と意図しない挙動になったので、回避策について備忘録を残しておきたいと思います。

環境

terraform(0.12.07)

やりたかったこと

パラメータストアに登録するデータが複数あったため、配列のように定義して一括登録したかった。

発生した問題

登録後にさらにパラメータストアを追加したところ、追加分だけでなく他のパラメータについても差分と判定された。

はじめに書いたtfファイル

以下が最初に書いたパラメータストアの設定になります。作成は問題なく完了しました。

paramete_store.tf

resource "aws_ssm_parameter" "ssm_parameter_app" {
  count       = "${ length( var.ssm_parameter_app_list.params ) }"
  name        = "/${var.env}/${values(var.ssm_parameter_app_list.params)[count.index].name}"
  value       = "${values(var.ssm_parameter_app_list.params)[count.index].value}"
  type        = "SecureString"
}

variable.tf

variable "env" {
  default = "dev" 
}

variable "ssm_parameter_app_list"{
  default = {
    params = {
      param1 = {
        name = "param1"
        value = "xxxxxxxxxxxx"
      }
      param2 = {
        name = "param2"
        value = "xxxxxxxxxxxx"
      }
      param3 = {
        name = "param3"
        value = "xxxxxxxxxxxx"
      }
         ・
         ・
         ・
      param10 = {
        name = "param10"
        value = "xxxxxxxxxxxx"
      }
    }
  }
}
この書き方の問題点
  • 追加の際に追加分以外の差分が発生する
  • 途中の要素の削除においても、削除分以外の差分が発生する

例えば、param11を追加した場合、param2以降の全ての要素が差分として発生します。また、削除においてもparam5を削除した場合も同様にparam5以降の要素が差分となります。

なぜこのような結果になるかというのはstateファイルを確認することで分かりました。以下stateファイルの抜粋になります。
要素のkeyが数字となっており、param1-10でソートされていることが分かります。
stateファイル(抜粋)

      "instances": [
        {
          "index_key": 0,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param1",
            "description": "",
            "id": "/dev/param1",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param1",
            "overwrite": null,
            "tags": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": 1,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param10",
            "description": "",
            "id": "/dev/param10",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param10",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": 2,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param2",
            "description": "",
            "id": "/dev/param2",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param2",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": 3,
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param3",
            "description": "",
            "id": "/dev/param3",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param3",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },


配列のキー(param1-10)でソートされた結果としては以下のようになります。param10がparam1の次に並び替えされてしまうため、上記のような差分が発生することになります。param1ではなくparam01と定義することでソートの問題は解決できるのですが、追加は正常にできるものの削除の問題は残ったままとなります。

param1
param10
param2
 ・
 ・
param9

このように意図しない差分を防ぐためには、for_eachを使えば解決できることがわかりました。

for_eachを使った書き方(その1)

variableに key=value連想配列を定義し、for_eachを利用して以下のようにセットすることができます。

paramete_store.tf

resource "aws_ssm_parameter" "ssm_parameter_app" {
  for_each = var.ssm_parameter_app_list

  name        = "/${var.env}/${each.key}"
  type        = "SecureString"
  value       = each.value
}

variable.tf

variable "ssm_parameter_app_list"{
  default = {
      "param1"  = "xxxxxxxxxxxx"
      "param2"  = "xxxxxxxxxxxx"
      "param3"  = "xxxxxxxxxxxx"
       ・
       ・
      "param10" = "xxxxxxxxxxxx"
  }
}

上記の書き方でparameter storeを作成すると以下のようなstateファイルになります。index_keyにparam1-10が設定されるため、追加や削除の際に意図しない差分が発生することはありません。

      "instances": [
        {
          "index_key": "param1",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param1",
            "description": "",
            "id": "/dev/param1",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param1",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param10",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param10",
            "description": "",
            "id": "/dev/param10",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param10",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param2",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param2",
            "description": "",
            "id": "/dev/param2",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param2",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },

ただし、このようにkeyとvalueのセットでリソースを定義できるのであればこの書き方で問題ないのですが、各要素ごとに複数の設定を持つのであればこの方法では難しくなります。

for_eachを使った書き方(その2)

例えば、これまではtypeは「SecureString」固定でしたが、これも変数で定義したいとなった場合は上記の方法では対応できません。
この場合については以下の書き方で対応ができました。

paramete_store.tf

resource "aws_ssm_parameter" "ssm_parameter_app" {
  for_each = var.ssm_parameter_app_list

  name        = "/${var.env}/${lookup(each.value, "name")}"
  value       = lookup(each.value, "value")
  type        = lookup(each.value, "type")
}

variable.tf

variable "ssm_parameter_app_list"{
  type = map(map(string))
  default = {
      param1 = {
        name = "param1"
        value = "xxxxxxxxxxxx"
        type  = "SecureString"
      }
      param2 = {
        name = "param2"
        value = "xxxxxxxxxxxx"
        type  = "String"
      }
      param3 = {
        name = "param3"
        value = "xxxxxxxxxxxx"
        type  = "SecureString"
      }
       ・
       ・
      param10 = {
        name = "param10"
        value = "xxxxxxxxxxxx"
        type  = "SecureString"
      }
   }
}
      "instances": [
        {
          "index_key": "param1",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param1",
            "description": "",
            "id": "/dev/param1",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param1",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param10",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param10",
            "description": "",
            "id": "/dev/param10",
            "key_id": "alias/aws/ssm",
            "name": "/dev/param10",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "SecureString",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },
        {
          "index_key": "param2",
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/dev/param2",
            "description": "",
            "id": "/dev/param2",
            "key_id": "",
            "name": "/dev/param2",
            "overwrite": null,
            "tags": null,
            "tier": "Standard",
            "type": "String",
            "value": "xxxxxxxxxxxx",
            "version": 1
          },
          "private": "bnVsbA=="
        },

まとめ

今回は配列をループしたかったためにcountを使いましたが、あまりよくない使い方だったため思わぬところでハマってしまいました。幸いまだ本番リリースされていないサービスだったので再作成することが可能でしたが、terraformの制御文は気をつけるポイントが多いですね。

EKSでEBSのReadIOが高騰した話

f:id:cloudfish:20200116173700p:plain:w200

はじめに

あるシステムのステージング環境を運用している中で、EKSクラスタのワーカーノードが頻繁にダウンするというか事象が発生しました。
色々な調査の末、なんとか解決できましたので発生の経緯と対応内容を書いておきたいと思います。

環境

発生経緯

あるサービスのステージング環境を作成し開発側に引き渡し、アプリのデプロイが行われシステムテストが進められていました。

突然のNot Ready

開発担当からシステムからレスポンスがなくなったと連絡があったので、確認したところノードのステータスがNot Readyとなっていました。
開発作業に支障が出ていたため、とりあえず該当のEC2を再起動して復旧させました。
その後メトリクスをざっと確認したとところCPUが100% でしばらく張り付いていたので、該当時間帯にCPU使用率の高かったコンテナを調べて開発側に連絡して対応しました。開発中なのでよくあることだと思い、この時はそれ以上深追いもしませんでした。

以降1日1回程度Not Readyが発生

数回は再起動で対処していたのですが、流石に頻度が多くなってきていたのでインスタンスタイプをt3.mediumからc4.largeに変更ししました。別件で手を取られていたこともあるのですが、安易にタイプ変更をおこない一旦様子を見ることにしました。
が、それでも相変わらずNot Readyとなるため、本格的に原因調査することにしました。
その時点でやっと気が付いたのですが、当初見ていたメトリクスがCloudWatchから取得していたものではなくホスト上のメトリクスだったため実際は張り付くまでCPUが高くもなくCPUクレジットを使い切っている状況でもなかったことが分かりました。ここは始めにしっかり見るべきところだったと思います。

EBSの読み取り高負荷

CPU使用率をよく見たところのIOwaitの占める割合が異常に高いことに気がつきました。
そこでEBSへの読み込みもしくは書き込みが高くなっているのではと考えメトリクスを確認したところ、読み取りが最大秒間150MiBでしばらく張り付き、バーストバランスを使い切って0%となっていることが分かりました。バーストバランスがなくなるタイミングでNot Readyとなっていたため、原因はEBSのRead高負荷ということが分かりました。

ReadIOが高いプロセスの調査

Not Readyになるトリガーは分かりましたが、EBSのReadIOが高い原因を引き続き調査しました。
バッチ処理を行うPodがそのノードで常に起動していたため開発側でも調査してもらいましたが、特段不審な処理はないようでした。
インフラ面からは、IOの高いプロセスを特定するため、ノードにログインしてiostatコマンドで高負荷プロセスを確認することにしました。
ログ出力のfluentdのIOが高い可能性があることは分かっていましたが、さすがに秒間150MiBは高すぎですが、再発したタイミングでプロセスを確認したところ、ログ出力用のfluentdが秒間100MiBを超えるIOとなっていました。ログの読み込み設定を変更するなど試しましたが、いずれもIOは高騰するため最終的にfluentdのPodを落とすことにしました。
しかしながら、fluentdを落として以降も別のプロセスでIOが高騰し、しかも特定プロセスではなく様々なプロセスのIOが高いという状況となりいまいち原因がつかめませんでした。

ReadIOとメモリ

しばらくプロセスを眺めるもよく分からない状況が続きました。
そして改めて事象発生時のメトリクスを確認して見たとことろ、メモリと相関関係にあることに気がつきました。当初メモリも確認していたものの数百MBの空きがあったことや、OOM killerも発生していなかったので関連をあまり見ていなかったのですが、下のグラフを見るとメモリ使用量がある一定の閾値を超えた時点でReadIOが高騰し始めるように見えました。他の発生時点のメトリクスも同様の傾向を示していましたのでおそらくReadIOが高騰するトリガーとしてはメモリ使用量だということが分かりました。同時にCPUも上がっていますが、これはReadIOが高騰した結果、IO待ちが発生してCPU使用率が上がったものと考えられます。そのため、今回の発生順としては、メモリが使用率が上がった結果、ReadIOが高騰し、それによりCPU使用率も上がったものと推測できます。
f:id:cloudfish:20200130191737p:plain

対策

メモリが発生トリガーだと分かったため、以下2点の対策を取ることにしました。

  • インスタンスタイプのサイズアップ(t3.medium → m5.large)
  • k8sのリソース制限(kube-reserved、system-reservedを適当に確保)

1ヶ月ほど様子見しましたが、今のところ再発もしていないので解消されたものと思われます。

まとめ

今回の事象はなかなか原因がつかめず調査にも苦労しました。また、メモリ使用率が上がったことでなぜReadIOが上がるのかは気になったので調べてみたのですが、明確な原因は分かりませんでした。もしご存知の方がいれば教えて欲しいです。

kubernetesのServiceを使ってプライベートサブネットのRDSに接続する方法

EKSとRDSを利用して構築している環境があるのですが、RDSについてはプライベートサブネットに配置しているため、DB接続するには踏み台サーバを置いてポートフォワードするなどの対応が必要になります。
この方法だと踏み台サーバが必要になるため、以下のような構成でRDSに対して接続経路を用意しています。このPodはsocatが同梱されておりプロキシサーバーとなっています。

Podでプロキシを行いRDSに接続する構成

f:id:cloudfish:20200129221205p:plain

負荷も高いわけではなく特に問題はないのですが、このためだけにPodを起動させておくのもなーと思い、Podを使わずにプライベートなRDSへ接続できる方法がないか検証した結果、以下のような構成で接続できましたので、やり方を紹介したいと思います。ただし、RDSの接続先についてはDNSではなくIP指定となるためあまり使い勝手の良い方法ではないと思います。

Podを使わずにRDSへ接続する構成

f:id:cloudfish:20200129225249p:plain

通常ServiceはPodをメンバとして登録しますが、このケースではEndpointsというリソースをメンバとして登録します。そしてEndpointsからRDSへ接続するという形になります。

ELBの作成

以下の設定でロードバランサーを作成します。
当然ですが、セキュリティグループも以下に合わせて作成しておいてください。
Type: CLB
Protocol: TCP
Load Balancer Port: 13306
Instance Port: 30306

Kubernetesのリソースデプロイ

以下がRDS(MySQL)に接続するサンプルになります。
RDSのIPを自環境に合わせて書き換えてapplyしてください。

apiVersion: v1
kind: Service
metadata:
  name: rds-service 
spec:
  type: NodePort
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
      nodePort: 30306

---
apiVersion: v1
kind: Endpoints
metadata:
  name: rds-service 
subsets:
  - addresses:
    - ip: 10.10.213.27 #RDSのIP
    ports:
    - port: 3306

注意点としてはServiceとEndpointsのnameは同じにしておく必要があります。
また、マルチAZを利用している場合は、フェイルオーバーした場合にIPを書き換えるという一手間が必要になりますので注意してください。

以下のコマンドで接続できます。

mysql -h ELB_DNS -u mysql_user -P 13306 -p

初回接続は何故か数十秒かかります。またkeep aliveを調整しておかないと接続がすぐ切れるので長めに設定しておくことをお勧めします。

まとめ

Podを使わずに接続できたのはよかったのですが、Endpointsには接続先をIPでしか登録できないところがすごく惜しい機能です。DNSが指定できるようになったらいいですね。

EKSのPod起動数の制限

f:id:cloudfish:20200116173700p:plain
最近、いくつかEKSの環境構築を進めているなかで、EKSのPodの起動数の制限がネットワークとインスタンスタイプに依存することが分かりました。すでに知っている人も多いと思いますが、備忘録として残しておきたいと思います。

Podの起動数

通常、Podの起動を考える時に一番気にするポイントとしてはノードのリソース(CPU、メモリ)ではないでしょうか?
当然ですが、ノードインスタンスのCPUやメモリに余裕がないと新たにPodを起動できませんし高負荷時にスケールアウトもできなくなります。
Manifestにもrequestやlimitなどの設定項目がありますしリソースの確保については気を使われているのではないでしょうか?
EKSでは、ノードのリソース状況に加えてネットワークについても意識する必要があります。

Podのネットワーク制限

通常のKubernetesのPod用のネットワークはノードインスタンスとは別の内部ネットワークとなりあまり気にする必要はありません。
しかし、EKSではAmazon VPC CNI plugin for KubernetesというネイティブなVPCを利用可能とするプラグインを用いることで、VPCネットワーク上と同じIPをPodに割り当てることなります。つまり、PodはVPC上のIPを利用して通信するため、Podの起動数がVPCで利用可能なIP数に依存するということになります。
EKSのネットワークの詳細については以下を参照してください。
docs.aws.amazon.com

一般的にAWSのサブネットについては、24ビット以上で切られることも多いと思います。仮に24ビットでサブネットを作成した場合、AWS予約分を除くと利用可能なIP数は251となります。
AWSで指定可能な最も小さな28ビットで作成した場合は、11IPしか利用することができません。
ノードインスタンスをいくつ起動するかにもよりますが、インスタンス自身にもIPが必要となります。
また、RDSやElasticacheなどその他のリソース用のIPも確保しておく必要があるため、実際にPodで利用可能なIPはもっと少なくなります。
さらに、ノードインスタンスのスケールやPodのスケーリングがどれくらいになるかも想定しておく必要があります。
これらを踏まえてネットワーク設計にあたってはIPに余裕をもったCIDRを検討してください。

インスタンスタイプごとの制限

IP数に余裕があって、インスタンスのリソースにも余裕があった場合、1インスタンスに際限なくPodを起動できるのでしょうか?
AWSでは、インスタンスタイプごとに割り当て可能なIP数が制限されているため、こちらも意識しておく必要があります。具体化にはインスタンスタイプごとにアタッチ可能なネットワークインターフェイス(ENI)の数に制限があり、1つのネットワークインターフェイスあたりの利用可能なIP数に制限があります。

実際にc5.large(or m5.large)で見てみると、制限は以下の通りとなります。

ENIの最大数 3
ENIあたりの IPv4 アドレス 10

この場合、利用可能IPは30となります。
特にt系のmicro、smallなどは場合は、利用可能なIP数も少ないため注意する必要があります。
インスタンスタイプごとの制限は以下のドキュメントを参照してください。
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.htmldocs.aws.amazon.com

スケールした場合のIPの消費

例として以下のVPCのケースで考えてみます。

VPC CIDR 192.168.0.0/16

Publicサブネットを2つPrivateサブネットを2つ作成したとして、EKSのノードグループをPrivateサブネットに配置するものとします。

Subnet CIDR 利用可能IP数
Private A 192.168.10.0/24 251
Private C 192.168.11.0/24 251

構成

インスタンスタイプ c5.large
通常時起動台数 10
ピーク時起動台数 20

上記のケースの場合、ノードグループは2つのPrivateサブネットに配置されているため502のIPが利用可能です。
c5.largeは最大で30ip利用可能ですが、1台あたり20ip程度利用していたと想定します。
通常時では200ip程度消費することとなり300程度の余裕があります。
しかし、ピーク時では倍の400ipとなり残りが100しかなくなります。
そうすると、さらに負荷が増大した場合にスケールアウト可能なインスタンスが5台までとなり、あまり余裕がありません。
さらにローリングアップデートやBlue・Greenデプロイメントを行うのであれば、余分にipが必要となるためそれらの考慮も必要となってきます。
通常のEC2であれば、24ビットのCIDRで十分なことも多かったかと思いますが、EKSではかなりのipが消費されることになるため注意が必要です。

まとめ

このようにEKSでは、EC2レベルではなくPodレベルでIPが必要になり、より多くのVPC上のIPを消費するので上記に注意してネットワーク設計を行ってください。
インスタンスタイプについても、想定しているPodが起動可能となるようにタイプを選択しましょう。小さいインスタンスだと想定しているPodが起動できなくなる場合があります。

EKSにおけるSecurity Group変更の注意点

長くなるので結論を先に書きます。
EKSのノードインスタンスにおいてセキュリティグループを変更(アタッチ or デタッチ)する際は、ノードグループを更新しましょう。要するに手動での変更は止めましょうということです。ちなみにアタッチ済のセキュリティグループのルール変更は問題ありません。

手動で変更したために、疎通に関して問題が起こったので経緯と原因について書きたいと思います。

やりたかったこと

以下のような構成でPodからRDSへ疎通させたかった。

構成

f:id:cloudfish:20191202225547p:plain

対応内容

上記のような構成でノードのセキュリティグループを手動で変更しました。
具体的な設定内容としては、自己参照設定されているセキュリティグループををノードインスタンスとRDSにアタッチしました。
そもそもノードグループはオートスケーリンググループのため、手動変更がよくないことは認識していましたが、開発環境ということや諸々の状況から、一時的な対応として手動で新しいセキュリティグループをアタッチしました。(要するに手抜きでやってしまいました。)

発生事象

上記の対応後に、PodからRDSに接続できたりできなかったりする事象が発生。

原因調査

この時点で以下の確認を行いました。

1.セキュリティグループ

ソース、ポートに設定誤りがないことを確認
全てのインスタンスにセキュリティグループが付与されていることを確認

2.ネットワークACL

何も設定されていないことを確認

3.テスト用Podから接続確認

PodをCreateした直後は高確率でRDSに対して接続可能となるが、Podをkillしてリスタートすると接続できたりできなかったりというかなり微妙な結果となりました。特定のノードで接続できないとか特定のPodに問題があるのかなど確認しましたが特にそういった問題は見受けられませんでした。

4.セキュリティグループの設定変更

RDSのSGにおいて、自己参照ではなくインバウンドのソースにSharedNodeSecurityGroup(eksctlでデフォルトで作成されるノードのSG)を指定するように変更したところ、RDSへ接続が可能となることが判明。

この時点では、明確な原因が分かるところまでは調査できませんでした。
他の環境において全く同様の設定でDB接続に問題は発生していなかったため、自己参照のSGに問題があるとは考えづらかったのですが、状況から見ると何か問題がありそうということで、一旦自己参照における設定はやめることとしました。(結果的に自己参照が問題ではありませんでした。)

余談ですが、この調査の際に以下の方法を利用してホスト側に接続しtcpdumpなどで調査を進めました。
SSH接続しなくてよいのでかなり便利です。
dev.classmethod.jp

EKSのネットワークについて調査

原因がはっきりとしなかったため、後日改めてEKSのネットワークについて調べてみました。
EKSのネットワークについては、Amazon VPC CNI plugin for Kubernetesというプラグインを使用することで、PodについてもVPCネットワーク上のアドレスと同じIPを利用することが可能となっています。
このため、ノードインスタンスではPod数分のIPを確保する必要があるため、ネットワークインターフェースを1ノードに複数アタッチする構成となっています。以下AWSの資料です。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/images/networking.png

問題が起こった環境では、インスタンスタイプはt3.mediumを利用していました。このタイプのネットワーク制限は以下となり、最大18個のIPが利用可能となります。
■t3.medium

アタッチ可能なENI数 3
ENIあたりのIPv4 6

ここでENIが複数アタッチされていることに改めて着目しました。
そもそもセキュリティグループはインスタンスに対して割り当てられるものではなく、ENIにセットされるものとなります。通常、あまり複数ENIを利用することも少ないこともあり、EC2のコンソールからはインスタンスにセットされているように見えますが実際にはENIにセットされていることになります。

この時、EC2のコンソールからセキュリティグループを追加した際に全てのENIに反映されていないのではないかと思い至り、検証してみました。
もし上記の通りの動作だとすると、セキュリティグループがアタッチされたENIからのみRDSへ疎通が可能となり、それ以外のENIからは通信できないという動きになると考えられます。

ENIに対しセキュリティグループがどのようにアタッチされるか?

以下のようにセキュリティグループが割り当てられているインスタンスを用意しました。
3つのセキュリティグループが割り当てられています。
f:id:cloudfish:20191203100029p:plain

このインスタンスには以下のように3つのENIがアタッチされており、セキュリティグループについても全て同様となっています。
f:id:cloudfish:20191203100408p:plain
f:id:cloudfish:20191203100430p:plain
f:id:cloudfish:20191203100448p:plain

このインスタンスに対してEC2コンソールからadd_sgというセキュリティグループをアタッチしました。
f:id:cloudfish:20191204103257p:plain

アタッチ完了後にENIを確認したところプライマリENIのみadd_sgが付与されていることが分かりました。
■プライマリENI
f:id:cloudfish:20191203100815p:plain
セカンダリENI
f:id:cloudfish:20191203101018p:plain
f:id:cloudfish:20191203100912p:plain

検証結果から上記の想定通りの動作ということが確認できました。

接続不具合となった原因

改めて状態を図にしてみると以下のようなイメージになります。
EC2コンソールから手動でセキュリティグループを変更するとENI_1にのみadd_sgがアタッチされることとなります。
そのため、ENI_1に紐付くIPが割り当てられたPodからのみRDSへの疎通が可能となり、そうでないENIに紐付くIPが割り当てられたPodからは疎通ができません。ということで、今回発生した事象について原因が判明しました。
f:id:cloudfish:20191204184335p:plain

ちなみに何度か検証した際に気付きましたが、EC2コンソールからのセキュリティグループ変更時には、以下のようにインターフェースIDが表示されていました・・・コンソールはしっかり確認する必要がありますね。
f:id:cloudfish:20191203100654p:plain

全てのENIに手動でセキュリティグループをアタッチすれば問題ないか?

完全に一時しのぎとしてなのですが、仮に全てのENIのセキュリティグループを手動で変更すれば問題ないかという観点でもう少し考えてみました。オートスケーリンググループのテンプレートの問題は一旦おいておきます。
ノードインスタンスは、起動直後にPodの数が少ない場合、ENIは1つしかアタッチされていません。
そこからPodの数が増えて1つのENIで割り当て可能なIP数を超えた場合は、新たにENIがアタッチされることになります。その際、増えたENIはどのようなセキュリティグループがセットされているかを確認してみました。

以下のようなENIが一つのみのインスタンスを用意し、add_sgというセキュリティグループをセットします。
f:id:cloudfish:20191203101518p:plain
インスタンにENIが一つアタッチされている状態です。
f:id:cloudfish:20191203101738p:plain

この状態でPodの数を増やし、各ENIの状態を確認しました。
プライマリENIにはadd_sgが付与されていることが分かります。
f:id:cloudfish:20191203102737p:plain

追加された2つのENIを確認すると、add_sgは付与されていませんでした。
おそらくテンプレートから引っ張ってきてると思うので当然の動作かもしれません。
f:id:cloudfish:20191203102759p:plain
f:id:cloudfish:20191203102815p:plain

ということから、仮に全てのENIのセキュリティグループを手動で変更したとしてもENIが勝手に増える可能性があるため、突然、通信できなくなる問題が起こりそうですね。

まとめ

・ノードインスタンスのセキュリティグループの手動変更はやめましょう。(あまりやらないと思いますが)
・eksctlのドキュメントにもありますが、基本的にノードグループはイミュータブルなものとしてデザインされているので変更する時はノードグループの更新で対応しましょう
・セキュリティグループの変更程度でノードグループの更新はあまりやりたくないので、構築時にしっかりセキュリティグループの設計を行いましょう。

今回は余計なことをして、無駄にハマった感がかなりありますが、改めてEKSについて知ったこともあり非常に勉強になりました。
以上、どなたかの参考になれば幸いです。

Amazon CloudWatch SyntheticsでURL監視を設定

Amazon CloudWatch Syntheticsがプレビューされていましたので早速試してみました。
CloudWatch Syntheticsとは、サービスのエンドポイントやAPIのエンドポイントなどをモニタリングするサービスになります。DataDogでも同様のサービスがありますね。

現時点では、US East (N. Virginia), US East (Ohio), and EU (Ireland)のいずれかのリージョンでし利用できませんので、今回は、バージニア北部を利用しました。

Canaryを作成

以下の画面からCanaryを作成を選択します。
f:id:cloudfish:20191127080636p:plain

デフォルトで以下のように選択されていますのでそのまま次進みます。
f:id:cloudfish:20191127081041p:plain

Canary名と監視対象のURLを入力します。
スクリプトエディタの内容は自動で生成されるます。
f:id:cloudfish:20191127081558p:plain

以降はデフォルトのままとし、Canaryを作成します。
f:id:cloudfish:20191127081926p:plain
f:id:cloudfish:20191127082114p:plain

モニタリングの実行

作成直後はステータスがグレイアウトされていますが、しばらくすると以下のように実行結果が表示されます。
f:id:cloudfish:20191127082405p:plain

詳細を確認するとスクリーンショットやHARファイルなどを見ることができます。
実際の画面を取得してくれるのはすごくいいですね。
f:id:cloudfish:20191127082814p:plain

ログを確認してみると、Lambdaのログに似ていたので、Lambdaのコンソールを確認したところsynthetics用の関数が自動で作成されていました。消したりしないように注意が必要ですね。

f:id:cloudfish:20191127100526p:plain

アラート通知

また、Threshholdsを有効にし、Canaryでしきい値を有効にすると、アラームが作成されますので、これをもとにchatbotやLambdaと連携することでアラート通知が可能になります。

カスタマイズ

スクリプトがカスタマイズできるので、Basic認証やログインしての監視なども可能ですね。
ちなみにログイン情報などの秘匿情報が必要な場合はSecrets Managerの利用が推奨されています。

料金

1canaryあたり月額$0.0012となっています。

まとめ

特にドキュメント読むことなく簡単に設定することができました。また、スクリプトをカスタマイズできるなど拡張性も高いと思いますので、ぜひ使っていきたいですね。

CloudFormationでDataDogのIntegration設定

CloudFormationで以下のアップデートがありサードパーティ製品についてもCoudFormationで設定できるようになりました。
DataDogのIntegration設定などをCloudFormationで設定できるようなので、早速試してみました。
aws.amazon.com

CloudFormationへDatadogリソースの登録

aws cliで以下のコマンドを実行しリソースの登録を行います。
実行前にawscliのアップデートが必要になります。(pip install -U awscli)

aws cloudformation register-type \
    --region ap-northeast-1 \
    --type RESOURCE \
    --type-name "Datadog::Integrations::AWS" \
    --schema-handler-package s3://datadog-cloudformation-resources/datadog-integrations-aws/datadog-integrations-aws-1.0.0.zip

※type-name、schema-handler-packageの設定値についてはこちらgithubを参照してください。

以下の通りRegistrationTokenが表示されれば登録は完了です。

{
    "RegistrationToken": "122c2c39-12c8-4e93-a711-f6eb1234eae0"
}

登録されているかは以下で確認できます。

$aws cloudformation list-types
{
    "TypeSummaries": [
        {
            "Description": "Datadog AWS Integrations",
            "LastUpdated": "2019-11-20T03:13:25.218Z",
            "TypeName": "Datadog::Integrations::AWS",
            "TypeArn": "arn:aws:cloudformation:ap-northeast-1:123412341234:type/resource/Datadog-Integrations-AWS",
            "DefaultVersionId": "00000001",
            "Type": "RESOURCE"
        }
    ]
}


リージョンごとにリソース登録が必要となるようですが、このあたりの設定は今後もっと簡略化されていくと嬉しいですね。

Integration設定

事前に設定するDataDogのAPIキーとApplicationキーを取得しておいてください。
以下のyamlをファイルに保存しCloudFormationから実行し、パラメータにて上記のキーを設定して実行してください。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'IAM Role and IAM Policy for Datadog AWS Integration'
Parameters:
  DatadogAPIKey:
    Description: "Datadog's API Key"
    Type: String
  DatadogAPPKey:
    Description: "Datadog's APP Key"
    Type: String
Resources:
  DatadogAWSIntegrationResource:
    Type: 'Datadog::Integrations::AWS'
    Properties:
      AccountID: !Ref AWS::AccountId
      RoleName: DatadogAWSIntegrationRoleTest
      HostTags: ["env:staging"]
      AccountSpecificNamespaceRules: {"ec2": true, "api_gateway": false}
      DatadogCredentials:
        ApiKey: !Ref DatadogAPIKey
        ApplicationKey: !Ref DatadogAPPKey

DataDogの設定画面を確認すると以下の通り登録されていれば完了です。
f:id:cloudfish:20191121105655p:plain

AWS側のRoleも合わせて作成してくれないか期待しましたが、残念ながらそこまではしてくれないようでした。
また、ExternalIDの取得方法が現状ではなさそうなので今後に期待したいと思いますが、機能が拡充されていくと思いますので、まとめてCloudFormationで管理することが可能になりますね。