もうずっといなかぐらし

かたいなかのブログ

EKSのIAM Role for Service Accountsを使用するときにはSTSのリージョナルエンドポイントを使うように設定しようという話

先日、DynamoDBにアクセスするAPIをLambdaからEKS上への再実装・載せ替えを行いました。その際、IAM Role for Service Account(IRSA)を使用した一時クレデンシャル取得時のレイテンシが大きく高まる事象に遭遇しました。

原因と解決策を調査から解決に至るまでの過程とともにご紹介します。

TR;DR

IRSAを使用する際は一時クレデンシャル取得時にSTSにリージョナルエンドポイントを使用させるためAWS_STS_REGIONAL_ENDPOINTS=regionalという環境変数を設定することを検討すること。

経緯

現在転職会議ではECS or Lambdaで実装された既存のサービスを順次EKS上に載せ替えていっています。

先日、私がLambdaで実装されていたAPIをGoで再実装しEKS上に載せ替えました。このAPIはDynamoDBの中身をレスポンスとして返すという極めてシンプルなAPIです。

リプレース後、レイテンシの平均をとってみると以前のLambdaでの実装よりも速くはなっていたのですが、1時間おきにレスポンスが2s程度まで跳ねあがる現象が発生しました。

f:id:katainaka0503:20191225185947p:plain
レイテンシの最大値

Datadog APMでレイテンシ急増時のフレームグラフを見てみると、2秒かかっているうちDynamoDBへのアクセスにかかっているのはリクエスト処理時間のうち最後の方の0.2秒程度だけであることが分かりました。

f:id:katainaka0503:20191225191141p:plain

ここまでのことから、DynamoDBにアクセスする前に必要な処理でありかつ1時間おき発生する処理が原因と仮定し、IAM Role for Service Accounts(IRSA)による一時クレデンシャルの取得処理が疑わしいと考えました。

そこで、行われている動作をもう少し詳しく調べるため、ステージング環境でtcpdumpで調べてみることにしました。具体的な手順としては以下です。

  1. kubectl execAPIサーバのコンテナに入る
  2. tcpdumpをコンテナにインストール
  3. tcpdumpでダンプを取得
  4. ダンプファイルをkubectl cpでローカルにコピー
  5. Wiresharkでダンプファイルを見る

ダンプファイルを見てみると、STSとの通信ログからSTSへのアクセスに1.5s程度時間がかかっていることがわかりました。どうやら、Kubernetesの発行したトークンで一時クレデンシャルを取得するSTSAssumeRoleWebIdentityで時間がかかっているようです。 f:id:katainaka0503:20200106180125p:plain

ダンプの中身を詳しく見てみると、DNSの通信ログからそのSTSへのアクセスにグローバルエンドポイント(sts.amazonaws.com)が使用されていることがわかります。

STSでは可能な場合はリージョナルエンドポイント(sts.<region名>.amazonaws.com)を使用することが推奨されています。そこで、AWS_STS_REGIONAL_ENDPOINTS=regionalという環境変数を設定してリージョナルエンドポイントが使用されるように設定しました。

すると、デプロイ直後と1時間毎に発生している一時クレデンシャル取得時のAPIのレイテンシが2秒から0.4秒程度まで大きく改善しました!

f:id:katainaka0503:20200106182058p:plain
レイテンシの最大値

念の為ダンプファイルをとってみると、変更後は一時クレデンシャル取得時にはちゃんとSTSのリージョナルエンドポイントと通信していました。

f:id:katainaka0503:20200106181535p:plain

まとめ

IRSAにより一時クレデンシャルを取得してAWSAPIへアクセスするときはAWS_STS_REGIONAL_ENDPOINTS=regionalという環境変数を設定することを検討してください。この環境変数は最近のバージョンのAWS SDKおよびAWS CLIであれば使用できるようです。(ほかにSTSのリージョナルエンドポイントを使用させるもっとよい方法があればこっそり教えて下さい)

問題調査をするときには必要とあれば抽象化の皮を剥がして調査する能力が大事だと強く感じる事件でした。

参考資料

株式会社リブセンスに入社して1ヶ月が経ちました

はやいもので明日でリブセンス入社1ヶ月目の業務が終わり今年の仕事納めとなります。

今回の記事では私の入社1ヶ月での業務内容や入社1ヶ月目の所感などを書いていきます。

やったこと

キャッチアップ

入社時点で配属先である転職会議のインフラ周りがかなり整っていたことや、1on1等を厚めに行ってくれたこともあり、現状の把握に大きく時間を取られることがありませんでした。

AWS Lambda(Node.js)で作られたAPIをGo(Gin) on EKSにリプレース

  • コピペされることを想定してKubernetesYAMLは基本的なベストプラクティスを意識して作成
    • limit, requestsの設定
    • preStopでsleep
    • PodDisruptionBudget
  • Webサーバも最低限必要そうな事項を意識して実装
  • IRSAを使用したことに関連するパフォーマンスバグ対応(後日記事化)

DatadogのエージェントをEKSクラスタに導入

RDSの証明書更新対応準備

  • メンテ日時の設定
  • ステージング環境でのリハーサル

HackDays(年末最終週がリリース禁止であるため、その間エンジニアが好きな技術で遊んで成果を発表する社内イベント)

その他

  • Node.js製APIサーバにDatadogのAPMのトレーサーを仕込む
  • メールのテスト用に使用していたmailhogをECSからEKS上に移行
  • DynamoDBをオンデマンド化

入社1ヶ月の所感

事業会社のSREとして働くのは初めての経験でしたが、周りの方のサポートもありスムーズに入っていくことができました。一緒に働くエンジニアの方々はみんなそれぞれレベルが高く、担当領域を超えたところにも改善を行っていく強いマインドを持っていて一緒に働いていてとても楽しい1ヶ月でした。

コミュニケーションの面でも1 on 1やエンジニアが全員集まって課題を話し合うMTGなどうまく交通整理されており、かなりやりやすさを感じました。

転職会議のインフラ面での現在の課題である全面EKS化に向け、来年はさらに気合いを入れて頑張っていきたい所存です。

EKS上でもBerglasみたいにクレデンシャルをいい感じにしたい!!!

こちらはAmazon EKS #1 Advent Calendar 2019 4日目の記事です。

こんにちは、かたいなかです。株式会社リブセンスに入社してはや一ヶ月弱が経過しました。

私が所属する転職会議事業部では、ECSからEKSへの移行を進めています。その中で現在議論が白熱しているのがクレデンシャルの管理方法についてです。検討の中で「GCPにはBerglasという便利ツールがあり、Podに渡す環境変数をよしなに指定するとSecret Managerにあるクレデンシャルをいい感じに展開してくれる」という話を同僚に聞きました。

github.com

そこで、今回はMutationWebhookの実装の素振りを兼ねてEKS上でAWS Secrets ManagerからBerglas同様にクレデンシャルを取得するしくみを作ってみましたのでご紹介します。

TL;DR

Beglas風にAWS Secrets Managerからクレデンシャルを取ってきてくれるツール(Not Production Ready)を試験的に作成。MutationWebhookは便利だが劇薬なので用法用量注意が必要。

Berglasとは

BerglasはGCPが提供しているGCP上に保存されたクレデンシャルを便利に扱うツールです。

BerglasのCLIにはberglas exec --というコマンドで実行するラッパーの機能があります。このコマンドを実行する際、berglas://<credentialへの参照>という形式の値を持つ環境変数があると、当該の環境変数を参照先から取得した実際の値に置き換えてラップされたコマンドを実行します。

$ export API_KEY="berglas://<credentialへの参照>"
#"API_KEY"の値が参照先のクレデンシャルの値で置き換わった状態で"--"の後ろに指定したコマンドを実行
$ berglas exec -- some command

MutationWebhook

前節で紹介したCLIツールをどのようにKubernetesと連携させればよいかについては、BerglasのGitHubリポジトリKubernetesのMutationWebhookのサンプル実装が置かれています。(https://github.com/GoogleCloudPlatform/berglas/tree/master/examples/kubernetes)

このサンプル実装では、Podの作成時にberglas://<credentialへの参照>環境変数が設定されている場合、initContainerでBergalsのコンテナからemptyDirのボリュームにバイナリをコピーする設定を追加します((1)の部分)。そして、commandを書き換え、もともと指定されていたコマンドをberglas exec --でラップするように設定します((2)の部分)。以下のようなYAMLが作成されます。

apiVersion: v1
kind: Pod
metadata:
  name: envserver
spec:
  # ボリュームにberglasのbinを流し込むinitContainersを追加(1)
  initContainers:
    - name: copy-berglas-bin
      image: gcr.io/berglas/berglas:latest
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "cp /bin/berglas /berglas/bin/"]
      # berglasのバイナリ流し込み用ボリューム
      volumeMounts:
        - name: berglas-bin
          mountPath: /berglas/bin/
  containers:
    - name: server
      image: katainaka0503/server
      imagePullPolicy: Always
      # もとのPodで指定していたcommandを「"berglas exec --"でラップして実行するように修正(2)
      # command: ["/bin/server"]
      command: ["/berglas/bin/berglas"]
      args: ["exec", "--", "/bin/server"]
      env:
        # berglas://の形式の環境変数がある場合にPodを編集
        - name: API_KEY
          value: berglas://berglas-test-secrets/api-key
        - name: TLS_KEY
          value: berglas://berglas-test-secrets/tls-key?destination=tempfile
      # berglasのバイナリ流し込み用ボリューム
      volumeMounts:
        - name: berglas-bin
          mountPath: /berglas/bin/
  volumes:
    # berglasのバイナリ流し込み用ボリューム
    - name: berglas-bin
      emptyDir:
        medium: Memory

これにより以下のような利便性が提供されます。

  • 環境変数GCP上に保存されたSecretの参照を指定するかたちのため、リポジトリ上でのYAMLの暗号化等は不要
  • 実行時にemptyDirボリューム経由でラッパーのバイナリを埋め込むことで、アプリケーションのDockerfileに手を入れることなくKubernetesデプロイ時にラッパーを実行
  • Podのenv等にクレデンシャルが展開されたりもしないので機密性が高い

ただし、Podの設定のcommand, argsで指定しているコマンドをラッパースクリプトの中で実行させる仕組みであるため、command, argsがPodのYAMLの中で明示的に指定されている必要があります。(Dockerfileで指定したENTRYPOINT, CMDをそのまま使わせることができない)

AWSでもBerglas風にクレデンシャルを管理したい

さて、ここからが本題です。GCPでできることはAWS上でも同じようにやりたくなるのが自然な心理です。そこで、今回はBerglasと同様の仕組みでSecretsManagerから値を取得できるようなWebhookおよびCLIを写経を兼ねて実装してみました。

今回のコードは私が自身の学習用に作成したものです。継続的にメンテナンスするつもりは現在のところないので、そのまま使用することはおすすめしません。

動作

今回作成したものをデプロイした状態で以下のようなYAMLでPodを作成しようとすると、

apiVersion: v1
kind: Pod
metadata:
  name: server
  annotations:
     eks.amazonaws.com/role-arn: hogehoge
spec:
  containers:
    - name: server
      image: server:any-tag
      command: ["/bin/server"]
      # berglas-aws://<Secrets ManagerのARN>の形式の環境変数がある場合にPodを編集
      env:
        - name: API_KEY 
          value: berglas-aws://arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:<SECRET_ID>

以下のようなYAMLに修正されてPodが作成されます。

apiVersion: v1
kind: Pod
metadata:
  name: server
  annotations:
     eks.amazonaws.com/role-arn: hogehoge
spec:
  # ボリュームにberglas-awsのbinを流し込むinitContainersのを追加
  initContainers:
    - name: copy-berglas-aws-bin
      image: katainaka0503/berglas-aws:latest
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "cp $(which berglas-aws) /berglas-aws/bin/"]
      # berglas-awsのバイナリ流し込み用ボリューム
      volumeMounts:
        - name: berglas-aws-bin
          mountPath: /berglas-aws/bin/
  containers:
    - name: server
      image: katainaka0503/server
      imagePullPolicy: Always
      # もとのPodで指定していたcommandを「"berglas exec --"でラップして実行するように修正(2)
      # command: ["/bin/server"]
      command: ["/berglas-aws/bin/berglas-aws"]
      args: ["exec", "--", "/bin/server"]
      env:
        # berglas-aws://<Secrets ManagerのARN>の形式の環境変数がある場合にPodを編集
        - name: API_KEY 
          value: berglas-aws://arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:<SECRET_ID>
      # berglas-awsのバイナリ流し込み用ボリューム
      volumeMounts:
        - name: berglas-aws-bin
          mountPath: /berglas-aws/bin/
  # berglas-awsのバイナリ流し込み用ボリューム
  volumes:
    - name: berglas-aws-bin
      emptyDir:
        medium: Memory

これにより、特定の形式で環境変数を指定するとAWS Secrets Managerに保存した値を取ってきて環境変数に設定した状態でコマンドを実行してくれるようになります。

CLI

CLI部分はBerglasの実装を参考に実装しました。

https://github.com/katainaka0503/berglas-aws

本家Berglasと同様に、ラップされたコマンドを実行する際に、berglas-aws://<SecretsManagerのARN>という形式の環境変数があればSecretManagerから取得した実際のシークレットの値で当該環境変数を置き換えます。

$ export API_KEY="berglas-aws://<SecretsManagerのARN>"
#"APK_KEY"の値が参照先のAWS Secrets Managerに保存された値で置き換わった状態で"--"の後ろに指定したコマンドを実行
$ berglas-aws exec -- some command

Webhookの実装

Webhookの部分も同様にBerglasのKubernetesへのデプロイのサンプルを参考にkube-webhookというフレームワークを使用して作成しました。

https://github.com/katainaka0503/berglas-aws-webhook

こちらも本家と同様の仕組みで、Pod作成時にberglas-aws://<SecretsManagerのARN>という形式の環境変数がある場合にcommandargsをいじってberglas-aws exec --でラップして実行されるように修正します。

Kubernetesへのデプロイ

最後にWebhookをKubernetesにデプロイする部分です。

https://github.com/katainaka0503/berglas-aws-webhook/tree/master/deploy

MutationWebhookを設定する際には、Webhookサーバ用の証明書とkube-apiserverがWebhookサーバが提供する証明書を検証するためのルート証明書が必要です。今回はCertManagerを使用して証明書を発行しました。それ以外は概ね通常のDeploymentとServiceを使用したAPIサーバの構成です。

まとめ

Secret管理方法の検討とMutationWebhookの学習を兼ねてBerglas風にAWS Secrets Managerからクレデンシャルを取得できるツールを作成して遊んでみました。想像の数倍容易に実装できたので意外でした。実装したツールを継続的にメンテナンスしていける組織であれば、このようなWebhookの仕組みを用意する方法がシークレットの管理方法として選択肢に入ってくるのかもしれません。

今回のコード

参考資料

補足: ECSでは

ちなみにECSでは、ここまで紹介してきたようなECSのタスク起動時にクレデンシャルの値をとってきて環境変数に設定する機能がすでに提供されています。タスク定義内のコンテナ定義のsecretsという属性にAWS Secrets Manager/AWS Systems ManagerパラメータストアのARNを指定すれば、参照先から保存された値を取ってきてくれます。

dev.classmethod.jp

EKSでの認証認可 〜aws-iam-authenticatorとIRSAのしくみ〜

こちらはAmazon EKS #1 Advent Calendar 2019 7日目の記事です。

EKSでIAM RoleをUserAccountに紐付けたり、ServiceAccountをIAM Roleに紐付けたりする際、AWSのドキュメントに従って設定してはいるものの、その設定によって実際にどんな処理が行われているかを具体的に知らない方も多いのではないでしょうか?(私も今回の記事のために調べるまではそうでした。)

そこで今回の記事では、Kubernetesの認証認可の仕組みを解説したあと、AWSのIAMの認証情報をKubernetes内のUserAccountに紐付けるaws-iam-authenticatorの動作の仕組みとKubernetesのService AccountにIAM Roleを紐づける仕組みについて設定方法のレベルから一段掘り下げて実際の動作に焦点を当てながら説明していきます。

目次

Kubernetesの認証認可・AdmissionControl

まずは事前知識として、Kubernetesの認証認可およびAdmission Controlの仕組みについておさらいしていきましょう。KubernetesでのAPIサーバへのリクエストは、実際にその内容に従って処理を行う前に以下の各stageで順番に処理されます。

  1. Authentication(認証)
  2. Authorization(認可)
  3. Admission Control (リクエストのバリデーション・変更等)

それぞれ順番に見ていきましょう。

Authentication(認証)

認証方法

KubernetesではBasic認証を使用した方法やクライアント証明書を使用する方法など、様々な認証方法が提供されています。

EKSでは主に以下の2つの認証方法が使用されています。

  • Service Account Tokens
    • ServiceAccountに紐付いたトークンをAPIサーバに送信し、APIサーバがそのトークンを検証します。
    • 後のServiceAccountの節で詳しく説明します。
  • Webhook Token Authentication
    • クライアントからAPIサーバに送られたトークンをさらに別のプロセスにWebhookで投げて検証を委譲します。
    • aws-iam-authenticatorによるIAMとUserAccountのリンクはこれにより実現されています。

認証主体

Kubernetesの認証主体は以下の2種類です。

  • ServiceAccount
  • UserAccount
ServiceAccount

ServiceAccountはKubernetesAPIによって管理される名前空間に紐付いたリソースです。

ServiceAccountごとに、Kubernetesクラスタが発行したトークンをクラスタ内のSecretとして保持しています。Podのコンテナにこのトークンを埋め込み、Pod内のプロセスにこのトークンを使用させることでkube-apiserver から認証されます。

ServiceAccountは以下のようなYAMLで定義します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: test
  namespace: default
secrets: # ServiceAccount作成後のSecret作成に伴って追加される
- name: test-token-mfb7n

ServiceAccountを作成すると、TokenControllerによってトークンを保持する以下のようなSecretが作成されます。

# 見やすいよう項目の順番を入れ替えた
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  # 省略
  name: test-token-xxxxx
  namespace: default
  # 省略
data:
  ca.crt: LS0...(省略)...= # APIサーバのCA証明書がBase64エンコードされたもの
  namespace: ZGVmYXVsdA== # 名前空間名がBase64エンコードされたもの
  token: ZXI...(省略)...= # JWT形式のトークンがBase64エンコードされたもの

このようなSecretはServiceAccountを指定してPodを作成すると、自動でマウントされます。具体的には、後述するAdmission Controllerの仕組みによってPodに以下のような設定が追加されます。

volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
  name: test-token-xxxx
  readOnly: true
  volumes:
  - name: test-token-xxxx
    secret:
      defaultMode: 420
      secretName: test-token-xxxxx

これにより、コンテナ内の/var/run/secrets/kubernetes.io/に以下のような3つのファイルが作成されます。

なお、ServiceAccountを指定せずにPodを作成すると、Podと同じnamepaceのdefault という名前のServiceAccountの指定が追加され、defaultのServiceAccountに紐付いたトークンが埋め込まれます。これも後述するAdmission Controlによるものです。

KubernetesのGo ClientではPodにトークンが埋め込まれた状態で以下のような設定をすることで、APIサーバへのアクセス時にAuthorization: Bearerヘッダでトークンを送信するようになり、認証された状態で操作を行うことができるようになります。(エラーハンドリング処理は省略)

config, err := rest.InClusterConfig()
clientset, err := kubernetes.NewForConfig(config)
UserAccount

UserAccountはServiceAccountとは違い、Kubernetesの外部で管理されているユーザです(=kubectlでUserAccountを作成するようなことはしない)。外部の認証情報と連携して使用されることが意図されており、例えばGKEではGCPのアカウントとリンクしていたりします。

EKSの場合はaws-iam-authenticatorという仕組みを使用し、IAMの認証情報から得たトークンを使用しKubernetes内のユーザに紐付けることができます。こちらは後ほど詳しく解説します。

Authorization(認可)

Kubernetesでの認可はRBAC(Role Based Access Control)と呼ばれる仕組みです。RBACではRole or ClusterRoleで許可する操作を定義し、RoleBinding or ClusterRoleBindingでServiceAccount等に紐付けます。Role or ClusterRoleはAWS IAMロールと名前は似ていますが認証される主体ではなく、むしろ権限を定義するIAMポリシーに近いものです。

以下はClusterRoleとClusterRoleBindingでServiceAccountにPodに対する操作を許可する場合の例です。

まず、ClusterRoleで許可する操作を定義します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

これをClusterRoleBindingでServiceAccountに紐付けます。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: read-pods-test
subjects:
- kind: ServiceAccount
  name: test
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

これにより、ServiceAccountが紐付いたPod内からClusterRoleで許可された操作が行えるようになります。

Admission Control

Kubernetesでは、認証及び認可の処理が終わったリクエストを実行する前に、さらにAdmission Controlという仕組みで内容のバリデーションと修正を行います。

様々なAdmissionControllerが用意されており、基本的にはkube-apiserverと一緒にコンパイルされ同じバイナリにまとめられています。ただし、MutatingAdmissionWebhookValidatingAdmissionWebhook を使用することで Webhookで外部のプロセスにバリデーションおよび修正の処理を委譲できます。このようなWebhoookの設定はKubernetesのリソースとして管理されており、EKSではデフォルトで以下のような設定が生えています

$ kubectl get mutatingwebhookconfiguration
NAME                   CREATED AT
pod-identity-webhook   2019-11-22T12:59:57Z

先程のServiceAccountの解説の中でPod作成時に自動的にトークンがマウントされたり、ServiceAccountが指定されていないときにはdefaultのServiceAccountの指定が追加されていましたが、これはTokenControllerによりPodの作成のリクエストが修正されたためです。

また、後で説明するServiceAccountにIAMロールを紐づける仕組みの中でもトークンや環境変数の設定を MutatingAdmissionWebhook でPodに追加しています。

EKSのUserAccount(aws-iam-authenticator)

EKSではaws-iam-authenticatorによりIAMのエンティティとKubernetesのUserAccount/Groupを紐付けます。

ここでは、紐付けに必要な設定を見たあとで、実際にどのような処理が行われているかを掘り下げてみていきます。

設定内容

aws-iam-authenticatorでIAMのエンティティとKubernetesのUserAccount/Groupを紐づけを定義するのは以下のような aws-auth ConfigMapです。

apiVersion: v1
data:
  mapRoles: |
    - rolearn: arn:aws:iam::XXXXXXXXXXXX:role/<ワーカーノードのロール名>
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes
  mapUsers: |
    - userarn: arn:aws:iam::XXXXXXXXXXXX:user/admin
      username: admin
      groups:
        - system:masters
kind: ConfigMap
metadata:
  # 省略
  name: aws-auth
  namespace: kube-system
  # 省略

EKSのkube-apiserverにIAMの認証情報からトークンを作成してアクセスするにはaws eks update-kubeconfigコマンド等で以下のようなkubeconfigファイルを作成します。

# 省略
users:
- name: arn:aws:eks:ap-northeast-1:<アカウントID>:cluster/<クラスタ名>
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      command: aws
      args:
      - --region
      - ap-northeast-1
      - eks
      - get-token
      - --cluster-name
      - <クラスタ名>

これらの設定により、クライアント側のIAMエンティティとaws-auth ConfigMapで定義した紐付けに従ってKubernetesのUserAccount/Groupとして認証されます。

余談ですが、将来のaws-iam-authenticatorのバージョンでは、このようなCustomResourceにより管理するようになるようです。これにより、1つの大きなConfigMapを使ってすべての紐付けを管理せず、IAMエンティティごとに別々に紐付けを定義するようになるようです。(https://github.com/kubernetes-sigs/aws-iam-authenticator/pull/116)

実際の処理

では、実際には前節の設定でどんな処理が行われているのでしょうか?

aws-iam-authenticator による認証の流れは以下のようになります。

f:id:katainaka0503:20191126011641p:plain

まず、kubeconfigの設定により、kube-apiserverへのアクセス前に、トークンを取得するためaws eks get-tokenコマンドが実行されます。これにより、STSGetCallerIdentityの署名付きURLが発行され、これをもとにトークンが作成されます。

トークンの検証時にはこのトークンから抜き出した署名付きURLでGetCallerIdentityを実行します。GetCallerIdentity では署名付きURLを発行したIAMエンティティの情報が得られます。これにより、クライアントが特定のIAMのエンティティであることが確かめられるというわけです。

クライアントがAPIサーバにリクエストする際には、Authorization: Bearerヘッダでトークンをいっしょに送信します。

ここから先はEKSのコントロールプレーン内での処理です。

トークンを受け取ったkube-apiserverはWebhook Token Authenticationの仕組みでaws-iam-authenticatorサーバにトークンの検証を委譲します。

aws-iam-authenticatorサーバではトークンから抜き出した署名付きURLでGetCallerIdentityを実行します。結果として得られたIAMのエンティティの情報とaws-auth ConfigMapの情報を突き合わせてIAMのエンティティに紐づくUserAccount/Groupを取得します。そして、kube-apiserverにUserAccount/Groupの情報を返します。

このような流れにより、リクエストが特定のグループの特定のユーザからのものとして認証されます。

ServiceAccountのIAM ロール(IRSA)

先程のaws-iam-authenticatorの仕組みは、IAMのエンティティをKubernetesのユーザアカウントに紐付ける仕組みでした。

逆に、Kubernetes内部にいるPodのService Accountの認証情報を使用してIAM Roleを引き受けるためにはいくつかの方法があります。

  • Service Accountのトークンを使用してIAM Roleを引き受ける(IAM Role for Service Account 以下 IRSA)
  • ノードのiptablesをいじくって、メタデータエンドポイントへのアクセスを横取りしていい感じにAssumeRoleできるようにする(KIAM/kube2iam)
  • AWSの認証情報をSecretにして埋め込む

ここでは、IRSAによりService AccountにAWSのロールを紐付ける方法をみていきます。

実際にはeksctlの便利なコマンドが用意されているため、実際にどのような設定が行われるのか深く意識せずとも使えますが、ここでもあえて一段掘り下げて何をやっているのかを順番に見ていきます。

概要

実際の動作のフローとしては以下のようになります。

  1. EKSクラスタが発行したトークンを信頼させるため、IAMのOIDCプロバイダーというエンティティを作成

  2. IAMロールがアノテートされたServiceAccountを準備

  3. ServiceAccountのトークンでのAssumeRoleを許可したIAMロールを準備

  4. このServiceAccountを指定したPod作成時、AdmissionControllerによりSTS向けのServiceAccountのトークンの埋め込みと必要な環境変数の設定が行われる

  5. 以下に示すバージョンより新しいAWS SDK動作時にはコンテナに埋め込まれているトークンを元にAssumeRoleWithWebIdenityを実行し、IAM Roleを引き受ける

図にすると以下です(Amazon Web Services ブログの記事より転載させていただきました)

https://d2908q01vomqb2.cloudfront.net/ca3512f4dfa95a03169c5a670a4c91a19b3077b4/2019/08/12/irp-eks-setup-1024x1015.png

設定

では実際の設定を見ていきながらそれぞれ確認していきましょう。

AWSでOIDCプロバイダーを作成

まず、IAMのOIDC providerというエンティティを作成します。これを作成することで、EKSクラスタから発行されたトークンを信頼するようになり、AssumeRoleWithWebIdentityによるWebフェデレーションが行えるようになります。

eksctlでは以下のようなコマンドで実行しますが、

eksctl utils associate-iam-oidc-provider \
            --name floral-mongoose-1574427060 \
            --approve

これは以下のようなコマンドと同等です。

EKSクラスタから発行されるIDトークンのISSUERのURLを取得し、IAMにOIDCのプロバイダとして設定することで、EKSクラスタが発行したIDトークンを信頼させています。

ISSUER_URL=$(aws eks describe-cluster \
                    --name irptest \
                    --query cluster.identity.oidc.issuer \
                    --output text)

aws iam create-open-id-connect-provider \
        --url $ISSUER_URL \
        --thumbprint-list $ROOT_CA_FINGERPRINT \
        --client-id-list sts.amazonaws.com

ServiceAccountとIAMRoleの作成

次にServiceAccountおよびそこからAsssumeRoleできるIAM Roleを作成していきます。eksctlを使用すると以下のようなコマンドで実行できます。

eksctl create iamserviceaccount --cluster floral-mongoose-1574427060 --name serviceaccount-iamrole-test --attach-policy-arn arn:aws:iam::aws:policy/PowerUserAccess --approve

このコマンドは、実際には以下のような信頼ポリシーを持つIAMロールを作成し、

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "$PROVIDER_ARN"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${ISSUER_HOSTPATH}:sub": "system:serviceaccount:default:<service account>"
        }
      }
    }
  ]
}

以下のようなアノテーションがついたServiceAccountを作成しています。

eks.amazonaws.com/role-arn=<IAMロールのArn>

信頼ポリシーを見ると、Service Accountから発行されたトークンを使用してAssumeRoleできるように設定されていることがわかります。

PodでServiceAccountを指定

最後にPodでこのServiceAccountを指定し、Pod内のプロセスがIAMロールを引き受けられるようにします。

AssumeRole先のIAM Roleに関するアノテーションがついたServiceAccountを指定してPodを作成すると、 MutatingAdmissionWebhookで処理が委譲されたamazon-eks-pod-identity-webhookにより、Podにいくつか設定が追加されます。

まず、KubernetesのService Account Token Volume projectionという仕組みでaudsts.amazonaws.comとなっているIDトークンが発行され、/var/run/secrets/eks.amazonaws.com/serviceaccount/tokenに埋め込むように設定が追加されます。

https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection

    volumeMounts:
    # 省略
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token

そして、トークンの場所及び引き受けるIAMロールを表す環境変数が追加されます。

  containers:
  - env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::795113267886:role/eksctl-floral-mongoose-1574427060-addon-iams-Role1-1BHQ40Q5V2FWF
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

このように環境変数が設定されトークンが埋め込まれている場合、ある一定のバージョン以上のAWS SDKであれば勝手にトークンを使用してAssumeRoleWithWebIdentityでIAMロールを引き受けてくれます。

このように、IRSAは、Kubernetesの外部audience向けにService AccountのIDトークンを発行する仕組み(Service Account Token Volume Projection)とAWSのウェブ ID フェデレーションの仕組みがうまく噛み合うことで実現されています。

まとめ

aws-iam-authenticatorおよびIRSAの仕組みを解説しました。

aws-iam-authenticatorは署名付きURLを用いてクライアントが実際にIAMで認証されていることを検証する巧妙な方法で実現されていました。またIRSAの仕組みはOIDCというオープンな仕様の上でKubernetesAWSの実装がうまく噛み合って実現されていました。

個人的に気になりつつも調べきれていなかったところではあったのですが、アドベントカレンダーという機会で半強制的に自分にプレッシャーを掛けて勉強できたので良かったなと感じています。

これからもEKSやっていくぞ!

参考資料

全体

aws-iam-authenticator

IRSA

CKA受験ログ

f:id:katainaka0503:20191114124257p:plain
CKA合格証

ねんがんのCKAに88%で合格(合格ライン74%)したので、事前に行った勉強内容等を紹介する。

TL;DR

試験概要

3時間で24問の問題を解き、74%以上得点できれば合格。得点は問題ごとに異なり、受験時には現在表示している問題の配点が画面上に表示されている。

問題とターミナルが表示されるタブに加え、1つだけ以下の3つのサイトを参照するためタブを開くことが許されている。

そのため、基本的には kubernetes.io/docs/ を開いているタブを参照しながら問題が表示されているタブ内のターミナルで解答していくことになる。

準備が必要なもの

  • 試験を受けるのに使用するPCやWebカメラ
    • MBPで問題なかった
  • 試験会場の確保
    • 私の場合は貸し会議室を試験時間3時間に前後30分余裕をもたせて4時間確保した
  • 身分証明書2種類
    • パスポート等の顔写真と署名の入ったもの
    • クレジットカード等の署名の入ったもの(私が受けた際には提示を求められなかった)

学習内容

勉強開始時点の知識(試験本番 2週間前)

  • EKSを使用したインフラの構築経験(技術検証程度)
  • Kubernetes 完全ガイドを半年前に1周読んだ状態
  • Amazon ECSを使用したインフラの構築・運用経験

試験概要・範囲の確認

何よりも先に公式の資料を読んで試験の概要や範囲を確認した。

Kubernetes 完全ガイド

https://www.amazon.co.jp/dp/B07HFS7TDT/

読みながらひたすら手を動かした。

  • 各リソースがどのような役割を持っていてどのような設定値があるか理解した
  • kubectl cordonkubectl rollout 等、馴染みの低かったコマンドを手を動かしながら意識して頭に入れるようにした
  • Helm等の周辺のプロダクトの章は試験対策としてはやらなかった

Kubernetes The Hard Way

Kubernetes The Hard Wayというリポジトリで紹介されている手順で何度かクラスタを組んだ。Kubernetesを構成するコンポーネントを一つ一つ手動で立てていくHardな手順であるため、Kubernetesの各コンポーネントの理解につながった。また、後述する演習問題をやる際にもここで作成したリポジトリを使用した。

この手順の中ではkubeletkube-apiserver等をsystemdを使用して立てていくため、systemdの復習も軽くしておいた。

kubectl の練習

スムーズに回答できるようにkubectlの機能を復習しておいた。

練習問題で手を動かす

一通り試験に出そうな内容が頭に入った状態にしたところで、手を動かす練習に移行した。

具体的にはHard Wayで作成したクラスタ上で、以下の2つのリポジトリを使って演習を行った。

公式ドキュメントのTasksをななめ読み

範囲に入ってそうなところをサラッと流し読みした。一通りの内容がドキュメントを参照すれば実行できる状態にしておいた。

Tasks - Kubernetes

その他

  • 試験会場として貸し会議室を借りた。試験時間が3時間のため前後30分余裕をもたせて4時間確保した。
  • 身分証明書としてパスポートとクレジットカードを準備した

やらなかったこと

  • Linux FoundationやUdemyのCKA対策コース

当日

  • 試験官とのやり取りはすべて英語でのチャット
  • カメラでの会場の様子の確認と身分証明書の確認が終わり次第試験開始
  • 試験中、通信が切れて再接続したりトイレに行きたくなったりしたが、チャット上で英語で説明すれば問題なく対応してもらえた
  • 後半明らかに面倒そうな問題が出てきたが、すぐに捨てる判断をして正答できそうな問題の見直しに時間を割いた

感想

用意された環境に対して3時間ひたすら手を動かす試験であり、問題を解いていて楽しかったが体力をかなり消耗した。時間勝負の試験でありいかにスムーズに設定できるかが重要となるため、事前に手を動かして練習しておく重要性を感じた。

追記: CKADも合格した

CKA受験から3日後に特に追加の勉強をすることなくCKADを受験したが、91%(合格ライン66%)で合格できた。

範囲はCKAからクラスタの管理の話題を除いたサブセットである印象だったが、CKAよりずっとスピードを求められ、受験した際はかなり時間不足を意識させられた。そのため、不安な方は以下のリポジトリ等で手を動かす練習をしてから受験することをおすすめする。

約2年間勤めたクラスメソッド株式会社を退職しました

仕事内容

2017年9月の入社以降しばらくは、ScalaRuby等でアプリのバックエンドのシステムのサーバサイドエンジニアをやってましたが、紆余屈折あってDevOps支援エンジニアとしてコンテナやCI/CDの導入支援等に携わり、最終的にB2BのサービスのSREに流れ着きました。

DevOps支援室時代は案件的にAWSのCodeシリーズを利用したCI/CD環境の導入やECS/EKSの技術支援に関わっていました。業務からちょっと外れたところでも、イケてる会社さんの取り組みを聞かせて頂いたりハンズオンを開催したりと、在籍期間中で一番楽しかった時期でした!

以下はDevOps支援室時代にマネージャだった藤村新さん(@aratafuji)にDevOpsDays Tokyo 2019でいじられる私の図

直近では tjinjin氏(@tjinjin)、wreulicke氏(@wreulicke)、ポン骨氏(@ponde_m)からの誘いもあり、事業開発部に社内転職してB2Bサービスに関わるSREとして動いていました。

具体的な業務としては、AutoScalingの導入、既存のTerraformのコードの運用フローの整備、Codenized Toolsで作られたレガシーのTerraformへのマイグレーション、増強縮退をはじめとする日々の運用オペレーション、障害対応などです。

良かったこと

ブログを書くことだけでなく外部向けの勉強会の開催等、アウトプットを後押ししてくれる会社でした。学んだことをアウトプットしていく習慣を身についたのは大きいなと感じています。

また、技術が好きな人ばかりなので、技術的な話が日常的にできるのは良いと思いました。特にサーバサイド/インフラの技術に関してはそれぞれ一家言ある人が揃っているので、議論しててとても楽しかったです。

なんでやめたの

入社以降一通り業務をやってきた中で、外の世界で自分がどれだけやれるのかを試してみたい気持ちが高まっていました。そんな折、仲の良かったのエンジニアが転職先で楽しそうにしているのを見るイベントが発生し、今がタイミングだなと思い転職活動しました。詳しい話は飲みの席で聞いてください。

今後

12月から株式会社リブセンスさんでお世話になります。

業務内容は今と同じAWS上で構築されたシステムに関わるSREですが、Kubernetes等使用した比較的モダンな環境を触ることになるのでニート期間にキャッチアップしていく所存。

ほしいものリスト

Fastly Yamagoya Meetup 2019に行ってきた

2019/10/23 (水)に開催されたFastly Yamagoya Meetup 2019に行ってきた。

Twitter等で共有されてた登壇スライド

Measuring the performance. Let’s own your analytics tool.

Leveraging Cloud Portability with Fastly

CDNフル活用でつくる、高速Webアプリ / Using CDN To Improve Web Performance

レガシーなサイトにFastlyを入れて改善した道のり

なぜCDNを移行をしようと思ったか/ DAC 's CDN Migration Case Study

感想

CDNはCloudFrontでいいだろ」みたいな思考停止をしていた自分(Fastly歴1日)にはとても刺激が強いイベントだった。

午前中はFiddleを使用したVCLのワークショップ。CloudFrontの設定変更の遅さに慣れていた自分にとっては設定変更して30秒弱待つだけで動作確認できるのはかなり驚きが強かった。(Lambda@Edgeの設定変更遅い)

VCLでちょっとしたコードを書くだけで地域判定してコンテンツを出し分けたり、IPベースでブロックしてみたりと非常に便利な印象だった

午後はトークセッションだったが「Fastly導入してみた」でとどまるような発表はなくそれぞれの内容がとても濃かった。

CDNでのヒット率を上げるためのキャッシュ戦略の話や、キャッシュのパージを自動化 or 非エンジニアが行える仕組み等の実際にどうやって運用しているかの話も面白かった。

発表を聞く中でFastlyの機能がかなりかゆいところに手が届いているように感じた。紹介されてた中で自分的に良いなと思ったのは以下のような機能。(一部ワークショップで聞いた機能あり)

  • 高速パージ
  • Terraformでの管理(terraform-provider-fastly)
    • 変更時にDraft版の変更だけにとどめて、本番でのactivateは手動でできたりする(activate=false)
  • VCLでの柔軟な処理が簡単に組める
    • クエリストリングの正規化によるキャッシュ率向上・HTTPヘッダの整理
    • IPからリージョン/国を判定してそれをもとにコンテンツ出し分け
    • ログ出力の柔軟さ
  • リアルタイムなメトリクス取得
    • Datadogとの連携も可能

実際に触ってみるともっと良さが見えてきそう。

個人的な話としても次の職場ではFastlyが導入されていると聞いてるので、高まったモチベーションで勉強進めたい。