AWS Lambdaのコンテナイメージ上のkubectlコマンド実行してEKSを管理してみる

Lambdaのコンテナイメージからkubectlを実行してEKSを操作してみます。AWS Lambda runtime API 辺りが少し面倒ですが、総じてとても便利です。

なじみの人が多いだろうシェルスクリプトで実装していきます。

はじめに

kubectlをLambdaで実行すると便利な場面もあると思っていて、実際に以下のような情報で今までもできたと思います。せっかくコンテナイメージが使えるようになったので、Dockerfileからビルドして利用してみようと思います。

github.com

最初に読んでおくべきもの

まずは日本語の資料で全体像つかむのが良いと思います。

aws.amazon.com

上記にもありますが、Lambdaのサービスと関数をイベント連携させるため、AWS Lambda runtime API を実装していく必要があります。

docs.aws.amazon.com

AWSからもいくつかの言語用にLambda Runtime API実装済みイメージが提供されているもありますが、今回は自分で実装してみます。その際には以下のチュートリアルが参考になると思います。(これ自体はコンテナイメージの説明ではないですが実装はほぼ同じです)

docs.aws.amazon.com

実装

Lambda実行要のIAMロールの作成

Lambda実行用に以下のような名前のロールを作成します。

arn:aws:iam::123456789012:role/kubectl-lambda

以下のような権限を付けました。

  • プライベートのEKSクラスタ扱うので、VPCでのLambda実行権限。
  • ECRからのコンテナイメージ読み込み権限
  • EKSクラスタの認証トークン取得用の権限

f:id:yomon8:20201217130423p:plain

LambdaかあAssumeするための設定もお忘れ無く。

f:id:yomon8:20201217130601p:plain

EKSクラスタへのKubenetesロールのバインディング

上記で作成したIAMロールにEKSのロールをBindingします。

この記事ではkube-systemのPod一覧取ろうと思うので、kube-system名前空間のView権限付けてみます。

$ ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
$ cat <<EOF > lambda-kubectl-role-binding.yml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: arn:aws:iam::${ACCOUNT_ID}:role/kubectl-lambda
      groups:
      - lambda-kubectl
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: lambda-kubectl-rolebinding
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
- kind: Group
  namespace: kube-system
  name: lambda-kubectl
EOF

$ kubectl apply -f lambda-kubectl-role-binding.yml

設定を確認します。

$ kubectl get cm -n kube-system aws-auth -oyaml

イメージの作成

ディレクトリ構成

Dockerfileと二つのShellで実装しています。

$ tree
.
├── bootstrap.sh
├── Dockerfile
├── handler.sh
handler.sh

やりたいことは handler() 関数の中だけで、他は AWS Lambda runtime API への対応となります。

Lambdaは /tmp に書き込み可能なのでkubectlのconfig等は /tmp 配下に設定しています。

#!/bin/sh
set -euo pipefail

handler () {
  EVENT_DATA=$1
  echo "$EVENT_DATA"
  export KUBECONFIG=/tmp/kubeconfig
  echo "update kubeconfig"
  /usr/local/bin/aws eks update-kubeconfig --name ${CLUSTER_NAME} --kubeconfig ${KUBECONFIG}
  echo "exec kubectl" 1>&2
  kubectl --cache-dir /tmp/kube-cache --log-dir /tmp/kube-log get all -n ${K8S_NAMESPACE}
  echo "completed"
}

while true
do
  HEADERS="$(mktemp)"
  # Get an event. The HTTP request will block until one is received
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")

  # Extract request ID by scraping response headers received above
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Run the handler 
  handler ${EVENT_DATA} 

  # Send the response
  curl -s -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "OK"
done

bootstrap.sh

上記の handler.sh を呼び出しています。

aws-lambda-rieというのはLambdaのエミュレータでローカルテスト時に利用します。

github.com

#!/bin/sh

export _HANDLER=/aws/handler.sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie $_HANDLER 
else
  exec $_HANDLER 
fi

Dockerfile

AWS CLI使うために amazon/aws-cli からビルドしてみましたが、AWS CLIを別途インストールすれば、ベースイメージは別のでも大丈夫です。

FROM amazon/aws-cli

# AWS Lambda Runtime Interface Emulator インストール
RUN curl -L -o /usr/local/bin/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
RUN chmod 755 /usr/local/bin/aws-lambda-rie

# kubectlインストール
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.20.0/bin/linux/amd64/kubectl && \
    chmod +x ./kubectl && \
    mv ./kubectl /usr/local/bin/kubectl

# スクリプトの配置
ADD ./bootstrap.sh /aws/bootstrap
ADD ./handler.sh /aws/handler.sh
RUN chmod +x /aws/handler.sh 
RUN chmod +x /aws/bootstrap
ENTRYPOINT ["/aws/bootstrap"]

ローカルテスト

ローカルテストの方法も簡単に書いておきます。

以下のように、イメージをビルドして8080に繋げた形でコンテナ起動します。起動するとすぐに待機状態になります。

$ docker build -t aws-kubectl .
$ docker run -p 9000:8080   aws-kubectl 

別のコンソールから以下のようにアクセスすると処理が動きます。必要に応じてイベントを渡したりも可能です。

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"EVENT":"DUMMY"}'

Lambdaを設定して実行

細かい手順は省きますが、Lambdaに設定していきます。

IAMロールは先程作成したものを設定します。

f:id:yomon8:20201217135739p:plain

DockerのイメージはECRにアップしておいて指定します。

f:id:yomon8:20201217135714p:plain

環境変数を指定します。 今回のスクリプトでは CLUSTER_NAMEK8S_NAMESPACE のみ必須です。

閉域ネットワークでPROXY設定が必要な場合は追加で設定を行います。NO_PROXYの設定辺りが参考になりそうだったので記載しておきます。

f:id:yomon8:20201217135601p:plain

あとは実行するだけです。以下のようにkubectlなじみの出力が取得できました。 (空のクラスタなのでcoredns起動してないですが・・) f:id:yomon8:20201217140121p:plain

所感

気になった点としては、 Lambdaのイメージ変更が1分程度かかります、また、 起動が少し遅いですが、ほとんどの管理タスク程度なら全く問題無い程度。もしかしたら、リアルタイムでの同期処理とかは辛い場面もありそうです。

AWS Lambda runtime API辺りが少し面倒です。

ただ、Cronみたいに使うとすると、ログがCloudWatchに残るのも便利だったり、依存関係で複雑なことすることなくなるので便利なポイント沢山のサービスに感じてます。