EKSクラスタのバージョンアップ手順

f:id:cloudfish:20200116173700p:plain
3/10にEKSでKubernetesのバージョン1.15のサポートが開始されたことに伴いバージョン1.12が5/11でサポート対象外となります。そのため、現在のバージョンが1.12の場合は速やかなアップデートが必要になります。Kubernetesについてはリリースサイクルが早く、最新の3つまでしかサポートされないため可能な限り追従していく必要があります。今回はバージョン1.12のクラスタをアップデートしましたのでその手順について紹介したいと思います。

バージョンアップに関しての考慮ポイント

EKSのリリースサイクル

Kubernetesのリリースサイクルは年4回となっておりおよそ3ヶ月に1回新しいバージョンがリリースされます。EKSもこれに追従しているため、ほぼ同じようなサイクルとなっています。

サポートバージョン

サポートについては最新の3つのバージョンをサポートします。今回であれば、1.15、1.14、1.13がサポートされ、1.12については2020/05/11でサポート廃止となります。そのため、最低、年に1回はバージョンアップが必要となります。

サポート停止日以降に古いバージョンで実行している場合は自動更新

今回のケースでは2020/05/12以降にバージョン1.12のままであれば自動的に1.13に更新されます。
ただし、自動で更新されるのはコントロールプレーンのみとなるためワーカーノードについては自分で更新する必要があります。

クラスタの更新中のアプリケーションへの影響

実行中のアプリケーションは影響を受けないとありますが、注意書きにもあるとおりAPIサービスが短時間中断する場合があるとのことなので、軽微かもしれませんが全く影響を受けないわけではないと思います。実際のアップデート作業ではメンテナンス時間帯を設定しておいた方が安全かと思います。

クラスタの更新に失敗するケース

クラスタの更新については自動で更新されるとはいえ必ず成功するわけではなく失敗する場合もあります。更新に失敗した場合は、そのままの状態で放置されることはなく必ずロールバックされます。以下の条件が満たされない場合は更新に失敗する場合があるようです。

  • クラスターの作成時に指定したサブネットから、2~3の空きIP アドレスがない場合。
  • クラスターの作成時に指定されたいずれかのサブネットやセキュリティグループが削除された場合

マスターとワーカーノードの互換性

Kubernetes では、少なくとも 2 つのマイナーバージョンにおいて、マスターとワーカーノード間の互換性をサポートしています。
例として
マスター(kube-apiserver)が1.15の場合
ワーカーノード(kubelet)は1.15、1.14および1.13がサポートされます。

バージョンアップ手順

今回はバージョン1.12から1.14へ以下の手順でアップデートを行いました。1.15はリリースされたばかりということもあり、1.14へアップデートを行いました。

手順概略

アップデート手順の概略については以下のような流れになります。
1.クラスタアップデート
2.kube-proxyアップデート
3.corednsアップデート
4.cniアップデート
5.(新バージョン)ノードグループ追加
6.(旧バージョン)ノードグループ削除

事前準備

バージョン確認

まずは現行のバージョンを確認しましょう。以下のコマンドを実行してバージョンを確認してください。

#クラスタバージョン
kubectl version --short
#ワーカーノードバージョン
kubectl get nodes
#kube-proxyバージョン
kubectl describe daemonset kube-proxy --namespace kube-system | grep Image | cut -d "/" -f 3
#corednsバージョン
kubectl describe deployment coredns --namespace kube-system | grep Image | cut -d "/" -f 3
#CNIバージョン
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2

確認結果

マスター ワーカーノード cni kube-proxy coredns
現行バージョン Server Version: v1.12.10-eks-aae39f v1.5.0 v1.12.7 v1.12.6 v1.2.2

クラスタのバージョンアップ後にアドオンについてもバージョンアップを行う必要があります。各種アドオンのバージョンアップが必要かどうかは以下の表を確認してください。上記のケースの場合全てのアドオンのバージョンアップが必要となります。

Kubernetes Version 1.15 1.14 1.13 1.12
Amazon VPC CNI plug-in 1.5.5 1.5.5 1.5.5 1.5.5
DNS (CoreDNS) 1.6.6 1.6.6 1.6.6 1.6.6
KubeProxy 1.15.10 1.14.9 1.13.12 1.12.10

上記の表からアップデート後の想定バージョンは以下となります。

マスター ワーカーノード cni kube-proxy coredns
更新後バージョン Server Version 1.14.9 v1.4.9 v1.5.5 v1.4.9 v1.6.6
ポッドセキュリティポリシーの設定確認
kubectl get psp eks.privileged

以下のようなエラーが出た場合は、ポッドセキュリティポリシーをインストールする必要があります。

Error from server (NotFound): podsecuritypolicies.extensions "eks.privileged" not found

ポッドセキュリティポリシーのインストールは以下を参照してインストールしてください。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/pod-security-policy.html#install-default-psp

バージョンアップ

バージョンアップ作業については、マネジメントコンソール、eksctl、aws-cliの3つで作業が可能ですが、今回はeksctlコマンドでの手順を紹介します。なお、eksctlコマンドのバージョンは0.14以降となります。また、可能ならサービスは止める調整をしておいたほうがいいと思います。

クラスタのバージョンアップ(1.12→1.13)

まずはクラスタを1.12から1.13へバージョンアップを行いますので、以下のコマンドを実行してください。時間は10~15分程度かかりました。

eksctl update cluster --name ${eks-cluster-name} --approve

完了後に以下のコマンドでバージョンを確認してください。

kubectl version --short
クラスタのバージョンアップ(1.13→1.14)

1.13へのバージョンアップが完了したら、次は1.14へバージョンアップを行います。先ほどと同様に以下のコマンドを実行してください。時間は20-25分程度かかりました。

eksctl update cluster --name ${eks-cluster-name} --approve

完了後に以下のコマンドでバージョンを確認してください。

kubectl version --short
kube-proxyバージョンアップ(→1.14.9)

クラスタのバージョンが1.14なのでそれに対応するkube-proxyのバージョンは1.14.9となります。以下のコマンドでバージョンアップを実行します。

kubectl set image daemonset.apps/kube-proxy \
    -n kube-system \
    kube-proxy=602401143452.dkr.ecr.us-west-2.amazonaws.com/eks/kube-proxy:v1.14.9

以下のコマンドでバージョンアップされているか確認します。

kubectl describe daemonset kube-proxy --namespace kube-system | grep Image | cut -d "/" -f 3
corednsバージョンアップ(→1.6.6)

corednsの現行バージョンが1.2.2のため、1.6.6へバージョンアップを行います。以下のコマンドを実行します。

kubectl set image --namespace kube-system deployment.apps/coredns \
coredns=602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/coredns:v1.6.6

以下のコマンドでバージョンアップされているか確認します。

kubectl describe deployment coredns --namespace kube-system | grep Image | cut -d "/" -f 3
cniバージョンアップ(1.5.5)

cniの現行バージョンが1.12.7のため、1.5.5へバージョンアップを行います。以下のコマンドを実行します。

kubectl apply -f https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/release-1.5/config/v1.5/aws-k8s-cni.yaml

以下のコマンドでバージョンアップされているか確認します。

kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
新バージョンでノードグループ作成

以下のコマンドで新しくノードグループを作成します。
パラメータは必要に応じて変更してください。cluster.yamlファイルを作成している場合は、バージョンを変更して実行してください。

eksctl create nodegroup \
--cluster default \
--version 1.14 \
--name ${eks-cluster-name} \
--node-type m5.large \
--nodes 2 \
--nodes-min 1 \
--nodes-max 4 \
--node-ami auto

ワーカーノードのバージョン確認

kubectl get nodes
旧バージョンのノードグループ削除
eksctl delete nodegroup --cluster= ${eks-cluster-name} --name= ${eks-workernode-name}

バージョン1.12から1.15へバージョンアップ

もし1.12から1.15へバージョンアップを行う場合は、マスターとワーカーノードの互換性を考慮して作業手順を考える必要があります。クラスタのバージョンを一気に1.15に上げると、ワーカーノードのバージョンが1.12なので互換性が保証されていない状態となります。そのため、以下のように1.15にアップする前に一度ワーカーノードのバージョンをアップしておくほうが安全かと思います。

  • クラスタのバージョンアップ(1.12→1.13)
  • クラスタのバージョンアップ(1.13→1.14)
  • ワーカーノードのバージョンアップ(1.12→1.14)
  • クラスタのバージョンアップ(1.14→1.15)
  • ワーカーノードのバージョンアップ(1.14→1.15)

まとめ

バージョンアップについては、可能であればブルー・グリーンデプロイで切り替える方が安全にバージョンアップが可能だと思います。今回は諸々の制約により現行のEKSクラスタをアップデートする手法を選択しました。
Kubernetesのリリースに追従していく必要があるとはいえ、バージョンアップ作業については、作業手順の確立や関係者の調整なども含めて対応に時間がかかるので先送りにしがちではないでしょうか。
しかしながら、Kubernetesを使うのであれば定期的なバージョンアップ作業も運用の一環として考えていく必要があると思いますので、ぜひ手順を確立するようにしてみてください。

sshuttleで簡易VPN的環境の構築

 昨今のテレワークブームはいかがお過ごしでしょうか?突然の世の中の働き方の転換を迎え、特に社内システム担当の方は非常に大変な状況に見舞われているのではないかと思います。
 ゼロトラストやBeyondCorpなど最先端の企業ではもはやVPNは使わないといったことが取りざたされていますが、実際のところ一足飛びにそういった環境が用意できる訳でもなく、普段からそういったセキュリティポリシーをしっかりと考え仕組みを構築している企業だからこそ可能な取り組みかと思います。
 そういう意味では、やはり昔ながらのVPNというのが簡単かつセキュアにリモートネットワークにアクセスする仕組みになるかと思いますが、そうした環境の構築も少なからず時間がかかるものになります。今回はそうした方々が簡単に導入可能なsshuttleという簡易なVPN環境が構築できるツールを紹介したいと思います。タイトルには簡易VPNと書きましたが、正確にはVPNではありませんのでご注意ください。
 これはSSHができる踏み台サーバーさえあれば、クライアント側でsshuttleをインストールするだけで利用可能になるため導入が非常に簡単なツールになります。

できること

  • ssh経由でリモートのネットワークにアクセスができる
  • リモートネットワークのWebサーバやファイルサーバにポートフォワードを設定しなくてもアクセスが可能

イメージ図

f:id:cloudfish:20200306171654p:plain

前提条件

SSH接続可能なサーバーが構築されていること

インストール方法

sshuttleはクライアント側にインストールするだけになります。
Macの場合のインストール方法は以下になります。

brew install sshuttle

Mac以外は以下を参照してください。残念ながらWindowsは対象外です。未検証ですがWSLから利用できるかもしれません。
github.com

接続方法

上記イメージ図の構成の場合、ターミナルから以下のようなコマンドを実行します
ssh configに以下を設定

Host bastion
  HostName xxx.xxx.xxx.xxx
  User ec2-user
  IdentityFile ~/.ssh/bastion.pem

以下の接続コマンドを実行する

sshuttle --dns -r bastion 10.10.0.0/16

「--dns」オプションを付与しておくと、接続中はリモートネットワークのDNSが参照可能になります。これは結構良いオプションだと思います。例えば、AWSの場合だとRoute53でプライベートゾーンが作成できますが、接続中はローカル端末からもそのゾーンの名前解決が可能になります。
最後のCIDRの「10.10.0.0/16」は、リモートネットワークのCIDRを指定します。複数ある場合はスペース区切りで設定してください。

上記で接続中にしたままにすることで、後はSSHなりDB接続なりファイルサーバへの接続が可能になります。

よりセキュアに接続するには?

sshで公開鍵認証とはいえ、SSHをフル解放するのはあまり精神的によくありませんよね。そこでよりセキュアにする方法としては以下の方法が考えられます。

  • MFAによる多要素認証を行う
  • Session Managerを利用してSSH接続を行う

MFAによる多要素認証

利用者が在宅で作業する場合にはIP制限が難しいと思いますので、鍵認証に加えてMFAによる認証を行うことで、よりセキュアな構成が可能になります。設定方法については以下を参照してください。
また、この方法であればリモートネットワークがAWS以外の場合でも対応が可能です。
dev.classmethod.jp

セッションマネージャを利用してSSH接続を行う

こちらはリモートネットワークがAWS環境前提の話となりますが、踏み台となるサーバをパブリックサブネットではなく、プライベートサブネットに配置してセッションマネージャを使ってアクセスすることできるのでよりセキュアにできます。セッションマネージャについてはSSHが利用できるようになりましたので、これを通じてsshuttleの利用も可能となります。

** 設定方法
SSMエージェントやSession Managerプラグインのアップデートが必要となりますので、以下のブログを参照してアップデートしてください。
dev.classmethod.jp

接続方法

ssh config

ssh configに以下の設定を行います。INSTANCE_IDとAWS_PROFILEについては接続先のEC2のインスタンスIDとAWSアカウントのプロファイルを指定してください。

Host bastion
  User ec2-user
  IdentityFile ~/.ssh/bastion.pem
  ProxyCommand sh -c "aws ssm start-session --target $INSTANCE_ID --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --region=ap-northeast-1 --profile $AWS_PROFILE"
接続

以下のコマンドで接続が可能です。

sshuttle --dns -r bastion 10.10.0.0/16

まとめ

いかがでしょうか?検証してみた印象としては結構簡単に使えるのではないかと思いました。ただし、SSH接続が必要になりますのでエンジニア以外が利用するには少しハードルが高いかもしれませんが、利用してみる価値はあるのではないでしょうか。

aws-google-authとdirenvを使ってEKSのクラスタ切り替えを安全に行う

f:id:cloudfish:20200116173700p:plain:w300

みなさんEKSクラスタの切り替えはどうしてますか?kubectxを利用されている方は多いのではないでしょうか?
kubectxは切り替えは非常に便利なのですが、逆に便利すぎてクラスタの選択を誤って冷や汗をかいたということはないでしょうか。
私自身は10程度のEKSクラスタを扱っていますが、あえて利便性を落として自分がミスしにくいと思う方法で切り替えを行っています。
今回はこのEKSクラスタを安全に切り替える方法について紹介したいと思います。

前提条件

aws.amazon.com

  • aws-google-authがインストールされていること

github.com

検証していませんが、saml2awsでも同じことができると思います。

設定方法

aws-google-authやdirenvの設定方法についてはここでは触れませんので別途調べてみてください。

プロジェクトごとにディレクトリを構成する

- clusterが複数ある場合はプロジェクトのディレクト配下に配置
- clusterのディレクトリにkubeconfigを配置

ProjectA
 ├── cluster_1
 |     ├── .envrc   #direnv用設定
 |     └── .kube    #kubeconfig
 └── cluster_2
 |     ├── .envrc
 |     └── .kube
ProjectB
 └── cluster_1
 |     ├── .envrc
 |     └── .kube
 :
  • kubeconfigの配置

以下のコマンドで直下にkubeconfigを配置できます

aws eks --region ap-northeast-1 update-kubeconfig --name cluster_1 --kubeconfig=.kube/config
  • direnvを設定

- clusterのディレクトリ直下に以下の内容で.envrcを作成する

export KUBECONFIG=$(pwd)/.kube/config
export AWS_PROFILE=test_admin
#zshの場合は以下の設定で右側にawsのprofile名とクラスタ名が表示されます
CLUSTER_NAME=$(grep "cluster: arn:aws:eks" .kube/config |awk -F"/" '{print $2}')
export RPROMPT="%F{154}$AWS_PROFILE:$CLUSTER_NAME%f" 

AWS_PROFILE名はaws-google-authのログイン時に設定しdirenvで設定した名称と同一にする。

aws-google-auth -u hoge@gmail.com -I GOOGLE_IDP_ID -S GOOGLE_SP_ID -D -d 3600 -r arn:aws:iam::123456789012:role/test_admin -p test_admin -R ap-northeast-1

動作確認

cluster_1のディレクトリにcdすると以下のようにAWS_PROFILEとKUBECONFIGが環境変数に設定されます。

$cd cluster_a
direnv: loading .envrc
direnv: export +AWS_PROFILE +KUBECONFIG +RPROMPT
$                                                     test_admin:cluster_1

この後、aws-google-authで該当のAWSアカウントにログインすると、kubectlコマンドが利用できるようになります。

$kubectl get node                               test_admin:cluster_1
NAME                                               STATUS   ROLES    AGE   VERSION
ip-10-10-137-223.ap-northeast-1.compute.internal   Ready    <none>   58d   v1.14.7-eks-1861c5
ip-10-10-148-4.ap-northeast-1.compute.internal     Ready    <none>   58d   v1.14.7-eks-1861c5

仕組み

クラスタ毎にディレクトリを作成してその直下にkubeconfigを配置し、direnvでkubeconfigの切り替えを行います。また、aws-google-authを使いログインするとクラスタにアクセスできるようになります。 
 こうすることで該当のクラスタディレクトリにcdすることでクラスタの切り替えを行いつつ、aws-google-authを利用して不用意にクラスタにアクセスできないようにしています。また、AWSのプロファイル名をdirenvの環境変数と同一にしておくことによって、セッションが残っていたとしてもディレクトリを抜けることによってプロファイルが参照されなくなるので思わぬミスが減らせます。

ディレクトリを抜けてkubectlコマンドを実行した場合、以下のようなエラーになります。

$kubectl get node
The connection to the server kubernetes.docker.internal:6443 was refused - did you specify the right host or port?

メリット、デメリット

メリット

デメリット

  • kubectxに比べてクラスタの切り替えが手間となる。
  • 初期設定に少し手間がかかる。

まとめ

 いかがでしょうか?利便性は少し落ちますが、それよりも安全性に重点を置いた仕組みにしました。このあたりはどの方法を選択してもトレードオフになるのではないかと思います。
 この方法で落ち着いてからは、今のところ冷や汗をかいたような場面には遭遇していませんが、もっといい方法がないかというのも模索していますので、もし他にもいい方法があれば是非教えてください。

EKS構築メモ

f:id:cloudfish:20200116173700p:plain:w300
Amazon EKSを触り始めて4ヶ月ほど経ちました。これまでの構築の経験を踏まえてハマった点やこうした方がいいなどの気づいた点や参考になったブログなど、あまりまとまった内容ではありませんがメモとしてまとめておきたいと思います。

VPCのCIDR設計

  EKSのPodのネットワークについては、VPCのネットワークと同じになるネットワーク方式を採用していることから、Podの数だけIPが必要となります。一般的に/24でサブネットを切ることも多いと思いますが、利用可能なIP数は251となるため、システムによってはかなり少ないIPとなりますので、構築するシステムに合わせて余裕を持ったCIDRを設計しておく必要があります。詳細は以下のブログにまとめていますので参照ください。
cloudfish.hatenablog.com

インスタンスタイプの考慮

EC2についても、インスタンスタイプによってEC2にアタッチできるENIの数とENIあたりに割り当て可能なIP数が異なってくるため、どのくらいのPod数が必要になるかある程度見込んだ上で、インスタンスタイプとノード数を決定する必要があります。詳細は上記のブログで説明してます。

EKSクラスタの管理者権限

デフォルトでは、クラスタの管理者権限については、クラスタを作成したIAMユーザー(or ロール)が保持しています。そのため、気づかずに別のユーザーから利用しようとした場合、kubectlコマンドが権限不足でエラーとなります。別のIAMユーザーから利用したい場合はEKSに対してユーザー追加を行う必要がありますので、以下を参照してください。configmapのaws-authを見ても初期ユーザーは登録されていないのですがこれはどこで管理されてるんでしょうね。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/add-user-role.html

EC2のロールによるPodのアクセス制御

 サービスアカウントを付与しないPodについてはデフォルトでEC2のロールを通してIAMのアクセス権限を取得します。当初はPodごとにアクセス権限を付与できませんでしたが2019/09のアップデートでPodごとにアクセス権限を付与することが可能になりました。
 これは試してみて分かったのですが、PodにはEC2のロールによるアクセス権限かPod(サービスアカウント)に割り当てたアクセス権限のどちらかしか利用できません。ORの関係ではないので注意が必要です。例えばEC2ロールでS3のアクセス権限があり、PodのサービスアカウントでDynamoDBへのアクセスを許可してPodにサービスアカウントを割り当てた場合、S3へのアクセスはできません。
 基本的にはPodごとにアクセス権限を付与することが望ましいと思いますが、そうできない場合は、全ての権限制御をEC2のロールに寄せるなどした方がいいと思います。中途半端に対応すると作業ミスに繋がるので気をつけてください。

Pod(サービスアカウント)ごとのIAMアクセス制御

当初、他のPodと同じアクセス権限が必要だったのでそれ用のポリシーを作ってそれぞれのPodのサービスアカウントに割り当てていました。
以下のように冗長化を避けるためポリシーを共通化しました。
f:id:cloudfish:20200227165756p:plain:w400

当然のことながら、共通化すると変更時にPod_Aの権限のみ変更したいにも関わらず、Pod_Bにも影響が及ぶことになります。
このため、変更の際の影響範囲や変更ポイントを考慮すると、多少冗長にはなりますが、Pod(=ServiceAccount)とポリシーは1対1で作成した方がいいと思いました。
f:id:cloudfish:20200227170919p:plain:w400

dev.classmethod.jp

ワーカーノードの設定変更

EKSのワーカーノードについては、基本的にイミュータブルとなります。そのため、ノードの設定変更については、ノードグループの再作成を行う必要があります。以前、安易に手作業でノードのセキュリティグループを変更したところ、Podの通信が不安定になったことがありました。原因は手作業で変更したことによるものでした。基本的にはワーカーノードの設定変更はノードグループの再作成を行ってください。Podの通信が不安定になった事象の詳細は以下に掲載しています。
cloudfish.hatenablog.com

リソース制限

Podのリソース(CPU、メモリ)制限を行わないと、Podは必要なだけリソースを使用することになります。当初、あまりこのあたりを気にせずに運用していたところ、ワーカーノードが頻繁にNot Readyとなる問題発生しました。直接的な原因はEBSのDiskのReadが高騰したことになりますが、Readが高騰する原因はノードのメモリの使用率が原因でした。(詳細は以下のブログに書いています。)
cloudfish.hatenablog.com

これに対処するにはkube-reservedとsystem-reservedを設定し、kubernetesの動作に必要なリソースやOSの動作に必要となるリソースを予め確保しておくことでPodに割り当てられるリソースを制限します。設定の詳細については以下が参考になります。
qiita.com
kubernetes.io
eksctl.io

eksctlのハマりポイント

cluster.yamlで設定値が小文字だと認識されないしエラーも出ない

cluster.yamlの設定値はキャメルケースとなっていますが、例えば、「volumeSize」を「volumesize」として表記しeksctlを実行した場合、現状ではエラーも出ずに指定した値で設定されないようなので気をつける必要があります。

クラスター作成時にPolicy指定でAmazonEC2ContainerRegistryReadOnlyは不要

AmazonEC2ContainerRegistryReadOnlyは必ず必要となるポリシーのため、eksctl側で付与されているようです。
そのため、作成時に指定すると以下のようなエラーが出ますので気をつけてください。

[✖] AWS::IAM::Role/NodeInstanceRole: CREATE_FAILED – "Property   ManagedPolicyArns contains duplicate values."

パッと見エラー内容だけでは自分が指定したポリシー内で重複しているわけではなったので原因がすぐには分かりませんでしたが、デフォルトで付与されていますので設定は不要になります。

eksctlのノード追加、削除時のエラー

eksctlでノードグループの追加もしくは削除する際に、以下のようなエラーが出る場合があります。

 getting nodegroup stack summaries: failed to find a nodegroup tag (alpha.eksctl.io/nodegroup-name)

これは作成時のeksctlのバージョンが古い場合で、新しいeksctlで作成した場合に起こります。デバッグログで確認してみると分かるのですが、エラー内容としてはノードグループのCloudFormationに指定されたタグが無いためノードグループが取得できずにエラーになっています。どのバージョンからか分かりませんが、付与されるタグは以下のようになっていました。

 旧:eksctl.io/v1alpha2/nodegroup-name
 新:alpha.eksctl.io/nodegroup-name

対処方法について以下の2通りがあります。

作成時点のeksctlのバージョンで作成する。

Macの場合は以下のコマンドで古いeksctlが取得できます。($versionに取得したいバージョンを設定する必要があります。)

curl --silent --location "https://github.com/weaveworks/eksctl/releases/download/$version/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp

今のところ、githubには全てのバージョンが残されているようですが、念の為、クラスターを作ったバージョンのeksctlは残しておいたほうがいいかもしれません。

CloudFormationテンプレートのタグを更新する

「alpha.eksctl.io/nodegroup-name」タグを既存のCloudFormationテンプレートに付与することで対処が可能です。ノードグループが複数ある場合は、全てのCloudFormationテンプレートにタグを付与する必要があります。
タグが正しく設定されていれば、以下のコマンドでノードグループのリストが取得できます。

eksctl get nodegroup --cluster=$CLUSTER_NAME

※ただし、この方法は公式で案内されているわけではないので、テスト環境などでしっかり事前に検証を行ってください。

デプロイ

Kubernetesの便利さはデプロイにあると思います。kubectlコマンドでマニフェストのデプロイも可能ですが、運用面の観点からもツールを使ってデプロイを行った方がいいと思います。ツールはJenkinsX、Spinaker、ArgoCDなど様々なツールがあるので自分に適したツールを選択して利用してください。ちなみに今はArgoCDを使っています。ArgoCDの概要については以下のブログを参照してください、
Argo CDによってGKEでGitOpsをする - Kekeの日記

秘匿情報の管理方法

 デプロイにおいて、DBのパスワードやAPIトークンなどの秘匿情報の管理をどうするかという問題があります。そこで、kubernetesにはsecretという秘匿情報を管理するリソースがあります。ただし、secret自体の定義もマニフェストで管理するとそこに秘匿情報が書かれるのでgitなどにアップしてしまうとセキュリティ上好ましくないことになります。
 より良い方法を模索中ではありますが、現状はAWSのパラメータストアに手動で秘匿情報を登録し、以下のようなコマンドでsecretの登録時にパラメータストアからデータを取得して登録しています。gitにはsecretの登録コマンドをアップすることで秘匿情報を誤ってしてしまうこともありません。

kubectl create -n test secret generic test-secret \
--from-literal=TEST_SECRET=$(aws ssm get-parameter --name test_secret --with-decryption | jq -r .Parameter.Value) \

他にも秘匿情報を管理するためにSealedSecretというツールもありますので参考にしてみてください。
GitOpsでも秘匿情報をバッチリ扱う方法、SealedSecretとは? / How to manage credentials on GitOps - Speaker Deck

※2020/3/5にEKSではKMSをネイティブに使えるようにサポートしました。今後はこれを使う方がいいと思います。
aws.amazon.com

Podが起動しない時のトラブルシューティング

Kubernetesを触り始めた時にPodが立ち上がらなかったりすると、まずどこから調べていいのかよくが分かりませんでした。
そんな時に以下の記事を見つけたのですが、Podのデプロイ時のトラブルシューティングの手順がフローチャートで分かりやすくまとめられていますのでぜひ参考にしてみてください。
learnk8s.io
qiita.com

まとめ

Kubernetesは非常に奥が深いので、まだまだ注意点があると思います。今後運用を続けていく中で気をつける点などEKS特有の問題点などが出てきたら追記していきたいと思います。

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が指定できるようになったらいいですね。